1.创建线程Thread
首先要引入头文件#include,管理线程的函数和类在该头文件中声明,其中包括std::thread类。
语句"std::thread th1(proc1);"创建了一个名为th1的线程,并且线程th1开始执行。
实例化std::thread类对象时,至少需要传递函数名作为参数。如果函数为有参函数,如"void proc2(int a,int b)“,那么实例化std::thread类对象时,则需要传递更多参数,参数顺序依次为函数名、该函数的第一个参数、该函数的第二个参数,···,如"std::thread th2(proc2,a,b);”。
总体来说,std::thread的构造函数会拷贝传入的参数:
a.当传入参数为基本数据类型(int,char,string等)时,会拷贝一份给创建的线程;
b. 当传入参数为指针时,会浅拷贝一份给创建的线程,也就是说,只会拷贝对象的指针,不会拷贝指针指向的对象本身。
c. 当传入的参数为引用时,实参必须用ref()函数处理后传递给形参,否则编译不通过,此时不存在“拷贝”行为。
使用std::thread时,对创建的线程有两种操作:等待/分离,也就是join/detach操作。
join() 的意思是:父线程等待子线程结束,并回收资源;
detach() 的含义是:父线程和子线程相互分离,即使父线程结束了,只要主线程没有结束,子线程就会继续正常运行。如果父线程就是主线程,主线程结束,子线程也会结束。可能存在的问题:若子线程还没来得及清理垃圾的话,主线程就结束了,那么就会导致内存溢出。
join是等待被创建线程的结束之后回收它的资源。因此,join的调用位置就比较关键。两种解决方式,防止join阻塞或者资源泄露:
a.主动让被创建线程结束,如设置退出标志位等;
b.在对象的析构函数中调用线程的join函数,等待资源回收。
使用joinable()来判断join()可否调用。同样,detach()也只能调用一次,一旦detach()后就无法join()了,有趣的是,detach()可否调用也是使用joinable()来判断。
注意:使用detach()时,可能存在主线程比子线程先结束的情况,主线程结束后会释放掉自身的内存空间;在创建线程时,如果std::thread类传入的参数含有引用或指针,则子线程中的数据依赖于主线程中的内存,主线程结束后会释放掉自身的内存空间,则子线程会出现错误。
ps:因为c++11的多线程编程以boost库的多线程编程为基础,所以两者在用法上很相似。
2.互斥量(锁)使用
2.1 什么是互斥量(锁)?
这样比喻:单位上有一台打印机(共享数据a),你要用打印机(线程1要操作数据a),同事老王也要用打印机(线程2也要操作数据a),但是打印机同一时间只能给一个人用,此时,规定不管是谁,在用打印机之前都要向领导申请许可证(lock),用完后再向领导归还许可证(unlock),许可证总共只有一个,没有许可证的人就等着在用打印机的同事用完后才能申请许可证(阻塞,线程1lock互斥量后其他线程就无法lock,只能等线程1unlock后,其他线程才能lock)。那么,打印机就是共享数据,访问打印机的这段代码就是临界区,这个必须互斥使用的许可证就是互斥量(锁)。
互斥量是为了解决数据共享过程中可能存在的访问冲突的问题。这里的互斥量保证了使用打印机这一过程不被打断。
互斥量:采用互斥对象机制。只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。
2.2 互斥量(锁)怎么使用?
首先需要#include;(std::mutex和std::lock_guard都在头文件中声明。)
然后需要实例化std::mutex对象;
需要在进入临界区之前对互斥量加锁,退出临界区时对互斥量解锁;
lock()与unlock():
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
m.lock();
cout << "proc1函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 2 << endl;
m.unlock();
}
void proc2(int a)
{
m.lock();
cout << "proc2函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 1 << endl;
m.unlock();
}
int main()
{
int a = 0;
thread t1(proc1, a);
thread t2(proc2, a);
t1.join();
t2.join();
return 0;
}
程序实例化mutex对象m,本线程调用成员函数m.lock()会发生下面 2 种情况: (1)如果该互斥量当前未上锁,则本线程将该互斥量锁住,直到调用unlock()之前,本线程一直拥有该锁。 (2)如果该互斥量当前被其他线程锁住,则本线程被阻塞,直至该互斥量被其他线程解锁,此时本线程将该互斥量锁住,直到调用unlock()之前,本线程一直拥有该锁。
不推荐实直接去调用成员函数lock(),因为如果忘记unlock(),将导致锁无法释放,使用lock_guard或者unique_lock则能避免忘记解锁带来的问题。
2.3 lock_guard
std::lock_guard()是什么呢?它就像一个保姆,职责就是帮你管理互斥量,就好像小孩要玩玩具时候,保姆就帮忙把玩具找出来,孩子不玩了,保姆就把玩具收纳好。
其原理是:声明一个局部的std::lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:创建即加锁,作用域{ }结束自动解锁。从而使用std::lock_guard()就可以替代lock()与unlock()。
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
lock_guard<mutex> g1(m);//用此语句替换了m.lock();lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
cout << "proc1函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 2 << endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁
void proc2(int a)
{
{
lock_guard<mutex> g2(m);
cout << "proc2函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 1 << endl;
}//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
cout << "作用域外的内容3" << endl;
cout << "作用域外的内容4" << endl;
cout << "作用域外的内容5" << endl;
}
int main()
{
int a = 0;
thread t1(proc1, a);
thread t2(proc2, a);
t1.join();
t2.join();
return 0;
}
2.4 unique_lock
std::unique_lock类似于lock_guard,只是std::unique_lock用法更加丰富,同时支持std::lock_guard()的原有功能。 使用std::lock_guard后不能手动lock()与手动unlock();使用std::unique_lock后可以手动lock()与手动unlock();
3. 条件变量
3.1如何使用?
头文件<condition_variable>针对条件变量提供了两个class,分别为condition_variable和condition_variable_any
condition_variable
std::condition_variable类搭配std::mutex类来使用,std::condition_variable对象(std::condition_variable cond;)的作用不是用来管理互斥量的,它的作用是用来同步线程,它的用法相当于编程中常见的flag标志(A、B两个人约定flag=true为行动号角,默认flag为false,A不断的检查flag的值,只要B将flag修改为true,A就开始行动)。
类比到std::condition_variable,A、B两个人约定notify_one为行动号角,A就等着(调用wait(),阻塞),只要B一调用notify_one,A就开始行动(不再阻塞)。
3.2 condition_variable
condition_variable用来唤醒一个或多个等待在某特定条件上的线程,下面列出了condition_variable提供的操作:所有等待(wait)某个条件的线程都必须使用相同的mutex,且必须使用unique_lock绑定mutex,并且让wait()等待在unique_lock上,否则会发生不明确的行为.
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx; // 全局互斥锁.
std::condition_variable cond_; // 全局条件变量.
bool ready = false; // 全局标志位.
void do_print_id(int id)
{
std::unique_lock <std::mutex> lck(mtx);
while (!ready) // 如果标志位不为 true, 则等待...
cond_.wait(lck); // 当前线程被阻塞, 当全局标志位变为 true 之后,
// 线程被唤醒, 继续往下执行打印线程编号id.
std::cout << "thread " << id << '\n';
}
void go()
{
std::unique_lock <std::mutex> lck(mtx);
ready = true; // 设置全局标志位为 true.
cond_.notify_all(); // 唤醒所有等待的线程. notify_one():唤醒一个等待的线程
}
int main()
{
std::thread threads[10];
// spawn 10 threads:
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(do_print_id, i);
std::cout << "10 threads ready to race...\n";
go(); // go!
for (auto & th:threads)
th.join();
return 0;
}
3.3 condition_variable_any
condition_variable_any提供的操作与condition_variable类似(见上图),但是不提供native_handle()和notify_all_at_thread_exit()
condition_variable_any不要求使用std::unique_lock对象当做lock
4. 原子类型atomic<>
原子操作指“不可分割的操作”,也就是说这种操作状态要么是完成的,要么是没完成的,不存在“操作完成了一半”这种状况。互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量(操作变量时加锁防止他人干扰)。 std::atomic<>是一个模板类,使用该模板类实例化的对象,提供了一些保证原子性的成员函数来实现共享数据的常用操作。
可以这样理解: 在以前,定义了一个共享的变量(int i=0),多个线程会用到这个变量,那么每次操作这个变量时,都需要lock加锁,操作完毕unlock解锁,以保证线程之间不会冲突;但是这样每次加锁解锁、加锁解锁就显得很麻烦,那怎么办呢? 现在,实例化了一个类对象(std::atomic I=0)来代替以前的那个变量(这里的对象I你就把它看作一个变量,看作对象反而难以理解了),每次操作这个对象时,就不用lock与unlock,这个对象自身就具有原子性(相当于加锁解锁操作不用你写代码实现,能自动加锁解锁了),以保证线程之间不会冲突。
提到std::atomic<>,你脑海里就想到一点就可以了:std::atomic<>用来定义一个自动加锁解锁的共享变量(“定义”“变量”用词在这里是不准确的,但是更加贴切它的实际功能),供多个线程访问而不发生冲突。
//原子类型的简单使用
std::atomic<bool> b(true);
b=false;
参考资源:
1.C++多线程并发基础入门教程
2.C++ 标准库 条件变量:condition_variable
3.C++11并发编程-条件变量(condition_variable)详解