C++互斥量mutex和原子变量atomic
1.互斥量mutex:
1.1 多线程的问题:
当使用多线程编程时,如果多个线程同时访问同一资源很可能会出现错误,下面程序展示了这点。
#include <thread>
#include <mutex>
#include <atomic>
#include <iostream>
void func1(int& a)
{
for (int i = 0; i < 100*1000; i++) {
a+=1;
}
}
int main()
{
int a = 0;
std::thread t1(func1,std::ref(a));
std::thread t2(func1, std::ref(a));
t2.join();
t1.join();
std::cout << a<<'\n';
a = 0;
t1=std::thread(func1, std::ref(a));
t2=std::thread(func1, std::ref(a));
t2.join();
t1.join();
std::cout << a;
return 0;
}
上述程序我们开启两个线程,每个线程对a进行10万次+1,理论上最终应该是20万,然而运行后结果如下:
显然,结果并不是20万,而且两次对a进行双线程操作后结果也不一样,这就是产生的问题。因此我们需要一种方式让资源同时仅会被访问一次,于是我们引入了互斥锁mutex。
1.2 互斥锁mutex的使用:
互斥锁mutex是c++11及更高版本所有的,需要包含头文件#include <mutex>。它可以使用lock函数锁住互斥量。首先看下列代码:
#include <thread>
#include <mutex>
#include <atomic>
#include <iostream>
std::mutex mtx;
void func1(int& a)
{
for (int i = 0; i < 100*1000; i++) {
mtx.lock();
a+=1;
mtx.unlock();
}
}
int main()
{
int a = 0;
std::thread t1(func1,std::ref(a));
std::thread t2(func1, std::ref(a));
t2.join();
t1.join();
std::cout << a<<'\n';
a = 0;
t1=std::thread(func1, std::ref(a));
t2=std::thread(func1, std::ref(a));
t2.join();
t1.join();
std::cout << a;
return 0;
}
下面是运行结果
可以发现成功输出了正确的值。
互斥锁使用lock可以锁住互斥量,然后此时如果互斥量mtx被其它线程锁住那么将阻塞直到该互斥量被unlock。将互斥量上锁之后一定要使用unlock(),否则会造成资源被锁住,进而有可能产生死锁。
1.3 lock_guard:
由于互斥锁上锁需要解锁,然而有时写代码时会忘记解锁。此外就算足够严谨,在代码里给出了解锁,也有可能会因程序异常终止而无法释放锁,例如下面伪代码:
void func()
{
mtx.lock();
otherFunc();
mtx.unlock();
}
如果函数otherFunc出现了异常或者触发了某种设置导致程序或者该函数直接终止,那么将无法调用unlock()。因此能够自动释放锁的方式是必要的。我们可以想到写一个类,然后在析构时释放锁。lock_guard就有如下效果。使用了lock_guard的程序如下:
#include <thread>
#include <mutex>
#include <atomic>
#include <iostream>
std::mutex mtx;
void func1(int& a)
{
for (int i = 0; i < 100*1000; i++) {
std::lock_guard<std::mutex> lck(mtx);
a+=1;
}
}
int main()
{
int a = 0;
std::thread t1(func1,std::ref(a));
std::thread t2(func1, std::ref(a));
t2.join();
t1.join();
std::cout << a<<'\n';
a = 0;
t1=std::thread(func1, std::ref(a));
t2=std::thread(func1, std::ref(a));
t2.join();
t1.join();
std::cout << a;
return 0;
}
结果仍然是正确的。
1.4 unique_lock
unique_lock也可以自动释放锁。和lock_guard相比unique_lock有更多功能,最常见的就是可以和条件变量共同使用。不过功能多付出的代价是效率和lock_guard相比会低一些。因此如果只是需要加锁和自动释放锁,则只使用lock_guard会更好一些,下面是unique_lock的使用方法:
void func1(int& a)
{
for (int i = 0; i < 100*1000; i++) {
std::unique_lock<std::mutex> lck(mtx);
a+=1;
}
}
在简单的加锁上,unique_lock和lock_guard使用方式几乎一样。
2.原子变量atomic:
2.1 原子操作:
原子操作表示在多个线程访问同一个全局资源的时候,能够确保所有其他线程都不在同一时间访问该资源。也就是确保了在同一时刻只有一个线程对这个资源进行访问。虽然上文的互斥对象也可以共享资源的访问的保护,但是原子操作更加接近底层,因而效率一般比互斥对象更高。
2.2 原子变量的使用:
原子变量需要c++11以上版本并包含头文件#include<atomic>。使用如下:
#include <thread>
#include <mutex>
#include <atomic>
#include <iostream>
//原子变量和int不是一种类型,因此需要使用std::atomic<int>修饰参数
void func1(std::atomic<int>& a)
{
for (int i = 0; i < 100*1000; i++) {
a+=1;
}
}
int main()
{
std::atomic<int> a(0);//定义原子变量
std::thread t1(func1,std::ref(a));
std::thread t2(func1, std::ref(a));
t2.join();
t1.join();
std::cout << a<<'\n';
a = 0;
t1=std::thread(func1, std::ref(a));
t2=std::thread(func1, std::ref(a));
t2.join();
t1.join();
std::cout << a;
return 0;
}
这样无需加锁也能得到正确的结果。
对于基本类型还可以使用预先定义的别名,如
std::atomic_int a(0);
std::atomic_char b('0');
原子变量的效率比互斥量要高很多。