文章目录
头文件
#include < thread >
构造函数
默认构造函数:
//创建一个空的 thread 执行对象。
thread() _NOEXCEPT
{ // construct with no thread
_Thr_set_null(_Thr);
}
有参构造函数:
//创建std::thread执行对象,该thread对象可被joinable,新产生的线程会调用threadFun函数,该函
数的参数由 args 给出
template<class Fn, class… Args>
explicit thread(Fn&& fn, Args&&… args);
拷贝构造函数:
// 拷贝构造函数被禁用,意味着 thread 不可被拷贝构造。
thread(const thread&) = delete;
thread t1;
thread t2 =t1; // 错误
Move构造函数:
//move 构造函数,调用成功之后 x 不代表任何 thread 执行对象。
注意:可被 joinable 的 thread 对象必须在他们销毁之前被主线程 join 或者将其设置为
detached。
thread(thread&& x)noexcept
thread t1;
thread t2 =move(t1); // 可以
线程创建
任务函数无参情况:
#include <iostream>
#include <thread> // 头文件
using namespace std;
// 1 传入0个值
void func1()
{
cout << "func1 into" << endl;
}
int main(){
std::thread t1(&func1); // 只传递函数
t1.join(); // 阻塞等待线程函数执行结束
return 0;
}
任务函数有参情况:
void func2(int a, int b)
{
cout << "func2 a + b = " << a+b << endl;
}
int main(){
int a =10;
int b =20;
thread t2(func2, a, b); // 加上参数传递,可以任意参数
t2.join();
}
任务函数引用传参情况:
void func3(int &c) // 引用传递
{
cout << "func3 c = " << &c << endl;
c += 10;
}
int main(){
int x = 10;
thread t3(func3, ref(x)); //引用传递要使用ref()函数处理
t3.join();
cout<<x<<endl;
return 0;
}
任务函数是类成员函数的情况:
class A{
public:
void func(int a, int b){
cout<<"thread th into" <<endl;
cout<< a+b <<endl;
}
};
int main(){
A* myptr = new A();
thread th(&A::func, myptr, 10, 20); //第一个和第二个参数都要传递指针
//thread th( [myptr](){myptr->func(10, 20)}; ); //使用lambda,在lamba内调用成员函数
//
th.join();
return 0;
}
线程脱离detach
在 C++11 中,std::thread 类的 detach 成员函数用于将线程设置为脱离状态。当一个线程被分离后,它将与主线程完全分离,主线程不再需要等待该线程完成,而且可以在任何时候退出,而不会影响已经分离的线程。
基本用途
异步执行:分离的线程可以在后台独立运行,不需要与主线程或其他线程同步。
资源管理:当线程被分离后,它的资源(如线程句柄)由 C++ 运行时库管理。当线程结束时,运行时库会自动清理与该线程相关的资源。
无等待退出:主线程可以在不等待分离线程完成的情况下退出。这对于需要长时间运行或独立于主程序的其他任务非常有用。
注意事项
不可加入:一旦线程被分离,就不能再通过 join 调用来等待其完成。尝试对分离线程调用 join 将会抛出 std::system_error。
资源清理:分离线程结束时,C++ 运行时库会自动清理与该线程相关的资源。但是,如果线程中使用了动态分配的资源(如堆内存),则需要确保在线程结束时正确释放这些资源,以避免内存泄漏。
数据竞争:如果分离的线程访问了共享资源,需要确保对这些资源的访问是同步的,以避免数据竞争和其他并发问题。
线程安全退出:在设计分离线程时,应考虑线程安全退出的策略,确保即使在主线程退出后,分离线程也能正确处理其任务并安全退出。
#include <iostream>
#include <thread>
#include <chrono>
void independentThread() {
this_thread::sleep_for(chrono::seconds(1)); // 休眠一秒
std::cout << "This is an independent thread." << std::endl;
}
int main() {
std::thread t(independentThread);
t.detach(); // 分离线程
// 主线程可以继续做其他事情,不需要等待 t 完成
//t也无法回收,使用t.join()会报错
return 0;
}
使用类封装线程
文件:zero_thread.h
#ifndef ZERO_THREAD_H
#define ZERO_THREAD_H
#include <thread>
class ZERO_Thread
{
public:
ZERO_Thread(); // 构造函数
virtual ~ZERO_Thread(); // 析构函数
bool start();
void stop();
bool isAlive() const; // 线程是否存活.
std::thread::id id() { return th_->get_id(); }
std::thread* getThread() { return th_; }
void join(); // 等待当前线程结束, 不能在当前线程上调用
void detach(); //能在当前线程上调用
static size_t CURRENT_THREADID();
protected:
void threadEntry();
virtual void run() = 0; // 运行
protected:
bool running_; //是否在运行
std::thread *th_;
};
#endif // ZERO_THREAD_H
文件:zero_thread.cpp
#include "zero_thread.h"
#include <sstream>
#include <iostream>
#include <exception>
ZERO_Thread::ZERO_Thread():
running_(false), th_(NULL)
{
}
ZERO_Thread::~ZERO_Thread()
{
if(th_ != NULL)
{
//如果到调用析构函数的时候,调用者还没有调用join则触发detach,此时是一个比较危险的动
//作,用户必须知道他在做什么
if (th_->joinable())
{
std::cout << "~ZERO_Thread detach\n";
th_->detach();
}
delete th_;
th_ = NULL;
}
std::cout << "~ZERO_Thread()" << std::endl;
}
bool ZERO_Thread::start()
{
if (running_)
{
return false;
}
try
{
th_ = new std::thread(ZERO_Thread::threadEntry, this);
}
catch(...)
{
throw "[ZERO_Thread::start] thread start error";
}
return true;
}
void ZERO_Thread::stop()
{
running_ = false;
}
bool ZERO_Thread::isAlive() const
{
return running_;
}
void ZERO_Thread::join()
{
if (th_->joinable())
{
th_->join(); // 不是detach才去join
}
}
void ZERO_Thread::detach()
{
th_->detach();
}
size_t ZERO_Thread::CURRENT_THREADID()
{
// 声明为thread_local的本地变量在线程中是持续存在的,不同于普通临时变量的生命周期,
// 它具有static变量一样的初始化特征和生命周期,即使它不被声明为static。
static thread_local size_t threadId = 0;
if(threadId == 0 )
{
std::stringstream ss;
ss << std::this_thread::get_id();
threadId = strtol(ss.str().c_str(), NULL, 0);
}
return threadId;
}
void ZERO_Thread::threadEntry()
{
running_ = true;
try
{
run(); // 函数运行所在
}
catch (std::exception &ex)
{
running_ = false;
throw ex;
}
catch (...)
{
running_ = false;
throw;
}
running_ = false;
}
文件:main.cpp
#include <iostream>
#include <chrono>
#include "zero_thread.h"
using namespace std;
class A: public ZERO_Thread
{
public:
void run()
{
while (running_)
{
cout << "print A " << endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
}
cout << "----- leave A " << endl;
}
};
class B: public ZERO_Thread
{
public:
void run()
{
while (running_)
{
cout << "print B " << endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
cout << "----- leave B " << endl;
}
};
int main()
{
{
A a;
a.start();
B b;
b.start();
std::this_thread::sleep_for(std::chrono::seconds(5));
a.stop();
a.join();
b.stop();
b.join(); // 需要我们自己join
}
cout << "Hello World!" << endl;
return 0;
}
互斥量
mutex又称互斥量,C++ 11中与 mutex相关的类(包括锁类型)和函数都声明在< mutex > 头文件中.
C++11提供如下4种语义的互斥量(mutex)
std::mutex,独占的互斥量,不能递归使用。
std::time_mutex,带超时的独占互斥量,不能递归使用。
std::recursive_mutex,递归互斥量,不带超时功能。
std::recursive_timed_mutex,带超时的递归互斥量。
独占互斥量mutex
std::mutex 的成员函数
构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于
unlocked 状态的。
lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没
有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当
前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁
住,则会产生死锁(deadlock)。
unlock(), 解锁,释放对互斥量的所有权。
try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该
函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直
到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回
false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex
volatile int counter(0); // non-atomic counter
std::mutex mtx; // locks access to counter
void increases_10k()
{
for (int i=0; i<10000; ++i) {
// 1. 使用try_lock的情况
// if (mtx.try_lock()) { // only increase if currently not
// ++counter;
// mtx.unlock();
// }
// 2. 使用lock的情况
{
mtx.lock();
++counter;
mtx.unlock();
}
}
}
int main()
{
std::thread threads[10];
for (int i=0; i<10; ++i)
threads[i] = std::thread(increases_10k);
for (auto& th : threads) th.join();
std::cout << " successful increases of the counter " << counter <<
std::endl;
return 0;
}
volatile 的用途(小小加注):
多线程编程:
在多线程程序中,当多个线程可以访问和修改同一个变量时,将变量声明为 volatile 可以确保每次访问变量时都是直接从内存中进行,而不是使用缓存中的值。这有助于防止编译器优化导致的竞态条件。
硬件访问:
当变量被硬件设备(如中断处理程序)修改时,声明为 volatile 可以确保每次读取变量的值都是最新的,而不是编译器优化后的值。
信号处理:
在信号处理函数中,某些变量可能会被信号处理程序修改。为了确保这些变量的更新能够被主程序正确看到,它们应该被声明为 volatile。
防止编译器优化:
编译器可能会优化代码以提高性能,例如通过缓存变量的值或重新排序指令。对于 volatile 变量,编译器会被告知不要执行这些优化,因此每次访问变量时都会直接读取或写入内存。
即共享资源都可以用volatile标志
递归互斥量recursive_mutex
递归锁允许同一个线程多次获取该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。
#include <iostream>
#include <thread>
#include <mutex>
struct Complex
{
std::mutex mutex;
// 解决方法:std::recursive_mutex mutex 声明这个递归锁
int i;
Complex() : i(0){}
void mul(int x)
{
std::lock_guard<std::mutex> lock(mutex);
i *= x;
}
void div(int x)
{
std::lock_guard<std::mutex> lock(mutex);
i /= x;
}
void both(int x, int y)
{
std::lock_guard<std::mutex> lock(mutex);
mul(x);
div(y);
}
};
int main(void)
{
Complex complex;
complex.both(32, 23); //里面有两次加锁,会导致死锁的问题
//解决方法:见上面
return 0;
}
虽然递归锁能解决这种情况的死锁问题,但是尽量不要使用递归锁,主要原因如下:
- 需要用到递归锁的多线程互斥处理本身就是可以简化的,允许递归很容易放纵复杂逻辑的产生,并且产生晦涩,当要使用递归锁的时候应该重新审视自己的代码是否一定要使用递归锁;
- 递归锁比起非递归锁,效率会低;
- 递归锁虽然允许同一个线程多次获得同一个互斥量,但可重复获得的最大次数并未具体说明,一旦超过一定的次数,再对lock进行调用就会抛出std::system错误。
带超时的互斥量 timed_mutex 和 recursive_timed_mutex
std::timed_mutex比std::mutex多了两个超时获取锁的接口:try_lock_for和try_lock_until
区别:try_lock 面对锁已被占用的情况,会直接放回false,而try_lock_for则会等待上一段时间,然后如果锁还是被占用,则放回false
try_lock_for传递的是时间段,比如等待5秒,而try_lock_until传递的是时间点,比如未来某个时间点停,未来5时30分停。
自动解锁:unique_lock,lock_guard的使用
lock_guard
- std::lock_guard 在构造函数中进行加锁,析构函数中进行解锁。
- 锁在多线程编程中,使用较多,因此c++11提供了lock_guard模板类;在实际编程中,我们也可以根
据自己的场景编写resource_guard RAII类,避免忘掉释放资源。
unique_lock
- unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。
- unique_lock比lock_guard使用更加灵活,功能更加强大。
- 使用unique_lock需要付出更多的时间、性能成本。
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::lock_guard
#include <stdexcept> // std::logic_error
std::mutex mtx;
void print_even (int x) {
if (x%2==0) std::cout << x << " is even\n";
else throw (std::logic_error("not even"));
}
void print_thread_id (int id) {
try {
// using a local lock_guard to lock mtx guarantees unlocking ondestruction /exception:
std::lock_guard<std::mutex> lck (mtx);
print_even(id);
// 离开作用域后,自动解锁
}
catch (std::logic_error&) {
std::cout << "[exception caught]\n";
}
}
int main ()
{
std::thread threads[10];
// spawn 10 threads:
for (int i=0; i<10; ++i)
threads[i] = std::thread(print_thread_id,i+1);
for (auto& th : threads) th.join();
return 0;
}
条件变量
条件变量的引入是为了方便线程的同步。线程同步是指线程间需要按照预定的先后次序顺序进行的行为。C++11对这种行为也提供了有力的支持,这就是条件变量。条件变量位于头文件
< condition_variable >下。
函数原型:
void wait (unique_lock<mutex>& lck);
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);
包含两种重载,第一种只包含unique_lock对象,另外一个Predicate 对象(等待条件),这里必须使用unique_lock,因为wait函数的工作原理:
- 当前线程调用wait()后将被阻塞并且函数会解锁互斥量,直到另外某个线程调用notify_one或者
notify_all唤醒当前线程;一旦当前线程获得通知(notify),wait()函数也是自动调用lock(),同理不
能使用lock_guard对象。 - 如果wait没有第二个参数,第一次调用默认条件不成立,直接解锁互斥量并阻塞到本行,直到某一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后无条件地继续进行后面的操作。
- 如果wait包含第二个参数,如果第二个参数不满足,那么wait将解锁互斥量并堵塞到本行,直到某一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后继续判断第二个参数,如果表达式为false,wait对互斥量解锁,然后休眠,如果为true,则进行后面的操作。
文件:sync_queue.h
#ifndef SYNC_QUEUE_H
#define SYNC_QUEUE_H
#include<list>
#include<mutex>
#include<thread>
#include<condition_variable>
#include <iostream>
template<typename T>
class SyncQueue
{
private:
bool IsFull() const
{
return _queue.size() == _maxSize;
}
bool IsEmpty() const
{
return _queue.empty();
}
public:
SyncQueue(int maxSize) : _maxSize(maxSize)
{
}
void Put(const T& x)
{
std::lock_guard<std::mutex> locker(_mutex);
while (IsFull())
{
std::cout << "full wait..." << std::endl;
_notFull.wait(_mutex);
}
_queue.push_back(x);
_notEmpty.notify_one();
}
void Take(T& x)
{
std::lock_guard<std::mutex> locker(_mutex);
while (IsEmpty())
{
std::cout << "empty wait.." << std::endl;
_notEmpty.wait(_mutex);
}
x = _queue.front();
_queue.pop_front();
_notFull.notify_one();
}
bool Empty()
{
std::lock_guard<std::mutex> locker(_mutex);
return _queue.empty();
}
bool Full()
{
std::lock_guard<std::mutex> locker(_mutex);
return _queue.size() == _maxSize;
}
size_t Size()
{
std::lock_guard<std::mutex> locker(_mutex);
return _queue.size();
}
int Count()
{
return _queue.size();
}
private:
std::list<T> _queue; //缓冲区
std::mutex _mutex; //互斥量和条件变量结合起来使用
std::condition_variable_any _notEmpty;//不为空的条件变量
std::condition_variable_any _notFull; //没有满的条件变量
int _maxSize; //同步队列最大的size
};
#endif // SYNC_QUEUE_H
文件:main.cpp
#include <iostream>
#include "sync_queue.h"
#include <thread>
#include <iostream>
#include <mutex>
using namespace std;
SyncQueue<int> syncQueue(5);
void PutDatas()
{
for (int i = 0; i < 20; ++i)
{
syncQueue.Put(888);
}
std::cout << "PutDatas finish\n";
}
void TakeDatas()
{
int x = 0;
for (int i = 0; i < 20; ++i)
{
syncQueue.Take(x);
std::cout << x << std::endl;
}
std::cout << "TakeDatas finish\n";
}
int main(void)
{
std::thread t1(PutDatas);
std::thread t2(TakeDatas);
t1.join();
t2.join();
std::cout << "main finish\n";
return 0;
}
原子变量
原子变量可以在保证线程安全的同时,实现无锁编程。简单来说就是,可以将临界资源声明成原子变量,然后使用store 和 load去写和读,是线程安全的。因为store和load是一个原子操作,不可分割。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter(0); // 线程安全的计数器
void increment_counter(int iterations) {
for (int i = 0; i < iterations; ++i) {
++counter; // 原子递增
}
}
int main() {
const int num_threads = 10;
const int increments_per_thread = 1000;
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.push_back(std::thread(increment_counter, increments_per_thread));
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final counter value: " << counter.load() << std::endl;
return 0;
}
异步操作
future
相当于一个特殊的线程,这个线程是异步的。同步线程是指多个线程的执行是有顺序的,可以预知的,比如:A->B->C。但是异步线程是不可以阈值的,如果B是异步线程,那么可能是A->B->C,也可能是A->C->B,这个顺序取决于B的执行时长。
future使用的时机是当你不需要立刻得到一个结果的时候,你可以开启一个线程帮你去做一项任务,并期待这个任务的返回,但是std::thread并没有提供这样的机制,这就需要用到std::async和std::future(都在头文件中声明)std::async返回一个std::future对象,而不是给你一个确定的值(所以当你不需要立刻使用此值的时候才需要用到这个机制)。当你需要使用这个值的时候,对future使用get(),线程就会阻塞直到future就绪,然后返回该值。
想象一个场景:一个老板(主线程),要写一份文件,然后其中有一些地方是需要查资料的。然后他现在让员工A去查找资料(启动异步线程),然后老板接着写其它地方的文件,等该写完的地方都写完后,他就问员工A是否已经查找好资料了(注意,这个过程必须是老板主动问的,不能由员工自动提醒老板),如果员工A已经完成了,就会将相关的资料给老板,如果还没完成,那么老板可能就会原地阻塞,或者继续干别的事,等一段时间后又要再次询问员工A······
#include <iostream>
#include <future>
#include <thread>
using namespace std;
int find_result_to_add()
{
// std::this_thread::sleep_for(std::chrono::seconds(2)); // 用来测试异步延迟的影响
std::cout << "find_result_to_add" << std::endl;
return 1 + 1;
}
int find_result_to_add2(int a, int b)
{
// std::this_thread::sleep_for(std::chrono::seconds(5)); // 用来测试异步延迟的影
响
return a + b;
}
void do_other_things()
{
std::cout << "do_other_things" << std::endl;
// std::this_thread::sleep_for(std::chrono::seconds(5));
}
int main()
{
std::future<int> result = std::async(find_result_to_add);
// std::future<decltype (find_result_to_add())> result=std::async(find_result_to_add);
// auto result = std::async(find_result_to_add); // 推荐的写法
do_other_things();
std::cout << "result: " << result.get() << std::endl; // 延迟是否有影响?延迟会阻塞,等待
//std::future<decltype(find_result_to_add2(int,int))>result2=std::async(find_result_to_add2, 10, 20); //错误
std::future<decltype (find_result_to_add2(0, 0))> result2 =std::async(find_result_to_add2, 10, 20);
std::cout << "result2: " << result2.get() << std::endl; // 延迟是否有影响?
// std::cout << "main finish" << endl;
//在 C++11 中,decltype 是一种用于推导表达式类型的工具。它允许你获取表达式的类型,而不需要显式地指定类型
return 0;
}
跟thread类似,async允许你通过将额外的参数添加到调用中,来将附加参数传递给函数。如果传入的函数指针是某个类的成员函数,则还需要将类对象指针传入(直接传入,传入指针,或者是std::ref封装)。
默认情况下,std::async是否启动一个新线程,或者在等待future时,任务是否同步运行都取决于你给的参数。这个参数为std::launch类型
std::launch::defered表明该函数会被延迟调用,直到在future上调用get()或者wait()为止
std::launch::async,表明函数会在自己创建的线程上运行
std::launch::any = std::launch::defered | std::launch::async
std::launch::sync = std::launch::defered
packaged_task
在 C++11 中,std::packaged_task 是一种用于包装任何可调用对象(如函数、Lambda 表达式、函数对象等)的模板类,以便异步执行。它是一种异步编程的机制,允许你将任务和其结果传递给线程,并在任务完成后获取结果。
#include <iostream>
#include <future>
#include <thread>
int calculate_sum(int a, int b) {
return a + b;
}
int main() {
std::packaged_task<int(int, int)> task(calculate_sum);
std::future<int> result = task.get_future();
//它不支持复制操作,因为复制一个 std::packaged_task 没有意义
//(它会失去与原始 future 的关联)。因此,std::packaged_task 只支持移动操作(move)。
std::thread t(std::move(task), 10, 20); // 也可以在主线程中直接使用task(10, 20)
t.join();
int sum = result.get();
std::cout << "Sum: " << sum << std::endl;
return 0;
}
promise
std::promise
是 C++11 中引入的一个模板类,用于在单个线程中设置值,并在另一个线程中检索该值。它是异步编程的一部分,通常与 std::future
和 std::async
一起使用。
std::promise
的主要用途包括:
- 设置值:你可以通过
std::promise
对象的set_value
方法来设置一个值,这个值可以在将来的某个时刻被其他线程检索。 - 异步操作的结果:在异步操作中,
std::promise
可以用来存储操作的结果,这样其他线程可以在操作完成时获取这个结果。 - 线程间通信:
std::promise
可以用作线程间的通信机制,一个线程可以设置一个值,而另一个线程可以等待并检索这个值。
下面是一个使用std::promise
的例子:
#include <iostream>
#include <future>
#include <thread>
void print_int(std::promise<int>& p) {
int x = 5;
p.set_value(x); // 设置 promise 的值
}
int main() {
std::promise<int> p;
std::future<int> f = p.get_future(); // 获取 future 对象
std::thread t(print_int, std::ref(p)); // 创建线程
t.join(); // 等待线程完成
int value = f.get(); // 获取 promise 的值
std::cout << "Value: " << value << std::endl;
return 0;
}
在这个例子中,我们创建了一个 std::promise
对象 p
,并启动了一个线程 t
来执行 print_int
函数。print_int
函数通过 p.set_value(x)
设置 promise 的值。在主线程中,我们通过 f.get()
获取 promise 的值,并打印出来。
std::promise
通常与 std::future
结合使用,后者可以用来检索 promise 的值。当你调用 promise
的 set_value
方法时,任何等待 future
的线程都会被唤醒,并能够获取到设置的值。