- mutex的作用:保证线程安全。即多个线程访问同一资源时,为了保证数据的一致性,最简单的方式就是使用 mutex(互斥锁)。接下面介绍几种加锁形式。
- 第一种形式:lock/unlock形式(直接对共享的资源加锁和解锁,注意:加和解的必须是同一把互斥锁,在加锁和解锁之间的部分均可保证数据一致性),这种形式的弊端是如果在解锁前程序出现异常,那么互斥锁将不能正常解开,程序陷入死循环。
#include<iostream>
#include<thread>
#include<mutex>
#include<vector>
std::mutex g_mutex;
int g_count = 0;
void Counter1()
{
g_mutex.lock();
int i = ++g_count;
std::cout << "count: " << i << std::endl;
//前面代码如有异常,unlock 就调不到了
g_mutex.unlock();
}
int main()
{
const std::size_t SIZE = 4;
std::vector<std::thread> v;
v.reserve(SIZE);
for (std::size_t i = 0; i < SIZE; i++)
{
v.emplace_back(Counter1);
}
for (std::thread& t : v)
{
t.join();
}
system("pause");
return 0;
}
- 第二种形式:使用 lock_guard 自动加锁、解锁。原理是 RAII(Resource Acquisition Is Initialization)。 如果把资源(这里指互斥量)用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。 从下面的代码可看出,将互斥量mutex封装到lock_guard类中,待lock_guard对象的声明周期结束时则会通过自动调用析构函数的形式将释放互斥锁(解锁)。相似的类还有unique_lock,但是提供了更多功能. 区别: unique_lock和lock_guard都是管理锁的辅助类工具,都是RAII风格;它们是在定义时获得锁,在析构时释放锁。它们的主要区别在于unique_lock锁机制更加灵活,可以再需要的时候进行lock或者unlock调用,不非得是析构或者构造时,这一区别通过各自的成员函数容易看出。
#include<iostream>
#include<thread>
#include<mutex>
#include<vector>
std::mutex g_mutex;
int g_count = 0;
void Counter2()
{
std::lock_guard<std::mutex> lock(g_mutex);
//std::unique_lock<std::mutex> lock(g_mutex);
int i = ++g_count;
std::cout << "count: " << i << std::endl;
}
int main()
{
const std::size_t SIZE = 4;
std::vector<std::thread> v;
v.reserve(SIZE);
for (std::size_t i = 0; i < SIZE; i++)
{
v.emplace_back(Counter2);
}
for (std::thread& t : v)
{
t.join();
}
system("pause");
return 0;
}
- 代码解析:
- 以上例子中,出现emplace_back(),其功能与push_back()相似,可以根据参数调用对应类型的构造函数进行对象构建,相比push_back的优点是0拷贝。
- emplace_back()中的Counter参数,通常写作&Counter,具体为什么这么写也可没搞太清楚。无论是Counter还是&Counter都是thread类构造函数中的参数。可从其构造函数看出,参数类型是个模板参数。
- 另外,IO都是线程不安全的,如果不对 IO 进行同步,不同线程的输出很可能混乱在一起(尤其一条cout语句多个原子性(通俗地讲不能一步完成)的),所以一般都要对IO操作进行加锁。如上面代码则需要对cout单独加锁,并放到一个语句块中:
{
std::unique_lock<std::mutex> lock(g_io_mutex);
std::cout << "count: " << i << std::endl;
}