C++11之后,有了标准的线程库。c++11发布之前,c++并没有对多线程编程的专门支持,c++11通过标准库引入对多线程的支持,极大地方便了程序员的工作。
需要说明的是:c++11标准库内部包裹了pthread库,因此,编译程序的时候需要加上-lpthread连接选项。
#include#include #include
1、std::thread
a. C++11中创建线程非常简单,使用std::thread类就可以。thread类定义于thread头文件,构造thread类的对象时传入一个可调用对象作为参数(如果可调用对象有参数,把参数同时传入),这样构造完成后,新的线程马上被创建,同时执行该可调用对象。
b. 用std::thread默认的构造函数所构造出来的对象不关联任何线程。判断一个thread对象是否关联某个线程,使用joinable()接口,如果返回true,表明该对象关联着某个线程(即使该线程已经执行结束)。
c. "joinable"的对象析构前,必须调用join()接口等待线程结束,或者调用detach()接口解除与线程的关联,否则会抛出异常。
d. 正在执行的线程从关联的对象detach(分离)后会自主执行直至结束,对应的对象变成不关联任何线程的对象,joinable()将返回false。
e. std::thread没有拷贝构造函数和拷贝赋值操作符,因此不支持复制操作(但是可以move,转移线程所有权)。也就是说,没有两个std::thread对象会表示同一执行线程。
f. 容易知道,如下几种情况下,std::thread对象是不关联任何线程的(对这种对象调用join或detach接口会抛出异常):
case1:默认构造的thread对象
case2:被移动后的thread对象
case3:detach或join后的thread对象
例子代码如下:
#include#include using namespace std; void increase(int *p, int times) { for (int i = 0; i < times; ++i) { ++* p; } } int main() { int num = 0; //线程1调用increase函数 thread thread1(increase, &num, 10000); //线程2调用lamda函数 auto thread_lambda = [&]() { for (int i = 0; i < 10000; ++i) { ++num; } }; thread thread2(thread_lambda); thread1.join();//等待线程1结束 thread2.join();//等待线程2结束 cout << "num=" << num << endl; return 0; }
代码中的:
//线程2调用lamda函数
auto thread_lambda = [&]() {
for (int i = 0; i < 10000; ++i)
{
++num;
}
};
thread thread2(thread_lambda);
也可以直接合并写作如下:
thread thread2([&]() {
for (int i = 0; i < 10000; ++i)
{
++num;
}
})
输出结果如下:
可以惊奇的发现:运行结果存在一定的随机性!
上面的例子程序中创建了两个线程,分别对变量num进行了10000次++操作,由于两个线程同时运行,++num也没有加锁保护,所以最后的结果在10000与20000之间,有一定的随机性,也证明了++num不是原子操作。
另外也注意一下lambda函数的用法:
[外部变量访问方式说明符] (参数表) -> 返回值类型
{
语句块
}
其中,“外部变量访问方式说明符”可以是=或&,表示{}中用到的、定义在{}外面的变量在{}中是否允许被改变。=表示不允许,&表示允许。当然,在{}中也可以不使用定义在外面的变量。“-> 返回值类型”可以省略。
下面是一个合法的Lambda表达式:
[=] (int x, int y) -> bool {return x%10 < y%10; }
可以参考:C++11 Lambda表达式(匿名函数)详解 (biancheng.net)
2、std::mutex
可以轻松实现互斥。
常做多线程编程的人,一定对mutex(互斥)非常熟悉,c++11当然也支持mutex,通过mutex可以方便地对临界区域加锁。std::mutex类定义在mutex头文件,是用于保护共享数据避免从多个线程同时访问的同步原语。它提供了lock,try_lock,unlock等几个接口,功能如下:
调用方线程从成功调用lock()或try_lock()开始,到unlock()为止占有mutex对象。
线程占有mutex时,所有其他线程若试图要求mutex的所有权,则将阻塞(对于lock的调用)或收到false返回值(对于try_lock)。
调用方线程在调用lock和try_lock前必须不占有mutex。
我们使用mutex改写了上面的程序例子,达到两个线程不会同时使用++num的目的:
#include#include #include using namespace std; mutex mtx; void increase(int *p, int times) { for (int i = 0; i < times; ++i) { mtx.lock(); ++* p; mtx.unlock(); } } int main() { int num = 0; //线程1调用increase函数 thread thread1(increase, &num, 10000); //线程2调用lamda函数 auto thread_lambda = [&]() { for (int i = 0; i < 10000; ++i) { mtx.lock(); ++num; mtx.unlock(); } }; thread thread2(thread_lambda); thread1.join();//等待线程1结束 thread2.join();//等待线程2结束 cout << "num=" << num << endl; return 0; }
或者
#include#include #include using namespace std; void increase(int *p, int times, mutex &mtx) { for (int i = 0; i < times; ++i) { mtx.lock(); ++* p; mtx.unlock(); } } int main() { int num = 0; mutex mtx; //线程1调用increase函数 thread thread1(increase, &num, 10000, ref(mtx));//要使用std::ref //线程2调用lamda函数 auto thread_lambda = [&]() { for (int i = 0; i < 10000; ++i) { mtx.lock(); ++num; mtx.unlock(); } }; thread thread2(thread_lambda); thread1.join();//等待线程1结束 thread2.join();//等待线程2结束 cout << "num=" << num << endl; return 0; }
或者:
#include#include #include using namespace std; void increase(int *p, int times, mutex &mtx) { for (int i = 0; i < times; ++i) { mtx.lock(); ++* p; mtx.unlock(); } } int main() { int num = 0; mutex mtx; //线程1调用increase函数 thread thread1(increase, &num, 10000, ref(mtx));//要使用std::ref //线程2调用lamda函数 thread thread2([&]() { for (int i = 0; i < 10000; ++i) { mtx.lock(); ++num; mtx.unlock(); } }); thread1.join();//等待线程1结束 thread2.join();//等待线程2结束 cout << "num=" << num << endl; return 0; }
注意这种方式:
thread thread1(increase, &num, 10000, ref(mtx));
针对这种形参为引用类型的,要使用std::ref进行引用拷贝。
即:和std::bind类似,多线程的std::thread也是必须显式地通过std::ref来绑定应用进行传参,否则参数的引用声明是无效的!!!
经过mutex对++语句的保护,使得同一时刻只可能有一个线程对num变量进行++操作,因此这段程序的输出必然是20000。
此外注意:mutex与thread一样,不可复制(拷贝构造函数和拷贝赋值操作符都被删除),而且mutex不同于thread的地方是,mutex不可被移动。
这里总结一下,互斥锁如何使用?
答:首先需要#include
注意:这里面的num就是互斥量,也是共享数据。
备注
1)作系统提供mutex可以设置属性,C++11根据mutext的属性提供四种的互斥量,分别是:
std::mutex,最常用,普遍的互斥量(默认属性)。
std::recursive_mutex,允许同一线程使用recursive_mutext多次加锁,然后使用相同次数的解 锁操作解锁。mutex多次加锁会造成死锁。
std::timed_mutex,在mutex上增加了时间的属性。增加了两个成员函数try_lock_for(), try_lock_until(),分别接收一个时间范围,再给定的时间内如果互斥量被锁 主了,线程阻塞,超过时间,返回false。
std::recursive_timed_mutex,增加递归和时间属性。
2)mutex成员函数加锁解锁
- lock(),互斥量加锁,如果互斥量已被加锁,线程阻塞。
- bool try_lock(),尝试加锁,如果互斥量未被加锁,则执行加锁操作,返回true;如果互斥量已被加锁,返回false,线程不阻塞(这句话猛地不好理解,实际就是该互斥量在当前线程下被加锁,就是说当前这个线程是可以运行的,当前这个线程是没有被阻塞的,而其他线程是被阻塞的是无法访问这个互斥量的)。
- unlock(),解锁互斥量。
3)mutex RAII式的加锁解锁
std::lock_guard,管理mutex类。对象构建时传入mutex,会自动对mutex加入,直到离开类的作用域,析构时完成解锁。RAII式的栈对象能保证在异常情形下mutex可以在lock_guard对象析构时被解锁。
std::unique_lock与lock_guard功能类似,但是比lock_guard的功能更强大。比std::unique_lock维护了互斥量的状态,可通过bool owns_lock()访问,当locked时返回true,否则返回false。
3、std::lock_guard作用:有作用域的mutex,让程序更稳定,防止死锁。
背景:很容易想到mutex的lock和unlock必须成对调用,lock之后忘记unlock将是非常严重的错误,再次lock时会造成死锁。有时候一段程序中会有各种出口,如return、continue、break等语句,在每个出口前记得unlock已经被上锁的mutex是有一定负担的,假如程序段中有抛出异常的情况,就更为隐蔽棘手,C++11提供了更好地解决方案,对的就是RAII。
类模板std::lock_guard是mutex封装器,通过便利的RAII机制在其作用域内占有mutex。创建lock_guard对象时,该lock_guard对象试图接收给定mutex的所有权。
当程序流程离开创建lock_guard对象的作用域时,lock_guard对象被自动销毁并释放mutex,且要注意:lock_guard类也是不可复制的。
一般,需要加锁的代码段,我们使用{ }括起来形成一个作用域,括号的开端创建lock_guard
对象,把mutex对象作为参数传入lock_guard的构造函数即可,比如上面的例子加锁的部分,我们可以改写如下:
thread thread2([&]() {
for (int i = 0; i < 10000; ++i)
{
std::lock_guard guard(mtx);
++num;
}
});
进入作用域,临时对象guard创建,获取mutex的控制权(std::lock_guard类的构造函数里面调用了mutex的lock接口);离开作用域,临时对象guard销毁,释放了mutex(std::lock_guard类的析构函数里面调用了unlock接口),这是对mutex的更为安全的操作方式(对异常导致的执行路径改变也有效),大家在实践中应该多多使用。
4、C++11 中std::unique_lock与std::lock_guard的区别c++多线程编程中通常会对共享的数据进行“写”保护(可以同时去读,但不可以去同时修改),以防止多线程在对共享数据成员进行读写时造成资源争抢导致程序出现未定义的行为。
通常的做法是在修改共享数据成员的时候进行加锁(mutex.lock())。
在使用锁的时候通常是在对共享数据进行修改之前进行lock操作,在写完之后再进行unlock操作,经常会出现由于程序员个人疏忽导致的lock之后在离开共享成员操作区域时忘记unlock,造成死锁。
针对以上问题,C++11引入了std::unique_lock与std::lock_guard两种数据结构。通过对lock和unlock进行一次薄的封装,实现自动unlock的功能。
std::mutex mut;
void insert_data()
{
std::lock_guard lk(mut);
queue.push_back(data);
}
void process_data()
{
std::unique_lock lk(mut);
queue.pop();
}
注意:std::unique_lock和std::lock_guard都可以自动实现加锁与解锁功能,但是std::unique_lock要比std::lock_guard更加灵活,但是更灵活的代价是占用空间相对更大一点,且相对会更慢一点。
std::unique_lock 的构造函数的数目相对来说比 std::lock_guard 多,其中一方面也是因为 std::unique_lock 更加灵活,从而在构造 std::unique_lock 对象时可以接受额外的参数。总地来说,std::unique_lock 构造函数如下:
下面我们来分别介绍以上各个构造函数:
(1)默认构造函数
新创建的uique_lock对象不管理任何Mutex对象。
(2)locking初始化
新创建的unique_lock对象管理Mutex对象m,并尝试调用m.lock()对Mutex对象进行上锁,如果此时另外某个unique_lock对象已经管理了该Mutex对象m,则当前线程将会被阻塞。
(3)try_locking初始化
新创建的unique_lock对象管理Mutex对象m,并尝试调用 m.try_lock() 对 Mutex 对象进行上锁,但如果上锁不成功,并不会阻塞当前线程。
(4)deffred初始化
新创建的 unique_lock 对象管理 Mutex 对象 m,但是在初始化的时候并不锁住 Mutex 对象。m 应该是一个没有当前线程锁住的 Mutex 对象。
(5)adopting初始化
新创建的 unique_lock 对象管理 Mutex 对象 m,m应该是一个已经被当前线程锁住的Mutex对象。
(并且当前新创建的 unique_lock 对象拥有对锁(Lock)的所有权)。
(6) locking 一段时间(duration)
新创建的 unique_lock 对象管理 Mutex 对象 m,并试图通过调用 m.try_lock_for(rel_time) 来锁住 Mutex 对象一段时间(rel_time)。
(7)locking 直到某个时间点(time point)
新创建的 unique_lock 对象管理 Mutex 对象m,并试图通过调用 m.try_lock_until(abs_time) 来在某个时间点(abs_time)之前锁住 Mutex 对象。
(8)拷贝构造 [被禁用]
nique_lock 对象不能被拷贝构造。
(9)移动(move)构造
新创建的 unique_lock 对象获得了由 x 所管理的 Mutex 对象的所有权(包括当前 Mutex 的状态)。调用 move 构造之后, x 对象如同通过默认构造函数所创建的,就不再管理任何 Mutex 对象了。
综上所述,由 (2) 和 (5) 创建的 unique_lock 对象通常拥有 Mutex 对象的锁。而通过 (1) 和 (4) 创建的则不会拥有锁。通过 (3),(6) 和 (7) 创建的 unique_lock 对象,则在 lock 成功时获得锁。
五、线程同步代码下面给出线程同步的代码:
暂时不搞(因为里面代码不全,且涉及到std::condition_variable类的使用)。
参考:(1条消息) C++多线程编程_Nine days-CSDN博客_c++ 多线程
六、thread的使用这个代码可以现在学习,不涉及std::condition_variable类的使用。
#include#include std::thread::id main_thread_id = std::this_thread::get_id(); void hello() { std::cout << "hello current word!n"; if (main_thread_id == std::this_thread::get_id()) { std::cout << "This is main thread!n"; } else { std::cout << "This is not main thread!n"; } } void pause_thread(int n) { std::this_thread::sleep_for(std::chrono::seconds(n)); std::cout << "pause of " << n << " seconds endedn"; } int main() { std::thread t(hello); std::cout << t.hardware_concurrency() << std::endl;//可以并发执行多少个(不准确) std::cout << t.native_handle() << std::endl;//可以并发执行多少个(不准确) t.join(); std::thread a(hello); a.detach(); //相当于int m[5]这种写法,这句话就是声明了一个thread类型的含有5个元素的一维数组threads //数组每个元素都是一个线程 std::thread threads[5]; std::cout << "Spawning 5 threads...n"; for (int i = 0; i<5;i++) { //i+1为传递给pause_thread函数的实参 threads[i] = std::thread(pause_thread, i+1); // move-assign threads,move赋值操作 } std::cout << "Done spawning threads. Now waiting for them to join:n"; for (auto& thread : threads) { thread.join(); } std::cout << "all threads joined!n"; return 0; }
运行结果如下:
我们先来捋一下上述代码的运行流程:
步骤1:std::thread t(hello);这里我们先定义了一个线程t,这个线程任务是调用函数hello,这里还没有开始运行这个线程任务。(实际上是线程创建完了,该线程就开始运行了,但是很奇怪输出界面总是先打印输出并发数目,这看起来是先执行的主线程main,再去执行的子线程t。)
步骤2:
std::cout << t.hardware_concurrency() << std::endl; std::cout << t.native_handle() << std::endl;
注意:hardware_concurrency()函数用于检测硬件的并发特性,返回当前平台的线程所支持的线程并发数目。
native_handle()函数返回handle(即句柄):由于std::thread的实现和操作系统相关,因此该函数返回与std::thread具体实现相关的线程句柄,例如在符合 Posix 标准的平台下(如 Unix/Linux)是 Pthread 库)。具体native_handle用法参考std::thread::native_handle · 大专栏 (dazhuanlan.com)
步骤3:运行到t.join()
此时主线程被阻塞,一直等到子线程t运行完毕后,与主线程进行会合。最终再一起往下走。
步骤4:
std::thread a(hello); a.detach();
到了这一步,也是先创建一个子线程a,然后使用分离函数,将子线程a与主线程main进行分离。
那么主线程就失去了对子线程的所有掌控。主线程也不用再等待与子线程进行会合,主线程main也不用被阻塞了,各自继续无阻的运行吧!
步骤5:
std::thread threads[5]; std::cout << "Spawning 5 threads...n";
这个相当于int m[5]这种写法,这句话就是声明了一个thread类型的含有5个元素的一维数组threads
且数组每个元素都是一个线程。
因此”Spawning 5 threads...“:生成5个线程。
for (int i = 0; i<5;i++)
{
//i+1为传递给pause_thread函数的实参
threads[i] = std::thread(pause_thread, i+1); // move-assign threads,move赋值操作
}
这里面实际是一个移动赋值操作。这里面就复杂很多了:
首先要明确:线程std::thread被禁止使用拷贝赋值操作!而允许移动赋值操作,
threads[i] = std::thread(pause_thread, i+1);
这句话涉及两个构造函数,先是等号右边:代码右边是一个匿名的线程实体,使用的构造函数如下
1)使用std::thread类的初始化构造函数如下:
template
explicit thread(Fn&& fn, Args&&… args)
初始化构造函数,创建一个 std::thread 对象,该 std::thread 对象可被 joinable,新产生的线程会调用 fn 函数,该函数的参数由 args 给出。
threads[i] = std::thread(pause_thread, i+1);
这个是使用的移动赋值操作,
调用的构造函数为:thread& operator=(thread&& rhs) noexcept;
move构造函数、移动构造函数或称为移动赋值操作:
操作符=(即operator=)的左边为:thread&,即左值引用
操作符=(即operator=)的右边为: thread&& rhs,即右值引用
| thread(thread&& x) noexcept; |
这里面又涉及右值引用问题了:
可以参考:
一次性搞定右值,右值引用(&&),和move语义 - 掘金
彻底搞懂 c++ 函数参数的 & 和 &&_Boy next door-CSDN博客_c++ 函数参数&
c++为什么要搞个引用岀来,特别是右值引用,感觉破坏了语法的简洁和条理,拷贝一个指针不是很好吗? - 知乎
理解 C++ 右值引用和 std::move - 知乎
一文读懂C++右值引用和std::move - 知乎
移动构造和移动赋值与std::move - 小念之歌 - 博客园 (cnblogs.com)
C++移动构造函数和移动赋值运算符详解 (biancheng.net)
移动构造函数和移动赋值 - 鸭子船长 - 博客园 (cnblogs.com)
通过以上移动赋值语句,就将临时线程对象(std::thread(pause_thread, i+1))的资源转移给了
threads[i]线程。
步骤6:
std::cout << "Done spawning threads. Now waiting for them to join:n";
for (auto& thread : threads)
{
thread.join();
}
std::cout << "all threads joined!n";
打印输出:“Done spawning threads. Now waiting for them to join:n”:完成生成线程。现在等待他们与主线程会合。
完成会合后:thread.join()。
七、多线程使用实例参考:(1条消息) C++多线程编程_Nine days-CSDN博客_c++ 多线程
八、Future使用参考
(2条消息) C++多线程编程_Nine days-CSDN博客_c++ 多线程
(3 封私信 / 2 条消息) 原子操作 - 搜索结果 - 知乎 (zhihu.com)
c++的原子操作 - 知乎 (zhihu.com)(2 封私信 / 2 条消息) 如何理解 C++11 的六种 memory order? - 知乎 (zhihu.com)



