一、线程的同步和互斥
同步是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。
互斥是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
二、加锁与解锁
上一章讲过同一个进程里的所有线程是共享资源池的。在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。
多个线程同时访问共享资源的时候需要用到互斥量,当一个线程锁住了互斥量后,其他线程必须等待这个互斥量解锁后才能访问它。
在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问。只要某一个线程上锁了,那么就会强行霸占公共资源的访问权,其他的线程无法访问直到这个线程解锁了。
互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。例如下面这个例子中,g_page_mutex这把锁保护了g_page这个map。
#include<iostream> #include<thread> #include<mutex> #include<string> #include<map> using namespace std; map<string, string> g_page; mutex g_page_mutex; void save_page(const string &url) { this_thread::sleep_for(chrono::seconds(2)); string result = "fake content"; g_page_mutex.lock(); //上锁 g_page[url] = result; g_page_mutex.unlock();//解锁 } int main() { thread t1(save_page, "http://foo"); thread t2(save_page, "http://bar"); t1.join(); t2.join(); for (const auto &pair : g_page) { cout << pair.first << "=>" << pair.second << endl; } }
运行结果:
lock_guard类介绍
但是上面手动上锁解锁的操作有个隐患是,如果上锁和解锁中间的代码出现了问题,那么这把锁就一直无法解开,成为了死锁。
c++11也提供了一种更安全更方便的上锁和解锁的方式,就是lock_guard这个类。从名字可以看出,这是一个监视锁的类。
#include<iostream> #include<thread> #include<mutex> #include<string> #include<map> using namespace std; map<string, string> g_page; mutex g_page_mutex; void save_page(const string &url) { this_thread::sleep_for(chrono::seconds(2)); string result = "fake content"; lock_guard<mutex> lc(g_page_mutex); //或者也可以写成 //lock(g_page_mutex); //lock_guard<mutex> lc(g_page_mutex, adopt_lock);
//或者也可以写成
//lock_guard<mutex> lc(g_page_mutex, defer_lock);
//lock(g_page_mutex);
g_page[url] = result; } int main() { thread t1(save_page, "http://foo"); thread t2(save_page, "http://bar"); t1.join(); t2.join(); for (const auto &pair : g_page) { cout << pair.first << "=>" << pair.second << endl; } }
在 lock_guard 对象构造时,传入的 Mutex 对象(即它所管理的 Mutex 对象)会被当前线程锁住。在lock_guard 对象被析构时,它所管理的 Mutex 对象会自动解锁,由于不需要程序员手动调用 lock 和 unlock 对 Mutex 进行上锁和解锁操作,因此这也是最简单安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的 Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常处理代码。值得注意的是,lock_guard 对象并不负责管理 Mutex 对象的生命周期,lock_guard 对象只是简化了 Mutex 对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个 lock_guard 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard 的生命周期结束之后,它所管理的锁对象会被解锁。
lock_guard构造时还可以传入一个参数adopt_lock或者defer_lock。adopt_lock表示是一个已经锁上了锁,defer_lock表示之后会上锁的锁。
unique_lock介绍
上文介绍的lock_guard类最大的缺点也是简单,没有给程序员提供足够的灵活度,因此C++11定义了另一个unique_guard类。这个类和lock_guard类似,也很方便线程对互斥量上锁,但它提供了更好的上锁和解锁控制。
顾名思义,unique_lock以独占所有权的方式管理mutex的mutex对象的上锁和解锁操作。所谓独占所有权,就是指没有其他的unique_lock对象同时拥有某个mutex对象。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。
unique_lock 对象也能保证在其自身析构时它所管理的 Mutex 对象能够被正确地解锁(即使没有显式地调用 unlock 函数)。因此,和 lock_guard 一样,这也是一种简单而又安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的 Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常处理代码。
#include<thread> #include<iostream> #include<mutex> using namespace std; struct Box { explicit Box(int num) : num_things{num} {} int num_things; mutex m; }; void transfer(Box &from, Box &to, int num) { unique_lock<mutex> lg1(from.m); unique_lock<mutex> lg2(to.m); //lock(lg1, lg2); from.num_things -= num; to.num_things += num; cout<< "from.num_things ="<<from.num_things<<endl; cout<<"to.num_things = "<<to.num_things<<endl; } int main() { Box a(100); Box b(50); thread t1(transfer,ref(a) ,ref(b),5); thread t2(transfer, ref(a), ref(b), 10); t1.join(); t2.join(); }
运行结果:
需要注意的是,mutex锁住的对象,其实就是mutex变量能够访问到的变量。只有当mutex为全局变量,那么所有的全局变量都会被锁住。如果mutex为局部变量,那么mutex的作用范围也仅限于{}范围内的变量。比如上面的例子中,m是每个对象都关联的一个mutex变量,所以m可以访问到每个对象的num_things这个变量。假如对象a的m上锁以后,一个时刻就只能有一个线程可以访问对象a。注意这种情况下是可以有两个线程可以同时分别去访问a和b的,因为a的m无法访问到b的任何变量,所以a的m是锁不住b的,同理,b也无法锁住a。
参考:
https://www.cnblogs.com/code-wangjun/p/7476559.html
https://blog.csdn.net/daaikuaichuan/article/details/82950711