1.为什么需要互斥锁
软件系统中往往存在多个线程共同来工作,如果多个线程同时对系统中的临界资源(或者代码段)进行访问的话,很容易发生混乱。比如打印机是临界资源,如果多个线程同时都要调用打印机来进行打印的话会出现什么情况?例如有两个线程都要打印,第一个线程调用打印机后在它所处的时间片内还没有打印完成的时候,打印机被第二个线程占有了,这样第一个线程的打印任务就被迫终止了。
其实在笔者上学的时候,我们老师讲这里的时候举了一个很简单的例子:你家的卫生间只有一个,家里有多个人,在一个人上卫生间的时候,如果其他人也想上,怎么办?把门锁上。
互斥锁(量)就是就是一个类似于门锁的东西。
2.std::mutex在线程并发中的使用
比如下面的例子,我在两个线程中打印信息,此时打印信息的代码段就是临界资源,它只能被一个线程使用,打印完成后另一个线程才能使用。
#include <iostream> // std::cout
#include <thread> // std::thread
void print_block (int n, char c) {
for (int i=0; i<n; ++i) { std::cout << c; }
std::cout << '\n';
}
int main ()
{
std::thread th1 (print_block,50,'*');
std::thread th2 (print_block,50,'$');
th1.join();
th2.join();
return 0;
}
结果我的显示结果如下:
咦,怎么和我想要的不一样呢?我其实期望一个线程打印50个*,另一个线程再打印50个$,结果出现了上面这样的结果。
这个就是线程并发没有做保护的结果。
下面使用c++中的std::mutex来进行保护。当一个线程打印信息的时候就上锁,打印完了再释放锁给另一个线程使用。
// mutex example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex
std::mutex mtx; // mutex for critical section
void print_block (int n, char c) {
// critical section (exclusive access to std::cout signaled by locking mtx):
mtx.lock();
for (int i=0; i<n; ++i) { std::cout << c; }
std::cout << '\n';
mtx.unlock();
}
int main ()
{
std::thread th1 (print_block,50,'*');
std::thread th2 (print_block,50,'$');
th1.join();
th2.join();
return 0;
}
运行结果如下:
这样就有了我期望的结果。两次执行结果不一样,这个是两个线程执行先后的事情。
在这段代码中,调用std::mutex创建了mtx对象,在执行打印代码之前先调用 mtx.lock();相当于对后边的临界代码上锁,这时候其他线程就不能抢占这部分“资源”了。使用完成后释放锁mtx.unlock();,此时其他线程可以调用这部分临界代码。
就是这么简单,在临界代码调用前和调用后进行上锁和解锁操作。但是要记住,千万不能上了锁忘记解锁,这样就麻烦了,其他线程永远也不能访问这段临界代码了。
3.智能锁boost::mutex::scoped_lock在线程并发中的使用
c++中为了避免new运算符申请的内存块因为“人为”错误忘记释放而带来内存泄露,在新标准中引入了智能指针。智能指针名称上有指针两个字,但是它其实是一个模板类定义的对象,只是这个对象有指针的一些行为。用智能指针对代码中所使用的实际对象类型进行了包装,也就是在创建智能指针的时候传入了新申请的实际对象。智能指针在其构造函数中初始化,在析构函数中进行释放。正因为是对象,所以在对象作用域结束的时候,析构函数会自动调用,这样就避免了内存泄露。
类似的,因为使用锁的话需要上锁,需要解锁,如果对锁忘记解锁的话,就会很麻烦。所以智能锁就出现了。
下面看一下boost::mutex::scoped_lock的使用。
// mutex example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <boost/thread/mutex.hpp>
boost::mutex mut;
void print_block (int n, char c) {
// critical section (exclusive access to std::cout signaled by locking mtx):
boost::mutex::scoped_lock lock(mut);
for (int i=0; i<n; ++i) { std::cout << c; }
std::cout << '\n';
}
int main ()
{
std::thread th1 (print_block,50,'*');
std::thread th2 (print_block,50,'$');
th1.join();
th2.join();
return 0;
}
执行结果如下:
boost::mutex::scoped_lock顾名思义,是一个区域锁,也就是在作用域内有效,当离开作用域自动释放锁。传入参数是一个boost::mutex类型的对象。
scoped_lock就是把锁封装到一个对象里面。锁的初始化放到构造函数里边,锁的释放放到析构函数。这样当锁离开其作用域时,析构函数会自动执行并释放锁。即使运行时抛出异常,由于析构函数仍然会自动运行,所以锁仍然能自动释放。不会像std::mutex那样,上锁之后如果异常返回而忘记解锁,就彻底锁死了。
下面是我从boost源码中截取的scoped_lock这个模板类的声明:
template <typename Lock>
class scoped_lock {
public:
scoped_lock(Lock &lock);
virtual ~scoped_lock();
private:
scoped_lock(const scoped_lock &);
scoped_lock& operator = (const scoped_lock &);
private:
Lock &lock_;
};
4.编译问题
1)使用std::mutex的时候,如果用g++命令编译,记得加上-lpthread
g++ mutex.cpp -o mutex -std=c++11 -lpthread
2)使用boost::mutex::scoped_lock的时候,使用g++编译,命令如下:
g++ mutex.cpp -o mutex -std=c++11 -lboost_system