C++11 Thread线程库的基本使用
多线程允许程序同时执行多个任务,充分利用多核处理器资源,提高程序性能,尤其在处理 I/O 密集型或并行计算任务时效果显著。C++11 标准引入了 <thread> 头文件,提供了对原生线程操作的支持。
1、创建线程
最基本的操作是创建一个线程来执行函数。使用
std::thread类。要创建线程,我们需要一个可调用的函数或函数对象,作为线程的入口点。在C++11中,我们可以使用函数指针、函数对象或lambda表达式来实现。创建线程的基本语法如下:
cpp#include <thread> std::thread t(function_name, args...); function_name 是线程入口点的函数或可调用对象 args... 是传递给函数的参数创建线程后,我们可以使用
t.join()等待线程完成,或者使用t.detach()分离线程,让它在后台运行。例如,下面的代码创建了一个线程,输出一条消息:
cpp#include <iostream> #include <thread> using namespace std; void hello() { std::cout << "Hello from thread!\n"; } int main() { std::thread t(hello); // 创建线程并执行 hello 函数 t.join(); // 等待线程结束 return 0; }
.join():主线程阻塞,等待子线程执行完毕。必须调用join()或detach()之一。.detach():分离线程,使其独立运行,主线程不再等待。分离后不能再join。
join()等待线程完成
当我们创建一个线程后,我们可能需要等待它完成,以便获取线程的执行结果或执行清理操作。我们可以使用
t.join()方法来等待线程完成。例如,下面的代码创建了两个线程,等待它们完成后输出一条消息:
cpp#include <iostream> #include <thread> void print_message(const std::string& message) { std::cout << message << std::endl; } int main() { std::thread t1(print_message, "Thread 1"); std::thread t2(print_message, "Thread 2"); t1.join(); t2.join(); std::cout << "All threads joined" << std::endl; return 0; }在这个例子中,我们创建了两个线程
t1和t2,它们都调用 print_message函数输出一条消息。然后,我们使用 t1.join()和 t2.join()等待它们完成。最后,我们输出一条消息,表示所有线程都已经完成。
detach():分离线程
有时候我们可能不需要等待线程完成,而是希望它在后台运行。这时候我们可以使用
t.detach()方法来分离线程。例如,下面的代码创建了一个线程,分离它后输出一条消息
cpp#include <iostream> #include <thread> void print_message(const std::string& message) { std::cout << message << std::endl; } int main() { std::thread t(print_message, "Thread 1"); t.detach(); std::cout << "Thread detached" << std::endl; return 0; }在这个例子中,我们创建了一个名为
t的线程,调用print_message函数输出一条消息。然后,我们使用 t.detach()方法分离线程,让它在后台运行。最后,我们输出一条消息,表示线程已经被分离。需要注意的是,一旦线程被分离,就不能再使用t.join()方法等待它完成。而且,我们需要确保线程不会在主线程结束前退出,否则可能会导致未定义行为。
joinable()
joinable()方法返回一个布尔值,如果线程可以被 join()或 detach(),则返回true,否则返回 false。如果我们试图对一个不可加入的线程调用 join()或 detach(),则会抛出一个 std::system_error异常。
cpp#include <iostream> #include <thread> void foo() { std::cout << "Thread started" << std::endl; } int main() { std::thread t(foo); if (t.joinable()) { t.join(); } std::cout << "Thread joined" << std::endl; return 0; }
2、传递参数
2.1**. 传递临时变量的问题,会产生未定义行为。**
cpp#include <iostream> #include <thread> void foo(int& x) { x += 1; } int main() { std::thread t(foo, 1); // 传递临时变量 t.join(); return 0; }在上述代码中,foo函数接受一个整数引用作为参数,并对其加 1。但在线程创建时,传入的1是一个临时变量,在std::thread解析参数时,该临时变量会被销毁,导致foo访问了已销毁的对象,产生未定义行为。
解决方案:使用
std::ref传递一个持久化变量的引用:
cpp#include <iostream> #include <thread> #include <functional> void foo(int& x) { x += 1; } int main() { int x = 1; std::thread t(foo, std::ref(x)); // 传递变量的引用 t.join(); return 0; }
2.2. 传递指针或引用指向局部变量的问题
cpp#include <iostream> #include <thread> void foo(int* ptr) { std::cout << *ptr << std::endl; // 访问已经被销毁的指针 } int main() { int x = 1; std::thread t(foo, &x); // 传递指向局部变量的指针 t.join(); return 0; }局部变量 x在main函数作用域结束后被销毁,但std::thread可能尚未运行或访问了无效的地址,导致未定义行为。
解决方案
使用
new在堆上分配内存,或者使用std::shared_ptr进行管理。
cpp#include <iostream> #include <thread> void foo(int* ptr) { std::cout << *ptr << std::endl; delete ptr; // 释放内存 } int main() { int* ptr = new int(1); std::thread t(foo, ptr); t.join(); return 0; }
cpp#include <iostream> #include <thread> #include <memory> void foo(std::shared_ptr<int> ptr) { std::cout << *ptr << std::endl; } int main() { auto ptr = std::make_shared<int>(1); std::thread t(foo, ptr); t.join(); return 0; }
2.3. 传递指针或引用指向已释放的内存的问题
cpp#include <iostream> #include <thread> void foo(int& x) { std::cout << x << std::endl; } int main() { int* ptr = new int(1); std::thread t(foo, *ptr); // 传递已释放的内存 delete ptr; t.join(); return 0; }问题分析
在线程
t启动前,ptr已被delete,导致foo访问了已释放的内存,行为未定义。解决方案
确保在线程执行期间,数据仍然有效。
cpp#include <iostream> #include <thread> #include <functional> void foo(int& x) { std::cout << x << std::endl; } int main() { int x = 1; std::thread t(foo, std::ref(x)); // 传递变量的引用 t.join(); return 0; }向线程函数传递参数遵循普通函数参数传递规则,但需注意参数是按值复制传递的。
若要传递引用,需使用
std::ref或std::cref。
2.4. 类成员函数作为入口函数,类对象被提前释放
当主线程结束或对象离开作用域时,子线程可能仍在尝试访问该对象,从而导致崩溃。
cpp#include <iostream> #include <thread> class MyClass { public: void func() { std::cout << "Thread " << std::this_thread::get_id() << " started" << std::endl; // do some work std::cout << "Thread " << std::this_thread::get_id() << " finished" << std::endl; } }; int main() { MyClass obj; std::thread t(&MyClass::func, &obj); // 错误:主线程没有等待子线程结束就退出了 // 当 main 结束时,obj 被销毁,但线程 t 可能还在运行 // 这会导致线程访问已销毁的对象,引发未定义行为或崩溃 return 0; } // obj 在这里被销毁,可能导致线程崩溃解决方案:使用智能指针管理生命周期
为了解决上述问题,可以使用
std::shared_ptr来管理对象的生命周期。通过传递shared_ptr的副本给线程,可以确保只要线程还在运行,对象就不会被销毁。
cpp#include <iostream> #include <thread> #include <memory> class MyClass { public: void func() { std::cout << "Thread " << std::this_thread::get_id() << " started" << std::endl; // 模拟一些工作 std::cout << "Thread " << std::this_thread::get_id() << " finished" << std::endl; } }; int main() { // 使用 make_shared 创建共享指针 auto obj = std::make_shared<MyClass>(); // 将 shared_ptr 的副本传递给线程 // 这会增加引用计数,保证对象在线程运行期间有效 std::thread t(&MyClass::func, obj); // 等待线程结束 t.join(); return 0; }
5. 入口函数为类的私有成员函数
通过友元函数来调用类的私有成员函数。
cpp#include <iostream> #include <thread> class MyClass { private: // 声明友元函数,使其能够访问本类的私有成员 friend void myThreadFunc(MyClass* obj); void privateFunc() { std::cout << "Thread " << std::this_thread::get_id() << " privateFunc" << std::endl; } }; // 友元函数的具体实现 void myThreadFunc(MyClass* obj) { obj->privateFunc(); } int main() { MyClass obj; // 将友元函数作为线程的入口函数,并传递类对象的指针 std::thread thread_1(myThreadFunc, &obj); thread_1.join(); return 0; }
3、 线程同步
在多个线程中共享数据时,需要注意线程安全问题。如果多个线程同时访问同一个变量,并且其中至少有一个线程对该变量进行了写操作,那么就会出现数据竞争问题。数据竞争可能会导致程序崩溃、产生未定义的结果,或者得到错误的结果。
当多个线程访问共享数据时,可能引发数据竞争 (Data Race),导致未定义行为。需要使用同步原语保护共享数据。
(1) 互斥锁 (std::mutex)
最基本的同步机制。通过
lock()和unlock()确保同一时间只有一个线程访问临界区。
cpp#include <mutex> #include <iostream> #include <thread> using namespace std; std::mutex mtx; int counter = 0; void increment() { for (int i = 0; i < 100000; ++i) { mtx.lock(); // 加锁 ++counter; // 临界区操作 mtx.unlock(); // 解锁 } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Counter: " << counter << '\n'; // 应为 200000 return 0; }注意 :手动
lock/unlock容易出错(如忘记解锁)。推荐使用std::lock_guard或std::unique_lock,它们在构造时加锁,析构时自动解锁(RAII 思想)。
cppvoid safe_increment() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); // 构造时加锁,析构时解锁 ++counter; } }
lock_guard
std::lock_guard是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题。
特点
当构造函数被调用时,该互斥量会被自动锁定。
当析构函数被调用时,该互斥量会被自动解锁。
std::lock_guard对象不能复制或移动,因此它只能在局部作用域中使用。
std::unique_lock
std::unique_lock是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。
成员函数
lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。
try_lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回false,否则返回true。
try_lock_for(const std::chrono::duration<Rep, Period>& rel_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。
try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。
unlock():对互斥量进行解锁操作。构造函数
unique_lock() noexcept = default:默认构造函数,创建一个未关联任何互斥量的std::unique_lock对象。
explicit unique_lock(mutex_type& m):构造函数,使用给定的互斥量m进行初始化,并对该互斥量进行加锁操作。
unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量m进行初始化,但不对该互斥量进行加锁操作。
unique_lock(mutex_type& m, try_to_lock_t) noexcept:构造函数,使用给定的互斥量m进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的std::unique_lock对象不与任何互斥量关联。
unique_lock(mutex_type& m, adopt_lock_t) noexcept:构造函数,使用给定的互斥量m进行初始化,并假设该互斥量已经被当前线程成功加锁。
(2) 条件变量 (std::condition_variable)
std::condition_variable 的使用步骤
std::condition_variable是 C++11 中用于线程间通信的同步机制,可以用于实现线程的等待与通知机制。其使用步骤如下:
创建一个
std::condition_variable对象。创建一个互斥锁
std::mutex对象,用于保护共享资源的访问。在需要等待条件变量的地方:
使用
std::unique_lock<std::mutex>对象锁定互斥锁。调用std::condition_variable::wait()、
std::condition_variable::wait_for()或
std::condition_variable::wait_until()函数等待条件变量。
在其他线程中需要通知等待的线程时,调用std::condition_variable::notify_one()或std::condition_variable::notify_all()函数来通知等待的线程。
生产者与消费者模型
下面是一个简单的生产者-消费者模型的案例,其中使用了std::condition_variable来实现线程的等待和通知机制:
cpp#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue> // 全局互斥锁,用于保护共享资源 g_queue std::mutex g_mutex; // 全局条件变量,用于在生产者和消费者之间传递信号 std::condition_variable g_cv; // 共享的任务队列(缓冲区) std::queue<int> g_queue; // 生产者线程函数 void Producer() { for (int i = 0; i < 10; i++) { { // 1. 获取互斥锁(使用 unique_lock 以便配合条件变量) std::unique_lock<std::mutex> lock(g_mutex); // 2. 向共享队列中添加数据 g_queue.push(i); std::cout << "Producer: produced " << i << std::endl; // 离开此作用域时,lock 会自动释放互斥锁,避免阻塞消费者 } // 3. 通知消费者线程:有新数据了,可以来取了 // 放在锁外面通知,可以减少锁的竞争,提高性能 g_cv.notify_one(); // 模拟生产耗时,休眠 100 毫秒 std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } // 消费者线程函数 void Consumer() { while (true) { // 1. 获取互斥锁 std::unique_lock<std::mutex> lock(g_mutex); // 2. 等待条件满足(队列不为空) // wait 会自动释放锁并让线程进入休眠(不占用 CPU) // 当被唤醒时,它会重新加锁,并检查 lambda 表达式 []() { return !g_queue.empty(); } // 使用谓词(lambda)可以有效防止"虚假唤醒"问题 g_cv.wait(lock, []() { return !g_queue.empty(); }); // 3. 条件满足,安全地从队列中取出数据 int value = g_queue.front(); g_queue.pop(); std::cout << "Consumer: consumed " << value << std::endl; // 离开此作用域时,lock 会自动释放互斥锁 } }<websource>source_group_web_2</websource> int main() { // 创建并启动生产者线程 std::thread producer_thread(Producer); // 创建并启动消费者线程 std::thread consumer_thread(Consumer); // 主线程等待子线程执行完毕 producer_thread.join(); // 注意:在当前代码中,由于 Consumer 是死循环且没有退出机制, // 主线程会一直阻塞在这里,程序无法正常结束。 consumer_thread.join(); return 0; }使用 std::condition_variable可以实现线程的等待和通知机制,从而在多线程环境中实现同步操作。在生产者-消费者模型中,使用std::condition_variable可以让消费者线程等待生产者线程生产数据后再进行消费,避免了数据丢失或者数据不一致的问题。
示例2:用于线程间的条件等待和通知。常与互斥锁配合使用,实现线程等待特定条件成立。
cpp#include <condition_variable> #include <queue> #include <iostream> #include <thread> using namespace std; // 全局互斥锁,用于保护共享资源(队列和标志位) std::mutex mtx; // 条件变量,用于线程间的通信与同步 std::condition_variable cv; // 共享的数据队列(缓冲区) std::queue<int> data_queue; // 退出标志位,用于通知消费者线程生产者已经完成任务 bool done = false; // 生产者线程函数 void producer() { // 模拟生产 5 个数据 for (int i = 0; i < 5; ++i) { // 模拟生产耗时,休眠 100 毫秒 std::this_thread::sleep_for(std::chrono::milliseconds(100)); { // 获取互斥锁(使用 lock_guard 自动管理锁的生命周期) std::lock_guard<std::mutex> lock(mtx); data_queue.push(i); // 将数据放入队列 // 注意:notify_one 放在锁的作用域内或外均可, // 但放在锁外面(如上个示例)通常能稍微减少锁的竞争。 } cv.notify_one(); // 通知一个正在等待的消费者线程:有新数据了 } // 生产任务全部完成,设置退出标志 { std::lock_guard<std::mutex> lock(mtx); done = true; } // 通知所有等待的线程(确保消费者能醒来检查 done 标志并退出) cv.notify_all(); } // 消费者线程函数 void consumer() { while (true) { // 获取互斥锁(必须使用 unique_lock,因为它支持 wait 的自动解锁与重新加锁) std::unique_lock<std::mutex> lock(mtx); // 等待条件满足:队列不为空 或者 生产者已经完成任务(done == true) // 使用 lambda 谓词可以有效防止"虚假唤醒" cv.wait(lock, []{ return !data_queue.empty() || done; }); // 优雅退出的核心逻辑: // 如果生产者已经做完(done == true)且队列里的数据也全部被消费完了,则跳出循环,结束线程 if (done && data_queue.empty()) break; // 取出并处理数据 int data = data_queue.front(); data_queue.pop(); // 手动释放锁。因为接下来的 cout 输出操作比较耗时, // 提前释放锁可以让其他线程(如生产者)更快获取到锁,提高并发效率 lock.unlock(); std::cout << "Consumed: " << data << '\n'; } } int main() { // 创建并启动生产者和消费者线程 std::thread prod(producer); std::thread cons(consumer); // 主线程等待子线程执行完毕 prod.join(); cons.join(); // 现在消费者线程可以正常结束,主线程不会永久阻塞 return 0; }
.wait(lock, predicate):释放锁lock并阻塞线程,直到被notify_*唤醒且predicate条件为true。唤醒后会自动重新获得锁。.notify_one():随机唤醒一个等待线程。.notify_all():唤醒所有等待线程。
4、跨平台线程池
本实现使用 C++11 标准库中的std::thread、std::mutex、std::condition_variable、std::function和 std::queue等组件来创建一个线程池。
cpp#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <functional> #include <queue> #include <vector> // 补充:std::vector 需要的头文件 class ThreadPool { public: // 构造函数:预先创建指定数量的工作线程 ThreadPool(int numThreads) : stop(false) { for (int i = 0; i < numThreads; ++i) { // 使用 emplace_back 直接在容器内构造线程对象 threads.emplace_back([this] { while (true) { std::unique_lock<std::mutex> lock(mutex); // 条件变量等待:当线程池停止 或 任务队列不为空时,停止阻塞 condition.wait(lock, [this] { return stop || !tasks.empty(); }); // 如果线程池已停止 且 任务队列已清空,则退出当前工作线程 if (stop && tasks.empty()) { return; } // 取出队列头部的任务(使用 std::move 移动语义,避免不必要的拷贝) std::function<void()> task(std::move(tasks.front())); tasks.pop(); // 提前释放锁,避免在执行耗时任务时阻塞其他线程提交或获取任务 lock.unlock(); task(); // 执行取出的任务 } }); } } // 析构函数:负责优雅地关闭线程池 ~ThreadPool() { { std::unique_lock<std::mutex> lock(mutex); stop = true; // 设置停止标志 } condition.notify_all(); // 唤醒所有处于等待状态的工作线程,让它们有机会退出 // 等待所有工作线程执行完毕并退出 for (std::thread& thread : threads) { thread.join(); } } // 任务入队函数:支持任意可调用对象(如 Lambda、函数指针等)及其参数 template<typename F, typename... Args> void enqueue(F&& f, Args&&... args) { // 使用 std::bind 将函数和参数绑定成一个无参的可调用对象,并使用完美转发 std::function<void()> task(std::bind(std::forward<F>(f), std::forward<Args>(args)...)); { std::unique_lock<std::mutex> lock(mutex); tasks.emplace(std::move(task)); // 将任务添加到任务队列中 } condition.notify_one(); // 唤醒一个正在等待的工作线程来执行新任务 } private: std::vector<std::thread> threads; // 存放工作线程的容器 std::queue<std::function<void()>> tasks; // 存放待执行任务的任务队列 std::mutex mutex; // 互斥锁,用于保护任务队列的线程安全 std::condition_variable condition; // 条件变量,用于线程间的同步与通信 bool stop; // 线程池的停止标志位 }; int main() { // 创建一个包含 4 个工作线程的线程池 ThreadPool pool(4); // 提交 8 个任务到线程池中 for (int i = 0; i < 8; ++i) { pool.enqueue([i] { std::cout << "Task " << i << " is running in thread " << std::this_thread::get_id() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟任务执行耗时 std::cout << "Task " << i << " is done" << std::endl; }); } // 注意:main 函数结束后 pool 会被析构,析构函数会等待所有任务执行完毕 return 0; }核心机制解析
- 线程复用 :在构造函数中一次性创建好固定数量的线程。这些线程会在
while(true)循环中不断从任务队列中取任务执行,避免了高并发场景下频繁创建和销毁线程的系统开销。- 同步机制 :使用
std::mutex保护共享的任务队列,防止多个线程同时操作导致数据竞争;使用std::condition_variable实现"生产者-消费者"模型,当队列为空时让工作线程休眠(不占用 CPU),有新任务时再将其唤醒。- 完美转发与类型擦除 :
enqueue函数使用了模板、std::forward(完美转发)和std::bind。这使得线程池可以接收任意形式的可调用对象(Lambda、普通函数、类成员函数等)和任意数量的参数,并将它们统一包装成std::function<void()>存入队列。- 优雅退出 :析构函数中先将
stop置为true,然后调用notify_all()唤醒所有休眠的线程。工作线程被唤醒后,检测到stop为真且队列为空,便会安全地退出循环,最后主线程通过join()回收所有子线程资源。说明
在这个示例中,我们定义了一个 ThreadPool类,并在构造函数中创建了指定数目的线程。在每个线程中,线程会不断地从任务队列中获取任务并执行,直到线程池被停止。
enqueue() 函数:将任务封装成一个std::function对象,并将其添加到任务队列中。
析构函数:在析构函数中,我们等待所有线程执行完成后,再停止所有线程。
在主函数中,我们创建了一个ThreadPool对象,并向任务队列中添加了 8 个任务。每个任务会输出一些信息,并在执行完后等待 1 秒钟。由于线程池中有 4 个线程,因此这 8 个任务会被分配到不同的线程中执行。在任务执行完成后,程序会退出
5、异步操作 (std::async, std::future)
<future> 头文件提供了更高级的异步操作接口。
std::async:异步执行函数,返回一个std::future对象。std::future:存储异步操作的结果。通过.get()获取结果(会阻塞直到结果准备好)。
5.1. async 与 futur``e
std::async和std::future是 C++11 引入的一个功能,允许我们异步执行一个函数,并返回一个std::future对象,表示异步操作的结果。使用std::async可以方便地进行异步编程,避免了手动创建线程和管理线程的麻烦。
cpp#include <iostream> #include <future> int calculate() { // 模拟一个耗时的计算 std::this_thread::sleep_for(std::chrono::seconds(1)); return 42; } int main() { std::future<int> future_result = std::async(std::launch::async, calculate); // 在这里可以做其他的事情 int result = future_result.get(); // 获取异步操作的结果 std::cout << result << std::endl; // 输出42 return 0; }在此例中,使用
std::async函数异步执行了一个耗时的计算,这个计算在另一个线程中执行,不会阻塞主线程。通过future_result.get()获取异步操作的结果并输出。
5.2. packaged_task
std::packaged_task是一个类模板,用于将一个可调用对象(如函数、函数对象或 Lambda 表达式)封装成一个异步操作,并返回一个std::future对象,表示异步操作的结果。packaged_task可以方便地将一个函数或可调用对象转换成一个异步操作,供其他线程使用。
cpp#include <iostream> #include <thread> #include <future> // 需要包含此头文件以使用 std::packaged_task 和 std::future // 定义带参数的任务函数 int calculate(int x, int y) { return x + y; } int main() { // 1. 包装任务:模板参数<int(int, int)> 必须与 calculate 的签名匹配 std::packaged_task<int(int, int)> task(calculate); // 2. 获取关联的 future 对象 std::future<int> future_result = task.get_future(); // 3. 启动线程:将 task 移动到线程中,并传入参数 1 和 2 std::thread t(std::move(task), 1, 2); // 4. 等待线程执行完毕 t.join(); // 5. 获取并打印结果 (1 + 2 = 3) std::cout << "Result: " << future_result.get() << std::endl; return 0; }在此例中,我们成功地将一个函数calculate封装成了一个异步操作,并在其他线程中执行。通过packaged_task和future对象,我们可以方便地实现异步编程,使得代码更加简洁和易于维护。
std::thread t(std::move(task), 1, 2);为啥move
这其实是由
std::packaged_task的特性决定的。简单来说:
std::packaged_task是不可复制的,只能被移动。以下是详细的技术原因分析:
核心原因:独占所有权
std::packaged_task内部持有一个共享状态 ,这个状态与std::future相关联。
- 一对一关系 :一个
packaged_task只能对应一个future。- 防止数据竞争 :如果允许复制
packaged_task,那么两个任务对象就会指向同一个共享状态。当这两个任务分别在不同线程执行时,它们都会尝试设置结果(set_value)。但future只能接收一次结果,第二次设置会导致程序崩溃(std::future_error)。为了保证安全,C++ 标准库将
std::packaged_task的复制构造函数 和复制赋值运算符 删除了(delete),只保留了移动构造函数。为什么 std::thread 需要 Move
当你创建一个
std::thread对象时,你需要把要执行的函数(在这里是task)传递给线程。
- 值传递 :
std::thread的构造函数会拷贝 或移动你传入的参数到线程内部的存储中。- 强制 Move :因为你传入的是
task,而task不能被拷贝,所以你必须显式地使用std::move(task)将其转换为右值引用。这告诉编译器:"我知道task不可复制,请把它的所有权转移给新线程。"
5.3. promise
std::promise是一个类模板,用于在一个线程中产生一个值,并在另一个线程中获取这个值。promise通常与future和async一起使用,用于实现异步编程。
cpp#include <iostream> #include <future> // 必须包含,提供 promise 和 future #include <thread> // 必须包含,提供 thread using namespace std; // 子线程执行的函数 // 注意:promise 对象不可复制,因此必须通过值传递(配合 move)或引用传递 void func(std::promise<int> f) { // 设置共享状态的值 f.set_value(1000); } int main() { // 1. 在主线程创建 promise 对象 std::promise<int> f; // 2. 获取与该 promise 关联的 future 对象 auto future_result = f.get_future(); // 3. 创建线程并传递 promise // 必须使用 std::move 将 promise 的所有权转移给新线程 std::thread t1(func, std::move(f)); // 4. 等待子线程执行完毕 t1.join(); // 5. 获取子线程设置的结果并打印 // future_result.get() 会返回 1000 cout << future_result.get() << endl; return 0; }
6、原子操作 (std::atomic)
std::atomic是 C++11 标准库中的一个模板类,用于实现多线程环境下的原子操作。它提供了一种线程安全的方式来访问和修改共享变量,可以避免多线程环境中的数据竞争问题。
std::atomic的使用方式类似于普通的 C++ 变量,但是它的操作是原子性的。也就是说,在多线程环境下,多个线程同时对同一个std::atomic变量进行操作时,不会出现数据竞争问题。
常用的 std::atomic 操作
load():将std::atomic变量的值加载到当前线程的本地缓存中,并返回这个值。store(val):将val的值存储到std::atomic变量中,并保证这个操作是原子性的。
exchange(val):将val的值存储到std::atomic变量中,并返回原先的值。
compare_exchange_weak(expected, val)和compare_exchange_strong(expected, val):
- 比较std::atomic变量的值和expected的值是否相同,如果相同,则将val的值存储到std::atomic变量中,并返回true;否则,将std::atomic变量的值存储到expected中,并返回false。
以下是一个示例,演示了如何使用std::atomic进行原子操作:
cpp#include <atomic> #include <iostream> #include <thread> std::atomic<int> count = 0; void increment() { for (int i = 0; i < 1000000; ++i) { count++; } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << count << std::endl; return 0; }在这个示例中,我们定义了一个 std::atomic<int>类型的变量count,并将其初始化为 0。然后,我们启动两个线程分别执行increment函数,这个函数的作用是将count变量的值加一,执行一百万次。最后,我们在主线程中输出count 变量的值。
由于count变量是一个std::atomic类型的变量,因此对它进行操作是原子性的,不会出现数据竞争问题。在这个示例中,最终输出的count变量的值应该是 2000000。
7、线程局部存储 (thread_local)
使用
thread_local关键字声明变量,使每个线程拥有该变量的独立副本。
cpp#include <iostream> #include <thread> using namespace std; thread_local int thread_specific_id = 0; void print_id() { std::cout << "Thread ID: " << thread_specific_id << '\n'; thread_specific_id = std::rand() % 100; // 修改本线程的副本 } int main() { std::thread t1(print_id); // 输出 0 std::thread t2(print_id); // 输出 0 t1.join(); t2.join(); return 0; }
8、C++11 std::call_once 与其使用场景
单例模式的线程安全问题
单例设计模式是一种常见的设计模式,用于确保某个类只能创建一个实例。由于单例实例是全局唯一的,因此在多线程环境中使用单例模式时,需要考虑线程安全的问题。
cppclass Singleton { public: static Singleton& getInstance() { static Singleton instance; return instance; } void setData(int data) { m_data = data; } int getData() const { return m_data; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; int m_data = 0; };在这个单例类中,我们使用了一个静态成员函数getInstance()来获取单例实例,该函数使用了一个静态局部变量instance来存储单例实例。由于静态局部变量只会被初始化一次,因此该实现可以确保单例实例只会被创建一次。
但是,该实现并不是线程安全的。如果多个线程同时调用getInstance()函数,可能会导致多个对象被创建,从而违反了单例模式的要求。此外,如果多个线程同时调用setData()函数来修改单例对象的数据成员m_data,可能会导致数据不一致或不正确的结果。
使用 std::call_once 解决线程安全问题
为了解决这些问题,我们可以使用std::call_once来实现一次性初始化,从而确保单例实例只会被创建一次。下面是一个使用std::call_once的单例实现:
cppclass Singleton { public: static Singleton& getInstance() { std::call_once(m_onceFlag, &Singleton::init); return *m_instance; } void setData(int data) { m_data = data; } int getData() const { return m_data; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; static void init() { m_instance.reset(new Singleton); } static std::unique_ptr<Singleton> m_instance; static std::once_flag m_onceFlag; int m_data = 0; }; std::unique_ptr<Singleton> Singleton::m_instance; std::once_flag Singleton::m_onceFlag;在这个实现中,我们使用了一个静态成员变量m_instance来存储单例实例,
使用了一个静态成员变量m_onceFlag来标记初始化是否已经完成。
在getInstance()函数中,我们使用std::call_once来调用init()函数,确保单例实例只会被创建一次。在init()函数中,我们使用了std::unique_ptr来创建单例实例。
使用std::call_once可以确保单例实例只会被创建一次,从而避免了多个对象被创建的问题。此外,使用std::unique_ptr可以确保单例实例被正确地释放,避免了内存泄漏的问题。
std::call_once 函数
std::call_once是 C++11 标准库中的一个函数,用于确保某个函数只会被调用一次。其函数原型如下:
cppemplate<class Callable, class... Args> void call_once(std::once_flag& flag, Callable&& func, Args&&... args);其中,flag是一个std::once_flag类型 的对象,用于标记函数是否已经被调用;
func是需要被调用的函数或可调用对象;args是函数或可调用对象的参数。
std::call_once的作用是,确保在多个线程中同时调用call_once时,只有一个线程能够成功执行func函数,而其他线程则会等待该函数执行完成。
使用 std::call_once 时需要注意的几点
flag参数必须是一个 std::once_flag类型的对象,并且在多次调用call_once函数时需要使用同一个 flag对象。
func参数是需要被调用的函数或可调用对象。该函数只会被调用一次,因此应该确保该函数是幂等的。
args参数是func函数或可调用对象的参数。如果func函数没有参数,则该参数可以省略。std::call_once函数会抛出std::system_error异常,如果在调用func函数时发生了异常,则该异常会被传递给调用者。