在我们系统中或项目中,存在同时运行多个任务需要使用同一种资源的情况。比如说,同一个文件,线程1对其进行写操作,而线程2同时对这个文件进行写操作,如果线程1还没写完,此时线程2开始了,那么最终的结果显然会混乱。若操作对象为容器等,甚至程序会崩溃。
为了防止上述情况,保护共享资源,在线程里需要加锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。
下面我们用thread库创建两个线程,简单探索下加锁和不加锁的直观现象:
一. 不加锁的现象
#include<iostream>
#include<string>
#include<thread>
using namespace std;
void f() {
cout << "线程id:" << this_thread::get_id() << endl;
}
int main()
{
thread tTest1(f);
thread tTest2(f);
tTest1.join();
tTest2.join();
system("pause");
return 0;
}
上图是3次运行的结果,第一幅图结果使我们理想中的结果,实际结果却不一定一致。这是因为两个函数线程都在执行,而输出屏幕却只有一个,因此信息就会发生混乱。这样的线程是不具备安全性的,要想使其具有安全性,我们就可以使用锁来进行编程。
二. 加锁的现象
线程安全:当多个线程访问某一个类(对象或方法)时,对象对应的公共数据区始终都能正确输出,那么这个类(对象或方法)就是线程安全的。
1. 使用互斥锁(mutex):
注意:必须手动调用unlock(),否则资源不释放
#include<iostream>
#include<string>
#include<thread>
#include<mutex>
using namespace std;
mutex m_lock;
void f() {
m_lock.lock();
cout << "线程id:" << this_thread::get_id() << endl;
m_lock.unlock();
}
int main()
{
thread tTest1(f);
thread tTest2(f);
tTest1.join();
tTest2.join();
system("pause");
return 0;
}
正常运行下,无论多少次,输入都是我们理想得到的。在C++中,通过构造std::mutex的实例创建互斥元,调用成员函数lock()来锁定它,调用unlock()来解锁,这能满足我们的需求。不过一般不推荐这种做法,lock()和unlock()是成对出现的,需要手动解锁,若在手动释放锁前程序异常,没有调用unlock(),这个资源会一直被锁着,没发释放,会导致其他异常。
标准C++库提供了std::lock_guard类模板,实现了互斥元的RAII惯用语法。std::mutex和std::lock _ guard。都声明在< mutex >头文件中。
2. 使用std::lock _ guard
注意:析构时自动释放,不能手动调用unlock(),否则析构时再次调用,会报异常。
在作用域内创建lock_guard对象时,会尝试获得锁(mutex::lock()),没有获得就像其他锁一样阻塞在原地。在lock_guard的析构函数内会释放锁(mutex::unlock()),不需要我们手动释放。
#include<iostream>
#include<string>
#include<thread>
#include<mutex>
using namespace std;
mutex m_lock;
void f() {
lock_guard<mutex> lg(m_lock);
cout << "线程id:" << this_thread::get_id() << endl;
}
int main()
{
thread tTest1(f);
thread tTest2(f);
tTest1.join();
tTest2.join();
system("pause");
return 0;
}
实际编程中建议使用lock_guard,防止在获得锁后程序运行出现异常退出而导致锁死。
注意:使用lock_guard时不要手动unlock(),否则lock_guard的析构函数中解锁时就会报异常。
3. 使用unique_lock
注意:若没有手动调用unlock(),析构时自动释放;若手动调用unlock(),调用时释放。
unique_lock也是类模板,和lock_guard差不多,也是构造函数中加锁,析构函数解锁,符合RAII原则。但unique_lock解锁能力更灵活,给我们提供了更好的优化代码能力。
lock_guard在创建获得锁之后必须要等到析构时才会释放锁,这样就造成其他线程在执行不涉及临界资源的代码时浪费了时间,大大降低效率。
而unique_lock在创建获得锁之后可等到析构时才会释放锁,也可以手动释放锁,这样可以在执行不涉及临界资源的代码是解锁,提高效率。
#include<iostream>
#include<string>
#include<thread>
#include<mutex>
using namespace std;
mutex m_lock;
void f() {
unique_lock<mutex> lg(m_lock);
cout << "线程id:" << this_thread::get_id() << endl;
}
void ff() {
unique_lock<mutex> lg(m_lock);
cout << "ff() 加锁." << endl;
lg.unlock();
cout << "ff() 已经解锁了." << endl;
}
int main()
{
thread tTest1(f);
thread tTest2(f);
thread tTest3(ff);
thread tTest4(ff);
tTest1.join();
tTest2.join();
tTest3.join();
tTest4.join();
system("pause");
return 0;
}
注意:unique_lock 赋值给另一个unique_lock后,原先的锁就会被释放,和unique_ptr有点像。
参考:
C++多线程
C++11线程中的几种锁