C++多线程:从基础讲解到线程池实现

1.引言

为什么需要多线程?

  • 提高吞吐量:通过并行处理多个任务
  • 增强响应性:保持UI线程响应同时后台处理任务
  • 优化资源利用:在I/O等待期间执行其他计算
  • 简化建模:对某些问题域更自然的表达式

线程与进程

特性 进程 线程
资源占用 独立内存空间 共享进程内存
创建开销
通信方式 IPC(管道、共享内存等) 共享变量
安全性 隔离性好 需同步防止数据竞争

并发与并行

  • 并发:逻辑上同时执行(单核上下文切换)
  • 并行:物理上同时执行(多核真正同时运行)

2.C++线程基础

创建线程的语法

可以通过创建std::thread对象,把一个函数交给它去执行。

示例1:最简单的线程

c++ 复制代码
#include <iostream>
#include <thread>

using namespace std;

void say_hello() {
    cout << "say_hello线程ID:" << this_thread::get_id() << endl;
    cout << "say_hello线程。\n";
}

int main() {
    cout << "主线程线程ID:" << this_thread::get_id() << endl;
    thread t(say_hello); // 创建线程,执行 say_hello 函数
    t.join(); // 等待线程执行完毕(阻塞主线程)
    cout << "主线程结束。\n";
    return 0;
}

说明:

  • thread t(say_hello):开启新线程执行say_hello
  • t.join():主线程等子线程执行完后再继续

示例2:传递参数到线程

c++ 复制代码
#include <iostream>
#include <thread>

using namespace std;

void print_number(int x) {
    cout << "Number: " << x << endl;
}

int main() {
    thread t(print_number, 42);
    t.join();
    return 0;
}

参数是值传递 。如果要传引用,要用std::ref():

c++ 复制代码
#include <iostream>
#include <thread>

using namespace std;

void add_one(int& x) {
    x += 1;
}

int main() {
    int n = 10;
    thread t(add_one, ref(n)); // 用 std::ref 传引用
    t.join();
    cout << "n = " << n <<endl; // 输出 11
}

为什么要用ref()来传引用?

如果这里thread t(add_one, ref(n));直接写成thread t(add_one, n);

这样看起来是想传引用,实际上传的是值复制,因为:

thread的构造函数按值 复制参数到新线程,所以num被复制成了一个新副本,函数里拿到的是int而不是int&,直接编译报错。

ref()会返回一个reference_wrapper<T>,这个包装器是对引用的一个代理,它告诉thread要按引用传进去。

线程生命周期:join()VSdetach()

join():主线程等待子线程执行完

detach():主线程不管子线程,线程在后台运行

线程一旦join()detach()过,就不能再调用,否则会报错。

创建线程的多种方式

c++ 复制代码
#include <iostream>
#include <thread>

using namespace std;
// 1. 函数指针
void thread_func(int x) {
    cout << "Thread function: " << x << endl;
}

// 2. Lambda表达式
auto lambda = [](int x) {
    cout << "Lambda thread: " << x << endl;
    };

// 3. 函数对象
struct ThreadFunctor {
    void operator()(int x) const {
        cout << "Functor thread: " << x << endl;
    }
};

int main() {
    thread t1(thread_func, 42);
    thread t2(lambda, 123);
    thread t3(ThreadFunctor(), 789);

    t1.join();
    t2.join();
    t3.join();
}

3.线程同步与mutex

为什么需要同步?

多线程虽然强大,但当多个线程同时修改同一个变量 时,会出现数据竞争

举个例子:

C++ 复制代码
#include <iostream>
#include <thread>

int counter = 0;

void increment() {
    for (int i = 0; i < 10000; ++i) {
        ++counter; // 可能多个线程同时修改
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Final counter: " << counter << std::endl; // 输出可能 < 20000
    return 0;
}

我们看到,实际上其运行结果低于期望结果,因为两个线程同时修改counter,产生了竞争条件

为什么会产生这样的结果?

我们有看到实际操作的是这一条代码:

c++ 复制代码
++counter;

这并非"原子"的代码,因为这一条代码CPU会将它拆分为以下多个步骤:

  1. 从内存中读取counter到寄存器
  2. 寄存器中+1
  3. 把寄存器结果写回内存

当两个线程同时运行++counter,线程交叉执行,模拟一下:

初始:

ini 复制代码
counter = 0

理想情况:

ini 复制代码
线程A : counter = 0 -> +1 ->写入 1   counter = 1
线程B : counter = 1 -> +1 ->写入 2   counter = 2

实际可能的情况(竞争条件):

ini 复制代码
线程A: 读取 counter = 0  
线程B: 读取 counter = 0   都读到旧值
线程A: +1 得到 1          
线程B: +1 得到 1          
线程A: 写入 counter = 1  
线程B: 写入 counter = 1   覆盖了A的结果
最终结果 counter = 1,少加了一次

这就是数据竞争

  • 多个线程同时访问、修改同一个变量
  • 至少有一个是写操作
  • 没有加锁同步,就会发送不可预测的结果

运行顺序不同,结果也不同,这种现象是非确定性行为

在前面的代码中,还有另外一个问题:

为什么只有counter受到影响,而外层for (int i = 0; i < 10000; ++i)循环没有被影响?

其关键区别在于:变量作用域 +存储位置

再次分析代码:

C++ 复制代码
void increment() {
    for (int i = 0; i < 10000; ++i) {
        ++counter; // 共享资源:有问题
    }
}

int i = 0局部变量,作用域在每个线程内部:

  • 每个线程都有自己的一份i
  • 存在在当前线程的空间中
  • 不同线程的i完全互不影响

因此,for循环本身是每个线程自己控制的,互不干扰。

而counter是一个全局变量,所以:

  • 所有线程访问的是同一份内存地址
  • 当多个线程同时修改它,没有保护就会出问题

其实可以验证一下for

c++ 复制代码
void increment(int id) {
    for (int i = 0; i < 5; ++i) {

        cout << "Thread " << id << " i= " << i << endl;
    }
}

int main() {
    std::thread t1(increment,1);
    std::thread t2(increment,2);

    t1.join();
    t2.join();

    return 0;
}

如果你运行,虽然能够看到cout << "Thread " << id << " i= " << i << endl;确实被打印了10次,但,打印情况并不理想;原因还是很简单:

  • cout是线程安全的输出流(同步到缓冲区)、全局资源;
  • 并不是线程安全的;
  • 它只是保证"不会崩溃",并不保证"按逻辑顺序打印";

因此看到的是拼接在一起的乱码,都是并发访问同一输出流时未加锁造成的输出"撕裂"现象

如何解决上述的问题?

互斥锁(std::mutex

互斥锁是一种资源保护机制:某段代码在任何时刻只能被一个线程执行。

改进代码:

c++ 复制代码
#include <iostream>
#include <thread>
#include<mutex>

int counter = 0;
std::mutex mtx; //定义全局互斥锁

void increment() {
    for (int i = 0; i < 10000; ++i) {
        std::lock_guard<std::mutex>lock(mtx); //自动加锁+作用域自动释放
        ++counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Final counter: " << counter << std::endl; // 输出 20000
    return 0;
}

使用互斥锁能够保证数据正确性,但会牺牲一部分性能;因此使用时,只保护临界区最佳。

在上面的代码中,我们创建了两个线程,又因为其机制,每个线程又各自执行了两次for,又加锁牺牲了一点性能。那么这种情况直接使用单线程不是更好?

很简单,因为我们任务量小。

当任务可以并行,使用多线程本质目的就是:加速

多线程本质是并发处理,多线程不是为了重复跑两遍,而是为了"并行分工、更快完成"。

如果我们把任务量成倍增加,是否使用多线程就更好?

由于上述代码有涉及到共享变量问题,在使用互斥锁会牺牲一点性能。回答问题之前需要先搞清楚两个问题:

1.加锁为什么能防止数据共享冲突?

c++ 复制代码
std::mutex m;
int counter = 0;

void increment() {
    for (int i = 0; i < 1000000; ++i) {
        std::lock_guard<std::mutex> lock(m);
        ++counter;
    }
}

这段代码关键在于:

  • 每次 ++counter 都会先 lock 互斥量,再执行操作,然后自动 unlock
  • 这样多个线程在同一时刻最多只有一个能进入这段代码
  • 所以就不会出现多个线程同时读/改写 counter造成的"数据覆盖"或"丢失"

这就叫临界区保护

2.加锁会不会影响性能?

答案是:会的,但不是"性能废掉",而是"保护数据+牺牲并行度"

是否需要加锁,有几种情况:

情况 是否加锁 效果
所有线程操作共享变量(如 counter 不加锁 结果错误、数据竞争
所有线程操作共享变量 加锁 结果正确,但性能下降(串行执行临界区)
所有线程操作自己变量 不用加锁 并行度高,结果也对!理想情况

因此,当任务量很大,能够并行,即使需要加锁,多线程依然是很好的选择。

当我们把上面代码中循环次数再次扩大N倍,该如何使用多线程?答案是

局部统计+最后合并

示例:

c++ 复制代码
#include <iostream>
#include <thread>
#include<mutex>
#include<vector>

using namespace std;

int counter = 0;
std::mutex mtx; //定义全局互斥锁

const int NUM_THREADS = 4; //线程数量
const int INCREMENTS = 1000000;//单次统计次数

void version() {
    int local = 0;  //局部变量
    for (int i = 0; i < INCREMENTS; ++i) {
        ++local;
    }
    lock_guard<mutex> lock(mtx);
    counter += local;
}

int main() {
   
    vector<thread> threads;
    //创建线程,执行version
    for (int i = 0; i < NUM_THREADS; ++i) {
        threads.emplace_back(version);
    }

    for (auto& t : threads) {
        t.join();
    }
    //输出结果
    cout << "Version result:" << counter << endl;
    return 0;
}

仅仅这样的写法看不出其速度到底多快、安全。因此,在此基础上改进:

  • 版本1:所有线程不加锁直接操作共享变量(不安全,结果错乱)
  • 版本2:所有线程直接操作共享变量,每次操作都加锁(安全但慢)
  • 版本3:每个线程操作自己的局部变量,最后加锁合并(安全且高效)
c++ 复制代码
#include <iostream>
#include <thread>
#include<mutex>
#include<vector>
#include<chrono>

using namespace std;

int counter1 = 0;
int counter2 = 0;
int counter3 = 0;
mutex mtx; //定义全局互斥锁

const int NUM_THREADS = 4; //线程数量
const int INCREMENTS = 1000000;//单次统计次数

//版本1:不加锁直接操作共享变量,不安全
void version1() {
    for (int i = 0; i < INCREMENTS; ++i) {
        ++counter1;  // 数据竞争,结果会错乱
    }
}

// 版本2:每次操作都加锁,安全但慢
void version2() {
    for (int i = 0; i < INCREMENTS; ++i) {
        lock_guard<mutex> lock(mtx);
        ++counter2;
    }
}

// 版本3:先累加到局部变量,最后加锁合并,安全且高效
void version3() {
    int local = 0;  //局部变量
    for (int i = 0; i < INCREMENTS; ++i) {
        ++local;
    }
    lock_guard<mutex> lock(mtx);
    counter3 += local;
}

int main() {

    // 版本1测试
    {
       
        vector<thread> threads;
        auto start = chrono::steady_clock::now();

        for (int i = 0; i < NUM_THREADS; ++i) {
            threads.emplace_back(version1);
        }
        for (auto& t : threads) {
            t.join();
        }
        auto end = chrono::steady_clock::now();
        chrono::duration<double> elapsed = end - start;

        cout << "Version 1 (no lock) result: " << counter1 <<endl;
        cout << "Time taken: " << elapsed.count() << " seconds\n\n";
    }
    cout << "==========================" << endl;
    
    // 版本2测试
    {
        
        vector<thread> threads;
        auto start = chrono::steady_clock::now();

        for (int i = 0; i < NUM_THREADS; ++i) {
            threads.emplace_back(version2);
        }
        for (auto& t : threads) {
            t.join();
        }
        auto end = chrono::steady_clock::now();
        chrono::duration<double> elapsed = end - start;

        cout << "Version 2 (lock every increment) result: " << counter2 << endl;
        cout << "Time taken: " << elapsed.count() << " seconds\n\n";
    }
    cout << "==========================" << endl;
   
    // 版本3测试
    {
        
        vector<thread> threads;
        auto start = chrono::steady_clock::now();

        for (int i = 0; i < NUM_THREADS; ++i) {
            threads.emplace_back(version3);
        }
        for (auto& t : threads) {
            t.join();
        }
        auto end = chrono::steady_clock::now();
        chrono::duration<double> elapsed = end - start;

        cout << "Version 3 (local + lock once) result: " << counter3 << endl;
        cout << "Time taken: " << elapsed.count() << " seconds\n\n";
    }
    return 0;
}

从结果中我们看到,版本2和版本3结果都是正确的,且版本3速度非常快;反观版本1,不仅速度慢,而且结果不准确。

此外,锁的类型还有很多:

1.std::mutex(最基础、最常用)

  • 用途:互斥访问共享数据。
  • 常用配合std::lock_guardstd::unique_lock
  • 场景:写共享变量、线程安全容器封装、日志输出等。

2.std::recursive_mutex

  • 用途:允许同一线程多次获得同一个锁。
  • 使用场景:递归函数或调用链中多次加锁。

3.std::timed_mutex / std::recursive_timed_mutex

  • 用途:可以设置超时时间来尝试加锁。
  • 使用场景:实时系统、UI响应保护(如限时尝试加锁避免 UI 假死)。

4.std::shared_mutex

  • 用途:读写锁,允许多个线程并发读,写操作互斥。
  • 配合
    • std::shared_lock(读锁)
    • std::unique_lock(写锁)
  • 场景:缓存系统、配置读取等读多写少的系统。

5.std::scoped_lock

  • 用途:一次性锁多个互斥量,避免死锁。
  • 场景:多个资源的同步,常见于转账等原子性要求场景。

6.std::atomic<T>(替代锁,性能优)

  • 用途:锁的轻量替代,适合简单原子操作。
  • 场景:计数器、状态标志、生产者-消费者索引管理等。

示例:使用shared_mutex实现读写锁

c++ 复制代码
#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>
#include <chrono>

using namespace std;

shared_mutex rw_mutex;
int shared_data = 0;

// 读者线程(可并发)
void reader(int id) {
    for (int i = 0; i < 3; ++i) {
        {
            shared_lock<shared_mutex> lock(rw_mutex);  // 读锁
            cout << "[Reader " << id << "] reads shared_data = " << shared_data << endl;
        }
        this_thread::sleep_for(chrono::milliseconds(100));
    }
}

// 写者线程(互斥)
void writer(int id) {
    for (int i = 0; i < 2; ++i) {
        {
            unique_lock<shared_mutex> lock(rw_mutex);  // 写锁
            ++shared_data;
            cout << ">>> [Writer " << id << "] updated shared_data to " << shared_data <<endl;
        }
        this_thread::sleep_for(chrono::milliseconds(150));
    }
}

int main() {
    vector<std::thread> threads;

    // 启动读者线程
    for (int i = 0; i < 4; ++i)
        threads.emplace_back(reader, i + 1);

    // 启动一个写者线程
    threads.emplace_back(writer, 1);

    for (auto& t : threads)
        t.join();

    return 0;
}

然后你会发现类似这样的现象,读者输出会乱掉,而写者不会。原因是:

所有读者线程使用 共享锁

  • 共享锁允许多个线程同时持有锁,只要没有线程在持有写锁。
  • 所以多个读者线程可以"并发读取"同一个共享资源,导致输出交错、不按顺序(这不影响数据正确性)。

写者使用 独占锁(exclusive lock)

当任何一个线程拿到独占锁后:

  • 所有其他的读者(shared)或写者(unique)线程都必须等待;
  • 所以写者只能一个个排队执行
  • 所以写者输出是整齐的。

所以要保证读者输出不乱掉,可以这样

c++ 复制代码
mutex io_mutex;

// 读者线程(可并发)
void reader(int id) {
    for (int i = 0; i < 3; ++i) {
        {
            shared_lock<shared_mutex> lock(rw_mutex);  // 读锁
            lock_guard<std::mutex> io_lock(io_mutex);
            cout << "[Reader " << id << "] reads shared_data = " << shared_data << endl;
        }
        this_thread::sleep_for(chrono::milliseconds(100));
    }
}

4.等待/唤醒机制

为什么需要等待/唤醒机制?

在多线程程序中,线程之间不仅会共享资源 ,还可能存在前后依赖关系。比如生产者-消费者模式。

如果没有等待机制,只能靠不断轮询,会严重浪费CPU。

因此std::condition_variable提供线程通知机制,让等待变得高效。

核心组件

组件 说明
std::condition_variable 条件变量,用于线程等待与通知
std::mutex 配套的互斥锁,用于保护共享数据
std::unique_lock 配合 condition_variable 使用,支持解锁/上锁控制
.wait() 让线程等待直到被唤醒(释放锁并阻塞)
.notify_one() / .notify_all() 唤醒一个 / 所有等待线程

示例:生产者-消费者模型

c++ 复制代码
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<queue>

using namespace std;

queue<int> buffer;
const unsigned int max_size = 5;

mutex mtx;
// 条件变量,用于线程间通信
condition_variable cv;

void producer() {
	for (int i = 0; i < 10; ++i) {
		unique_lock<mutex> lock(mtx);
		cv.wait(lock, [] {return buffer.size() < max_size; }); //等待缓存区有空间

		buffer.push(i);
		cout << "Produced:" << i << endl;

		cv.notify_one(); //通知消费者
	}
}

void consumer() {
	for (int i = 0; i < 10; ++i) {
		unique_lock<mutex> lock(mtx);
		cv.wait(lock, [] {return !buffer.empty(); });//等待缓存区非空

        int val = buffer.front();
		buffer.pop();
		cout << "Consumed:" << val << endl;

		cv.notify_one(); //通知生产者

	}
}

int main() {
	thread t1(producer);
	thread t2(consumer);

	t1.join();
	t2.join();
}

在上述代码中,有几点需要说明

  • wait(lock,condition),它会自动在被唤醒后重新判断条件。
  • .wait()内部会自动释放锁,再等待;被唤醒后重新获取锁。
  • notify_one()只唤醒一个线程,而notify_all()会唤醒所有正在等待的线程。

std::condition_variable 其实不止有 .wait().notify_one().notify_all() 这几个方法,它还有其他实用的成员函数:

方法 说明 常见使用场景
wait(lock) 阻塞当前线程,直到被唤醒。需要配合 std::unique_lock<std::mutex> 使用。 基础的等待场景,例如等待共享资源状态变化。
wait(lock, pred) 一种"条件等待"。线程阻塞直到 pred() 返回 true,期间自动释放锁。 推荐用法,防止虚假唤醒(spurious wakeups)。
notify_one() 唤醒一个等待中的线程。 生产者通知消费者、控制线程轮流执行等。
notify_all() 唤醒所有等待中的线程。 所有线程都可能被唤醒,例如任务队列有新任务。
wait_for(lock, duration) 最多等待一段时间(相对时间),超时后继续执行。返回 cv_status::timeoutno_timeout 限时等待某个事件或资源的变化,例如读取串口数据超时重试。
wait_for(lock, duration, pred) 限时 + 条件等待。若 pred() 在时限内变为 true,继续执行;否则超时。 等待数据更新,如果太久就超时处理,比如用户登录超时。
wait_until(lock, time_point) 等待直到某个绝对时间点(比如某个系统时间)。 定时器/调度器场景,如等到整点执行任务。
wait_until(lock, time_point, pred) wait_for(..., pred) 类似,只是以绝对时间为基准。 高精度定时同步、跨线程时间调度。

5.异步编程(std::async、future、promise)

什么是异步编程?

异步编程是一种非阻塞的编程方式,避免主线程被阻塞,提高程序响应性与资源利用率。

与"同步"方式每一步必须顺序完成不同,异步允许程序先安排一个任务执行,然后继续处理其他任务,待结果准备好再处理返回值。

C++中的异步支持

标准库提供了 <future> 头文件,支持以下异步机制:

  • std::async:启动异步任务
  • std::future:获取异步结果
  • std::promise:任务手动设置结果
  • std::packaged_task:封装可调用对象,用于线程池等场景

std::async

async 是 C++11 提供的标准库函数,用于创建异步任务,它会返回一个 std::future,用于获取任务返回值。

用法示例:

c++ 复制代码
#include <iostream>
#include <future>

int compute() {
    return 42;
}

int main() {
    std::future<int> result = std::async(std::launch::async, compute);
    std::cout << result.get() << std::endl; // 输出 42
}

说明:

  • std::launch::async:任务立即在线程中执行。
  • std::launch::deferred:任务延迟执行,直到调用 .get()
  • 默认模式:async | deferred,由实现决定是否异步。

两种模式的区别:

async会单独开辟一个线程;deferred在当前主线程执行。请看代码演示:

c++ 复制代码
#include <iostream>
#include <future>
#include<thread>

int computeAsync() {
    std::cout << "computeAsync id:" << std::this_thread::get_id() << std::endl;
    return 42;
}

int computeDeferred() {
    std::cout << "computeDeferred id:" << std::this_thread::get_id() << std::endl;
    return 43;
}

int main() {

    std::future<int> resultAsync = std::async(std::launch::async, computeAsync);
    std::future<int> resultDeferred = std::async(std::launch::deferred, computeDeferred);
    
    std::cout << "main id" << std::this_thread::get_id() << std::endl;
     
}

从输出结果来看std::cout << "computeDeferred id:" << std::this_thread::get_id() << std::endl;并没有输出,或者说

c++ 复制代码
int computeDeferred() {
    std::cout << "computeDeferred id:" << std::this_thread::get_id() << std::endl;
    return 43;
}

这段代码都没有被执行,这是因为std::launch::deferred的特性:延迟执行(lazy evaluation)。

当使用deferred模式,std::future<int> resultDeferred = std::async(std::launch::deferred, computeDeferred);这行代码不会被立即执行。在deferred模式下:

  • 函数不会在单独线程中执行;
  • 而是延迟到你调用 .get().wait() 时才在当前线程中执行
  • 所以没调用 resultDeferred.get(),它根本不会执行,std::cout 也自然不会输出。

因此,我们需要去调用一下get()wait()

之后能够看到的结果是:

std::future

std::future 是一个类模板,表示异步操作的结果。你可以从 asyncpromisepackaged_task 中获取。

常用方法:

方法名 功能说明
.get() 获取任务返回值,阻塞直到结果可用
.wait() 等待任务完成,不取值
.valid() 判断是否拥有共享状态
.wait_for() 等待一定时间

std::promise

std::promise 是一个线程间传递数据的容器,可以:

  • 一个线程中设置值 (通过 promise.set_value());
  • 另一个线程中通过对应的 future.get() 取得值

就像一个"许诺":我现在还没有结果,但我承诺之后会给你。

基本工作机制

  1. 你创建一个 std::promise<T>
  2. 从它获取 std::future<T>
  3. future 传递到另一个线程
  4. 当前线程中用 promise.set_value(x) 设定值;
  5. 异步线程中用 future.get() 获取值。

示例:子线程设置值,主线程获取值

c++ 复制代码
#include<iostream>
#include<thread>
#include<future>

using namespace std;

void compute(promise<int> prom) {
	int result = 10 + 20;
	this_thread::sleep_for(chrono::seconds(1));//模拟耗时
	prom.set_value(result); //设置结果
}

int main() {
	promise<int> prom;
	future<int> fut = prom.get_future(); //从 promise 拿到 future

	thread t(compute, move(prom));//将 promise 移动进子线程

	cout << "Waiting for result..." << endl;
	int value = fut.get(); //阻塞直到set_value	
	cout << "Result:" << value << endl;

	t.join();
}

说明:

  • std::promise 是一个写端(Producer),用来设置一个结果。
  • std::future 是一个读端(Consumer),用来获取这个结果。
  • 它们之间共享一个隐式的 共享状态(shared state)
  • promise 不可拷贝 → 必须 move 给子线程。
  • fut.get() 只能调用一次 → 结果只取一次。
  • 如果在子线程未调用 set_value() 就结束了,fut.get() 会抛异常。

整体流程:

  1. 主线程创建 promise,此时拥有"写权限"。
  2. 调用 get_future(),获得与之绑定的 future,用来在主线程等待并读取结果。
  3. 开启子线程 t ,传入 promise 的所有权(用 std::move()),子线程内负责设置最终结果。
  4. 子线程中调用 prom.set_value(result),将计算结果写入共享状态。
  5. 主线程调用 fut.get(),阻塞直到子线程设置了值,随后获取结果。
arduino 复制代码
主线程:
   promise<int> prom;
   future<int> fut = prom.get_future();
   └── fut.get() ←----------------------┐
                                        |
子线程:                                 
   └── compute(std::move(prom))         |
        └── prom.set_value(42); ────────┘

std::promise<void> 用法(无返回值)

示例:子线程完成后通知主线程

c++ 复制代码
#include <iostream>
#include <thread>
#include <future>

using namespace std;

void task(promise<void>&& prom) {
    this_thread::sleep_for(chrono::seconds(2));
    cout << "Task done!\n";
    prom.set_value();  // 通知完成,无返回值
}

int main() {
    promise<void> prom;
    future<void> fut = prom.get_future();

    thread t(task, move(prom));

    cout << "Waiting...\n";
    fut.get();  // 阻塞直到通知
    cout << "Received completion signal.\n";

    t.join();
}

promise与async的区别

特性 std::async std::promise + std::future
线程创建方式 自动创建线程 手动创建线程(灵活控制)
通信模型 只能从函数返回值获取结果 可以任意时刻、任意线程设值
是否可多次使用 否(一个值只能设置一次)
是否支持异常传递 支持(异常自动封装) 支持(通过 set_exception

std::packaged_task

std::packaged_task 是一个把任意可调用对象 (如函数、lambda、函数对象)封装起来的工具,它会自动将返回值绑定到一个 future 上,从而实现异步结果获取。

核心作用

  1. 接收一个可调用对象
  2. 将其"打包"
  3. 可以像函数一样调用
  4. 调用后,其结果可以通过对应的future获取

示例

c++ 复制代码
#include <iostream>
#include <thread>
#include <future>
#include <chrono>

int compute(int x) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return x * 2;
}

int main() {
    //1.创建 packaged_task,绑定函数
    std::packaged_task<int(int)> task(compute); // 包装一个函数:int(int)

    //2.获取 future 来接收任务结果
    std::future<int> result = task.get_future();

    //3.启动线程执行任务(通过调用 task)
    std::thread t(std::move(task), 21); // 传入参数21

    std::cout << "等待结果...\n";
    std::cout << "结果是: " << result.get() << std::endl;

    t.join();
}

流程拆解

步骤 动作 说明
1 packaged_task 包装函数 构造任务包装器
2 get_future() 获取结果 建立"返回值通道"
3 启动线程,调用任务 类似调用函数
4 result.get() 等结果 同步等待,或延迟获取

常见写法对比

用法 异步执行 获取结果 线程创建
std::async 自动
std::promise + 线程 手动
std::packaged_task + 线程 手动

无绑定参数的写法

c++ 复制代码
std::packaged_task<int()> task([]{
 return 10;
});

std::future<int> f =task.get_future();
task();
std::cout<<f.get()<<std::endl;

注意事项

  • std::promise 类似,packaged_task 不可拷贝,只能移动;
  • 如果打包任务未执行,调用 future.get() 会导致挂起;
  • task() 执行只能一次(和 promise.set_value() 一样,一次性);

6. std::atomic 与原子操作

什么是原子操作?

  • 原子操作(atomic operation) 是不可被线程调度器中断的操作,执行过程是"要么全部执行完,要么都不执行",中间不会被其他线程打断。
  • 这样能保证多个线程并发操作同一数据时,不会产生数据竞争(data race)。

为什么要用std::atomic?

  • 传统的多线程共享变量操作,如果不加锁,可能导致读取半写入状态(即"读到脏数据")。
  • 使用互斥锁(mutex)可以保证安全,但会带来性能开销。
  • std::atomic 提供了无锁的线程安全访问,常用于简单的计数器、标志位等。

原子操作特性

原子操作保证:

  • 不可分割性:操作要么完全执行,要么完全不执行
  • 顺序可见性:操作结果对其他线程立即可见
  • 无数据竞争:不需要额外同步机制

atomic基础用法

c++ 复制代码
#include<iostream>
#include<thread>
#include<atomic>
#include<vector>

using namespace std;

atomic<int> counter(0);

void increment(int n) {
	for (int i = 0; i < n; i++)
	{
		++counter; //原子递增
	}
}

int main() {
	vector<thread> threads;

	for (int i = 0; i < 5; ++i) {
		threads.emplace_back(increment, 1000);
	}

	for (auto& t : threads) {
		t.join();
	}

	cout << "Counter:" << counter.load() << endl;
}
  • std::atomic<int> counter 声明一个原子整型变量。
  • ++counter 是原子递增操作,多个线程同时执行不会冲突。
  • counter.load() 获取当前值。

常见的atomic操作函数

  • load():读取值
  • store():写入值
  • exchange():交换值
  • compare_exchange_weak() / compare_exchange_strong():比较并交换(CAS,常用的无锁同步手段)
  • 还有算术运算符重载(++--+=等)

原子类型与普通类型的区别

  • 普通变量多线程操作时会出现竞态条件。
  • std::atomic 对变量的访问都由底层硬件指令保证原子性。

何时用 std::atomic

  • 对简单数值操作(计数器、状态标志)频繁访问且要求高性能时。
  • 避免互斥锁带来的上下文切换开销。
  • 复杂操作或多个变量协同修改,仍需用锁。

原子操作的内存序

内存序 特性 性能 使用场景
memory_order_seq_cst 顺序一致性(默认) 最慢 需要严格顺序的场景
memory_order_acquire 保证之后的读操作不会被重排序到前面 中等 读操作
memory_order_release 保证之前的写操作不会被重排序到后面 中等 写操作
memory_order_acq_rel acquire+release组合 中等 读-改-写操作
memory_order_relaxed 只保证原子性 最快 计数器等简单场景

原子算术运算

c++ 复制代码
std::atomic<int> counter(0);

// 原子加法
counter.fetch_add(5, std::memory_order_relaxed);

// 原子减法
counter.fetch_sub(3, std::memory_order_relaxed);

// 原子递增/递减
counter++; // 等价于fetch_add(1)
counter--; // 等价于fetch_sub(1)

位操作

c++ 复制代码
std::atomic<int> flags(0);

// 原子或操作
flags.fetch_or(0x01, std::memory_order_relaxed);

// 原子与操作
flags.fetch_and(~0x01, std::memory_order_relaxed);

// 原子异或操作
flags.fetch_xor(0x03, std::memory_order_relaxed);

性能对比测试

模拟多线程环境下的计数操作,对比四种方式的效率与正确性:

1.不加锁

2.使用互斥锁

3.使用原子变量

4.使用互斥锁+局部统计再合并

5.使用原子变量+局部统计再合并

c++ 复制代码
#include<iostream>
#include<thread>
#include<atomic>
#include<vector>
#include<mutex>
#include<chrono>

using namespace std;

constexpr int THREAD_COUNT = 8; 
constexpr int ITERATIONS = 1'000'000;

int counter1 = 0;
int counter2 = 0;
atomic<int> counter3 = 0;
int counter4 = 0;
atomic<int> counter5 = 0;

//无锁
void test_no_lock() {
	counter1 = 0;
	auto start = chrono::high_resolution_clock::now();

	vector<thread> threads;
	for (int i = 0; i < THREAD_COUNT; ++i) {
		threads.emplace_back([] {
			for (int j = 0; j < ITERATIONS; ++j)
				++counter1;
			});
	}
	for (auto& t : threads)t.join();

	auto end = chrono::high_resolution_clock::now();
	cout << "无锁结果 counter1 = " << counter1
		<< ",耗时(ms) =" << chrono::duration_cast<chrono::milliseconds>(end - start).count() << endl;
}

//使用 mutex
mutex mtx;

void test_mutex() {
	counter2 = 0;
	auto start = chrono::high_resolution_clock::now();

	vector<thread> threads;
	for (int i = 0; i < THREAD_COUNT; ++i) {
		threads.emplace_back([] {
			for (int j = 0; j < ITERATIONS; ++j) {
				lock_guard<mutex> lock(mtx);
				++counter2;
			}
			});
	}
	for (auto& t : threads) t.join();

	auto end = chrono::high_resolution_clock::now();
	cout << "mutex结果 counter2 = " << counter2
		<< ",耗时(ms) = " << chrono::duration_cast<chrono::milliseconds>(end - start).count() << endl;
}


//使用 atomic
void test_atomic() {
	counter3 = 0;
	auto start = chrono::high_resolution_clock::now();

	vector<thread> threads;
	for (int i = 0; i < THREAD_COUNT; ++i) {
		threads.emplace_back([] {
			for (int j = 0; j < ITERATIONS; ++j)
				++counter3;
			});
	}
	for (auto& t : threads) t.join();

	auto end = chrono::high_resolution_clock::now();
	cout << "atomic结果 counter3 = " << counter3
		<< ",耗时(ms) = " << chrono::duration_cast<chrono::milliseconds>(end - start).count() <<endl;
}

//互斥锁+局部统计再合并
void test_mutex_local_sum() {
	counter4 = 0;
	auto start = chrono::high_resolution_clock::now();

	vector<thread> threads;
	for (int i = 0; i < THREAD_COUNT; ++i) {
		threads.emplace_back([] {
			int local = 0;
			for (int j = 0; j < ITERATIONS; ++j)
				++local;
			lock_guard<mutex> lock(mtx);
			counter4 += local; // 最后只加一次
			});
	}
	for (auto& t : threads) t.join();

	auto end = chrono::high_resolution_clock::now();
	cout << "mutex,局部累加结果 counter4 = " << counter4
		<< ",耗时(ms) = " << chrono::duration_cast<chrono::milliseconds>(end - start).count() << endl;
}

//局部统计+原子合并
void test_atomic_local_sum() {
	counter5 = 0;
	auto start = chrono::high_resolution_clock::now();

	vector<thread> threads;
	for (int i = 0; i < THREAD_COUNT; ++i) {
		threads.emplace_back([] {
			int local = 0;
			for (int j = 0; j < ITERATIONS; ++j)
				++local;
			counter5 += local; // 最后只加一次
			});
	}
	for (auto& t : threads) t.join();

	auto end = chrono::high_resolution_clock::now();
	cout << "局部累加结果 counter5 = " << counter5
		<< ",耗时(ms) = " << chrono::duration_cast<chrono::milliseconds>(end - start).count() <<endl;
}


int main() {
	test_no_lock();
	test_mutex();
	test_atomic();
	test_mutex_local_sum();
	test_atomic_local_sum();
}

7.线程池实现

为什么需要线程池?

线程池是管理多线程任务的经典设计模式,主要解决以下问题:

  • 线程创建/销毁开销大:每次请求创建线程约消耗1MB内存和数毫秒时间
  • 资源耗尽风险:无限制创建线程可能导致OOM(Out of Memory)
  • 任务调度优化:统一管理任务队列,提高CPU缓存命中率

线程池的核心思想是:使用一组固定数量的线程,来处理动态添加的多个任务。

线程池的核心组成

组件名称 说明
工作线程集 一组固定数量的线程,启动后始终等待任务
任务队列 存放待执行的任务,通常是 std::function<void()>
同步机制 使用 std::mutex + std::condition_variable 保证线程安全
停止机制 使用 std::atomic<bool> 控制线程池终止
任务提交接口 外部添加任务的入口

简易线程池实现

下面是一个可直接使用的小型线程池实现,支持任意参数的任务提交与future返回值获取:

c++ 复制代码
#include<iostream>
#include<vector>
#include<queue>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<functional>
#include<future>
#include<atomic>

using namespace std;

class ThreadPool {
public:

	//初始化指定数量的线程
	explicit ThreadPool(size_t threadCount) :stopFlag(false) {
		for (size_t i = 0; i < threadCount; ++i) {
			workers.emplace_back([this] {
				//每个线程循环等待并处理任务
				while (true) {
					function<void()>task;
					//从任务队列中取出任务(线程安全)
					{
						//取任务
						unique_lock<mutex> lock(queueMutex);
						//如果任务队列为空且线程池未关闭,则阻塞等待
						condition.wait(lock, [this] {
							return stopFlag || !tasks.empty();
							});
						//如果线程池关闭且队列为空,则退出循环(结束线程)
						if (stopFlag && tasks.empty()) return;
						//取出一个任务并移出队列
						task = move(tasks.front());
						tasks.pop();
					}

					task(); //执行任务
				}
				});
		}
	}

	//禁止拷贝构造和赋值,避免资源重复释放
	ThreadPool(const ThreadPool&) = delete;
	ThreadPool& operator =(const ThreadPool&) = delete;

	//提交任务
	template<class F,class... Args>
	auto submit(F&& f, Args&&... args) -> future<_Invoke_result_t<F, Args...>> {
		//推导函数返回类型
		using ReturnType = _Invoke_result_t<F, Args...>;
		//创建绑定参数后的任务,封装为 packaged_task
		auto taskPtr = make_shared<packaged_task<ReturnType()>>(
			bind(forward<F>(f), forward<Args>(args)...)
		);
		//获取 future 对象以支持结果获取
		future<ReturnType> res = taskPtr->get_future();
		{
			//向任务队列中添加任务
			unique_lock<mutex> lock(queueMutex);
			//如果线程池已关闭,拒绝提交
			if (stopFlag)
				throw runtime_error("ThreadPool is stopped");
			//存入包装后的任务
			tasks.emplace([taskPtr]() { (*taskPtr)(); });
		}
		//通知一个工作线程处理任务
		condition.notify_one();
		return res;
	}

	~ThreadPool() {
		{
			//设置终止标志
			unique_lock<mutex> lock(queueMutex);
			stopFlag = true;
		}
		//通知所有工作线程
		condition.notify_all();
		//等待所有线程执行完毕并回收资源
		for (thread& worker : workers)
			if (worker.joinable()) worker.join();
	}
private:
	vector<thread> workers;//工作线程集合
	queue<function<void()>> tasks;//任务队列

	mutex queueMutex;//队列互斥锁
	condition_variable condition;//用于线程等待/唤醒
	atomic<bool> stopFlag;//标志位,表示线程池是否停止
};

示例:批量提交任务

c++ 复制代码
int main() {
	ThreadPool pool(4); //创建4个线程

	vector<future<int>> results;

	for (int i = 0; i < 8; ++i) {
		results.emplace_back(
			pool.submit([i] {
				this_thread::sleep_for(chrono::milliseconds(100));
				return i * i;
				})
		);
	}

	for (auto& f : results) {
		cout << "Result:" << f.get() << endl;
	}
}

8.信号量

信号是什么?

信号量(Semaphore)是一种同步原语 ,用于控制多个线程对共享资源的并发访问数量 。本质上,它是一个计数器,代表当前可用资源的数量。

在 C++20 中引入了标准信号量:

  • std::counting_semaphore<N>:计数型信号量,最多 N 个资源。
  • std::binary_semaphore:一元信号量,最多 1 个资源(等价于互斥量)。

信号量本质

信号量的本质:计数器 +等待队列

信号量本质上由两个核心部分组成:

  1. 计数器(counter):记录当前可用资源的数量;
  2. 等待队列(wait queue):当资源不足时,被阻塞的线程会进入此队列等待唤醒。

两个基本操作:P操作&V操作

信号量源自操作系统经典同步模型------Dijkstra 的 P/V 操作(也称为 down/up):

操作 对应方法 描述
P acquire() 请求资源。若资源可用(计数 > 0),将其减 1;否则阻塞等待。
V release() 释放资源。计数加 1,并唤醒一个等待线程(如果有)。

线程访问资源时,信号量做了什么?

以 C++ 的 std::counting_semaphore 为例,来看内部机制:

获取资源时(acquire()):

c++ 复制代码
sem.acquire(); // P 操作

信号量内部执行:

  1. 检查当前计数器 count > 0 吗?
    • 是:减 1,线程继续运行。
    • 否:当前线程加入等待队列,挂起等待其他线程释放资源。
  2. 操作是原子的,由内部机制(或底层 OS 原语)保障线程安全。

释放资源时(release()):

c++ 复制代码
sem.release(); // V 操作

信号量内部执行:

  1. 计数器加 1;
  2. 如果有线程在等待队列中,唤醒其中一个线程(具体实现可能是 FIFO、公平或非公平策略);
  3. 被唤醒线程会重新尝试 acquire() 操作,并再次判断资源是否足够。

操作流程图

sql 复制代码
+-------------------+      acquire()       +----------------------+
| 线程尝试获取资源  |--------------------->| 信号量检查计数器      |
+-------------------+                      +----------+-----------+
                                                       |
                                    +------------------+------------------+
                                    |                                     |
                             count > 0                              count == 0
                                    |                                     |
                       - 计数器减 1                                - 加入等待队列
                       - 线程继续执行                              - 线程阻塞挂起

... 后续 release() 被调用 ...

+----------------------+
| release() 被调用     |
+----------+-----------+
           |
      - 计数器加 1
      - 若等待队列非空
        - 唤醒一个线程

示例

c++ 复制代码
#include<iostream>
#include<thread>
#include<semaphore>
#include<vector>

using namespace std;

counting_semaphore<3> sem(3); //初始容量 3,最多允许 3 个线程同时进入

void task(int id) {
    sem.acquire(); // 获取资源,若无资源则阻塞
    cout << "Thread " << id << " acquired semaphore\n";
    this_thread::sleep_for(chrono::seconds(1));
    cout << "Thread " << id << " releasing semaphore\n";
    sem.release(); // 释放资源
}

int main() {
    vector<thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(task, i);
    }

    for (auto& t : threads) t.join();
}

binary_semaphore 示例

c++ 复制代码
#include<iostream>
#include<thread>
#include<semaphore>
#include<vector>


using namespace std;
binary_semaphore sem(1); // 初始为 1,最多只能一个线程进入

int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        sem.acquire();
        ++counter;
        sem.release();
    }
}

int main() {
    thread t1(increment);
    thread t2(increment);
    t1.join();
    t2.join();
    cout << "counter = " << counter << endl; // 应为 200000
}

Barrier&Latch------阶段性同步机制

在多线程开发中,除了互斥锁和条件变量这类资源控制工具,有时我们还需要"线程间的阶段性同步"。也就是说,我们希望多个线程执行到某一阶段时必须等待彼此,然后再继续前进。例如:

  • 所有线程准备就绪后再统一执行下一阶段;
  • 多个线程汇总部分数据后,再开始进行统一处理。

这类需求在 C++20 中得到了标准化的支持:std::barrierstd::latch

Latch------一次性同步器

基本概念

std::latch 是一个一次性同步工具,可以让多个线程等待,直到计数值减为零。之后,它将"打开闸门",允许所有等待线程继续执行。而一旦计数变为零,它就无法重置。

使用示例

c++ 复制代码
#include <iostream>
#include <latch>
#include <thread>

std::latch done(3);

void task(int id) {
    std::cout << "Thread " << id << " initialized.\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100 * id));
    done.count_down(); // 表示该线程完成了准备
    done.wait();       // 等待其他线程也完成
    std::cout << "Thread " << id << " starting work.\n";
}

int main() {
    std::thread t1(task, 1);
    std::thread t2(task, 2);
    std::thread t3(task, 3);
    t1.join(); 
    t2.join(); 
    t3.join();
}

9. Barrier ------ 可重用的阶段性屏障

基本概念

std::barrier 是一个可多次重用的同步器。它允许多个线程在某个"屏障点"汇合,然后统一进入下一阶段。每到一个同步点时,线程都必须等待其他线程到达,才能共同继续。

使用示例

c++ 复制代码
#include <iostream>
#include <barrier>
#include <thread>

std::barrier sync_point(3); // 3个线程汇合

void stage(int id) {
    std::cout << "Thread " << id << " is preparing...\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100 * id));

    sync_point.arrive_and_wait(); // 等待所有线程到达
    std::cout << "Thread " << id << " begins stage 1.\n";

    sync_point.arrive_and_wait(); // 第二阶段同步
    std::cout << "Thread " << id << " begins stage 2.\n";
}

int main() {
    std::thread t1(stage, 1);
    std::thread t2(stage, 2);
    std::thread t3(stage, 3);
    t1.join(); 
    t2.join(); 
    t3.join();
}

Latch vs Barrier 对比

特性 Latch Barrier
重用性 一次性 可重复使用
同步时机 所有 count_down() 后触发 每一阶段的 arrive_and_wait()
场景 多线程"起步同步"或"终点汇合" 多阶段任务,各阶段同步
线程个数 可以比实际参与线程多或少 初始化时必须等于参与线程数

10.死锁

什么是死锁?

死锁是指两个或多个线程在执行过程中因争夺资源而造成互相等待的现象,从而导致程序无法继续执行。

死锁是一种永久阻塞 的状态,通常发生在多个线程循环等待资源时。例如:线程 A 拿着资源 1 等待资源 2,线程 B 拿着资源 2 等待资源 1,这种相互等待就可能导致死锁。

产生死锁的必要条件

  1. 互斥条件:资源不能被多个线程同时使用。
  2. 占有且等待:线程已经持有资源,同时又请求新的资源。
  3. 不可抢占:资源在未被线程释放之前,不能被其他线程抢占。
  4. 循环等待:多个线程之间形成一个资源等待的环路。

死锁示例

c++ 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex m1, m2;

void task1() {
    std::lock_guard<std::mutex> lock1(m1);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::lock_guard<std::mutex> lock2(m2);  // 等待 m2
    std::cout << "Task1 finished\n";
}

void task2() {
    std::lock_guard<std::mutex> lock2(m2);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::lock_guard<std::mutex> lock1(m1);  // 等待 m1
    std::cout << "Task2 finished\n";
}

int main() {
    std::thread t1(task1), t2(task2);
    t1.join();
    t2.join();
    return 0;
}

运行时,两个线程互相等待,导致程序卡住。

死锁的预防和避免

  • 统一锁顺序:所有线程都按照相同顺序加锁。

规定所有线程加锁时按照相同的顺序访问资源。

c++ 复制代码
void task1() {
    std::lock_guard<std::mutex> lock1(m1);
    std::lock_guard<std::mutex> lock2(m2);
}
void task2() {
    std::lock_guard<std::mutex> lock1(m1); // 统一顺序为 m1 -> m2
    std::lock_guard<std::mutex> lock2(m2);
}
  • 使用 std::lock() 同时加多个锁

标准库提供了 std::lock() 可同时锁住多个 mutex,并避免死锁。

c++ 复制代码
std::lock(m1, m2);
std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
  • 尝试锁(try_lock)

非阻塞方式加锁,如果失败,可以选择重试或退出。

c++ 复制代码
if (m1.try_lock()) {
    if (m2.try_lock()) {
        // do work
        m2.unlock();
    }
    m1.unlock();
}
  • 使用 std::scoped_lock

scoped_lockstd::lock 的封装,可以自动同时加多个锁并避免死锁。

c++ 复制代码
std::scoped_lock lock(m1, m2); // 一行搞定
  • 设置超时检测(需要条件变量或更复杂的同步机制配合)

线程无法长期等待锁,可以设置超时处理逻辑。

检查工具

工具 平台 功能
GDB Linux 查看线程状态、调试堆栈
Helgrind (Valgrind) Linux 检测数据竞争和死锁
Visual Studio 并发调试 Windows 可视化线程锁状态
Intel Thread Checker 跨平台 检测线程问题(包括死锁)
std::jthread + RAII C++20 自动线程管理,避免遗忘 join 导致假死

示例:使用std::lock排除死锁

使用 std::lock(m1, m2) 一次性锁住多个互斥锁,避免顺序不一致的问题。

c++ 复制代码
#include<iostream>
#include<thread>
#include<mutex>
#include<chrono>

std::mutex m1;
std::mutex m2;

void threadA_safe() {
    std::lock(m1, m2);
    std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
    std::cout << "Thread A acquired both locks safely\n";
}

void threadB_safe() {
    std::lock(m1, m2); // 注意锁顺序一致
    std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
    std::cout << "Thread B acquired both locks safely\n";
}

int main() {
    std::thread t1(threadA_safe);
    std::thread t2(threadB_safe);

    t1.join();
    t2.join();

    return 0;
}

11.任务调度与定时器实现

任务调度的基本概念

在多线程环境中,任务调度是指将任务合理地分配到线程池中,以确保任务能够高效、及时地执行。调度的目标包括:

  • 控制任务的执行顺序(如优先级队列)
  • 支持延迟执行、周期执行
  • 避免资源竞争和冗余执行

定时器的基本实现方式

C++标准库并未提供内建的"定时器"类,但可以通过线程 + std::chrono + std::condition_variable 模拟实现定时功能。

简单的定时器实现

c++ 复制代码
#include <iostream>
#include <thread>
#include <chrono>
#include <functional>
#include <atomic>

using namespace std;
class SimpleTimer {
public:
    //初始化状态:定时器初始为"已过期",可以启动
    SimpleTimer() : expired(true), try_to_expire(false) {}

    ~SimpleTimer() {
        stop();
    }

    //启动定时器,每interval_ms毫秒执行一次task函数
    void start(int interval_ms, function<void()> task) {
        if (!expired) return;

        expired = false;
        //创建一个新线程定时调用任务
        thread([this, interval_ms, task]() {
            while (!try_to_expire) {
                this_thread::sleep_for(chrono::milliseconds(interval_ms));
                if (!try_to_expire) task();
            }
            expired = true;
            }).detach();
    }

    void stop() {
        if (expired || try_to_expire) return;
        try_to_expire = true;
        while (!expired) this_thread::sleep_for(chrono::milliseconds(1));
        try_to_expire = false;
    }

private:
    atomic<bool> expired;
    atomic<bool> try_to_expire;
};
int main() {
    SimpleTimer timer;
    timer.start(1000, []() {
        cout << "Task triggered at: " << time(nullptr) << endl;
        });
    //主线程等待5秒钟,让定时器运行一会儿
    this_thread::sleep_for(chrono::seconds(5));
    timer.stop();
}

示例:生成者消费者周期调度

c++ 复制代码
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <atomic>
#include <functional>

using namespace std;


class SimpleTimer {
public:
    SimpleTimer() : expired(true), try_to_expire(false) {}

    ~SimpleTimer() {
        stop();
    }

    void start(int interval_ms, function<void()> task) {
        if (!expired) return;
        expired = false;

        thread([this, interval_ms, task]() {
            while (!try_to_expire) {
                this_thread::sleep_for(chrono::milliseconds(interval_ms));
                if (!try_to_expire) task();
            }
            expired = true;
            }).detach();
    }

    void stop() {
        if (expired || try_to_expire) return;
        try_to_expire = true;
        while (!expired) this_thread::sleep_for(chrono::milliseconds(1));
        try_to_expire = false;
    }

private:
    atomic<bool> expired;
    atomic<bool> try_to_expire;
};

// 任务队列
queue<function<void()>> taskQueue;
mutex queueMutex;
condition_variable cv;

int gCount = 0;

// 模拟任务生产者(定时触发)
void taskProducer() {
    
    {
        lock_guard<mutex> lock(queueMutex);
        int taskId = gCount++; //获取当前任务编号
        taskQueue.push([taskId]() {
            cout << "[消费者] 处理任务编号: " << taskId
                << " | 时间: " << chrono::system_clock::to_time_t(chrono::system_clock::now()) << endl;
            });
        cout << "[生产者] 任务编号 " << taskId << " 已入队" << endl;
    }
    cv.notify_one(); // 通知消费者
}

// 任务消费者线程函数
void taskConsumer() {
    while (true) {
        function<void()> task;
        {
            unique_lock<mutex> lock(queueMutex);
            cv.wait(lock, [] { return !taskQueue.empty(); });
            task = std::move(taskQueue.front());
            taskQueue.pop();
        }
        task(); // 执行任务
    }
}

int main() {
    SimpleTimer timer;

    // 启动一个消费者线程
    thread consumerThread(taskConsumer);

    // 启动定时器,每 1 秒生成一个任务
    timer.start(1000, taskProducer);

    // 主线程等待一段时间
    this_thread::sleep_for(chrono::seconds(10));

    // 停止定时器,程序结束
    timer.stop();

    // 主线程退出前通知中断
    consumerThread.detach(); 
    return 0;
}

小结

  • 使用线程 + sleep_for 可以构建定时器
  • 通过原子变量控制定时器生命周期
  • 可以与线程池结合实现延迟调度和周期调度

12. C++17/20多线程新特性

std::scoped_lock(C++17)

std::scoped_lock 是 C++17 新增的一个互斥锁管理类模板,用于方便地同时锁定多个 mutex 对象。它能防止死锁的经典写法错误,简化了多 mutex 加锁的代码。

示例

C++ 复制代码
#include <iostream>
#include <mutex>
#include <thread>

std::mutex m1, m2;

void task(int id) {
    // 同时锁定 m1 和 m2,避免死锁
    std::scoped_lock lock(m1, m2);
    std::cout << "Thread " << id << " has locked both mutexes." << std::endl;
    // 模拟工作
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "Thread " << id << " is releasing both mutexes." << std::endl;
}

int main() {
    std::thread t1(task, 1);
    std::thread t2(task, 2);

    t1.join();
    t2.join();

    return 0;
}

std::atomic_ref(C++20)

std::atomic_ref 是 C++20 新增的一个原子操作适配器,允许你对已有的非原子类型的变量进行原子操作,而无需把变量本身定义为 std::atomic

它的设计目的是方便对已经存在的变量进行原子操作,特别适合不能或不方便修改变量类型的场景。

简单说,std::atomic_ref 并不是自己拥有数据,而是"引用"一个普通变量,通过它可以安全地以原子方式访问和修改该变量。

语法

c++ 复制代码
std::atomic_ref<T> a_ref(variable);
  • T 是基础类型,比如 intboollong 等。
  • variable 是你想操作的普通变量。

特性

  • 绑定的变量必须是可共享的std::atomic_ref 只能引用 std::atomic 以外的普通变量)。
  • 允许多线程对同一普通变量进行原子操作而不产生数据竞争。
  • 适合在对第三方库变量或不方便修改变量类型时用。

示例

c++ 复制代码
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

int main() {
    int counter = 0;  // 普通变量,不是 atomic

    std::atomic_ref<int> atomicCounter(counter);

    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([&atomicCounter]() {
            for (int j = 0; j < 1000; ++j) {
                atomicCounter.fetch_add(1, std::memory_order_relaxed);
            }
        });
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final counter value: " << counter << std::endl;

    return 0;
}

运行结果会是 10000,说明即使 counter 是普通变量,但通过 atomic_ref 进行了安全的原子自增操作。

对比

特性 std::atomic<T> std::atomic_ref<T>
是否拥有数据 拥有数据 引用已有数据
是否需要修改变量类型 需要 不需要
适用场景 新变量设计为原子类型 对已有变量进行原子操作

std::jthread(C++20)

std::jthread 是对 std::thread 的增强,自动管理线程的生命周期 ,尤其是自动调用 join(),避免忘记 join 导致程序异常或崩溃。

换句话说,std::jthread 在析构时会自动调用 join(),让多线程编程更安全方便。

为什么要用 std::jthread

  • 避免忘记 join 或 detach,导致程序崩溃或者线程异常。
  • 支持请求线程停止 的机制(通过 stop_token),方便线程优雅退出。
  • 接口上与 std::thread 非常类似,易上手。

基本用法

c++ 复制代码
#include <iostream>
#include <thread>
#include <chrono>

void worker() {
    std::cout << "Worker thread id: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Worker done" << std::endl;
}

int main() {
    std::jthread t(worker);  // 启动线程

    // 无需调用 join,t 自动 join 于析构时

    std::cout << "Main thread id: " << std::this_thread::get_id() << std::endl;
    return 0;
}

线程停止请求 (stop_token)

std::jthread 支持传递一个特殊参数 std::stop_token 给线程函数,用于请求线程优雅退出。

示例:

c++ 复制代码
#include <iostream>
#include <thread>
#include <chrono>

void worker(std::stop_token stoken) {
    while (!stoken.stop_requested()) {
        std::cout << "Working..." << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    std::cout << "Stop requested, thread exiting..." << std::endl;
}

int main() {
    std::jthread t(worker);

    std::this_thread::sleep_for(std::chrono::seconds(2));
    t.request_stop();  // 请求停止线程

    // 线程析构时自动 join
    return 0;
}

线程函数的第一个参数是 std::stop_tokenrequest_stop() 会向线程发送停止请求,线程内需检测该信号优雅退出。

特点

特点 说明
自动 join 析构时自动 join,无需手动调用
停止请求支持 线程函数可接收 std::stop_token 实现优雅退出
std::thread 类似 构造和使用方式几乎相同

13. 补充:C++23 新特性 --- std::format 和流操作改进

作用 :类似 Python 的 format,可以格式化字符串,替代之前繁琐的 std::stringstreamprintf,提高输出代码的简洁性和安全性。

c++ 复制代码
#include <format>
#include <iostream>

int main() {
    int a = 10;
    double b = 3.1415;
    std::cout << std::format("a = {}, b = {:.2f}\n", a, b);
}

优势

  • 类型安全
  • 可读性强
  • 性能优于流式输出,尤其是大批量格式化
相关推荐
米饭「」34 分钟前
C++AVL树
java·开发语言·c++
心愿许得无限大1 小时前
Qt 常用界面组件
开发语言·c++·qt
GiraKoo1 小时前
【GiraKoo】C++17的新特性
c++
Rockson1 小时前
C++如何查询实时贵金属行情
c++·api
shenyan~1 小时前
关于 c、c#、c++ 三者区别
开发语言·c++
mit6.8242 小时前
[vroom] docs | 输入与问题定义 | 任务与运输工具 | json
c++·自动驾驶
charlie1145141913 小时前
如何使用Qt创建一个浮在MainWindow上的滑动小Panel
开发语言·c++·qt·界面设计
cpp_learners5 小时前
QML与C++交互之创建自定义对象
c++·qt·qml
尘世闲鱼5 小时前
解数独(C++版本)
开发语言·c++·算法·解数独
kyle~6 小时前
C/C++字面量
java·c语言·c++