锁
1、简介
多线程中的锁主要有五类:互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,所得功能与性能成反比。
2、互斥锁
互斥锁用于控制多个线程对它们之间共享资源互斥访问的一个信号量。也就是说为了避免多个线程在某一时刻同时操作一个共享资源,例如一个全局变量,任何一个线程都要使用初始锁互斥地访问,以避免多个线程同时访问发生错乱。
在某一时刻只有一个线程可以获得互斥锁,在释放互斥锁之前其它线程都不能获得互斥锁,以阻塞的状态在一个等待队列中等待。
用法:在C++中,通过构造std::mutex的实例创建互斥单元,调用成员函数lock()来锁定共享资源,调用unlock()来解锁。不过一般不使用这种解决方案,更多的是使用C++标准库中的std::lock_guard类模板,实现了一个互斥量包装程序,提供了一种方便的RAII风格的机制在作用域块中。
例子:
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <stdexcept>
//编译g++ huchiLock.cpp -lpthread
using namespace std;
int g_num = 0;
std::mutex g_mutex;
void ThreadFunc(int a)
{
cout << "启动线程:" << a << endl;
for (int i = 0; i < 1000000; i++)
{
//g_mutex.lock();
std::lock_guard<std::mutex> m(g_mutex);//作用域内的程序会独占执行
g_num++;
//g_mutex.unlock();
}
}
int main()
{
for (int i = 0; i < 4; i++)
{
std::thread t(ThreadFunc, i);
t.detach();
}
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
cout << "g_num:" << g_num << endl;
return 0;
}
//高阶版,将上述main()函数的函数名更改,再更改以下的mainTest()即可执行。两个方法的执行的结果相同,原理也相同。
int mainTest()
{
std::vector<std::thread *> ts;
for (int i = 0; i < 4; i++)
{
std::thread *t = new std::thread(ThreadFunc, i);
//t.detach();
ts.push_back(t);
}
for (auto begin = ts.begin(); begin != ts.end(); begin++)
(*begin)->join();
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
cout << "g_num:" << g_num << endl;
return 0;
}
3、条件锁
条件锁就是所谓的条件变量,当某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态,一旦条件满足则以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见的就是在线程池中,初始情况下因为没有任务使得任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒该线程来处理这个任务。
例子:
#include <iostream>
#include <thread>
#include <mutex>
#include <deque>
#include <stdexcept>
#include <condition_variable>
using namespace std;
std::deque<int> q;
std::mutex mu;
std::condition_variable cond;
void function_1() //生产者
{
int count = 10;
while (count > 0)
{
std::unique_lock<std::mutex> locker(mu);
q.push_front(count);
locker.unlock();
cond.notify_one(); // Notify one waiting thread, if there is one.
std::this_thread::sleep_for(std::chrono::seconds(1));
count--;
}
}
void function_2() //消费者
{
int data = 0;
while (data != 1)
{
std::unique_lock<std::mutex> locker(mu);
while (q.empty())
cond.wait(locker); //
data = q.back();
q.pop_back();
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
}
}
int main()
{
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
return 0;
}
4、自旋锁
当发生阻塞时,互斥锁可以让CPU去处理其他的任务;而自旋锁让CPU一直不断循环请求获取这个锁。
如果T1正在使用Public,而T2也想使用Public,此时T2肯定是得不到这个自旋锁的。与互斥锁相反,此时运行T2的处理器core2会一直不断地循环检查Public使用可用(自旋锁请求),直到获得到这个自旋锁为止。
例子:
// 用户空间用 atomic_flag 实现自旋互斥
#include <thread>
#include <vector>
#include <iostream>
#include <atomic>
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void f(int n)
{
for (int cnt = 0; cnt < 100; ++cnt) {
while (lock.test_and_set(std::memory_order_acquire)) // 获得锁
; // 自旋
std::cout << "Output from thread " << n << '\n';
lock.clear(std::memory_order_release); // 释放锁
}
}
int main()
{
std::vector<std::thread> v;
for (int n = 0; n < 10; ++n) {
v.emplace_back(f, n);
}
for (auto& t : v) {
t.join();
}
}
5、读写锁
因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。这就是简单的读者-写者模型。
6、死锁情况:
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
例如:假设线程1上锁成功,线程2上锁等待。但是线程1上锁成功后,抛出异常并退出,没有来得及释放锁,导致线程2“永久的等待下去”
例子:
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
using namespace std;
#if 0
//死锁
class LogFile {
std::mutex _mu;
std::mutex _mu2;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) {
std::lock_guard<std::mutex> guard(_mu);
std::lock_guard<std::mutex> guard2(_mu2);
f << msg << id << endl;
cout << msg << id << endl;
}
void shared_print2(string msg, int id) {
std::lock_guard<std::mutex> guard(_mu2);
std::lock_guard<std::mutex> guard2(_mu);
f << msg << id << endl;
cout << msg << id << endl;
}
};
void function_1(LogFile& log) {
for(int i=0; i>-1000; i--)
{
log.shared_print2(string("From t1: "), i);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
int main()
{
LogFile log;
std::thread t1(function_1, std::ref(log));
for(int i=0; i<1000; i++)
{
log.shared_print(string("From main: "), i);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
t1.join();
return 0;
}
#else
//解决方法
//c++标准库中提供了std::lock()函数,能够保证将多个互斥锁同时上锁.
//std::lock(_mu, _mu2);
//同时,lock_guard也需要做修改,因为互斥锁已经被上锁了,那么lock_guard构造的时候不应该上锁,只是需要在析构的时候释放锁就行了,使用std::adopt_lock表示无需上锁
//std::lock_guard<std::mutex> guard(_mu2, std::adopt_lock);
//std::lock_guard<std::mutex> guard2(_mu, std::adopt_lock);
class LogFile {
std::mutex _mu;
std::mutex _mu2;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) {
std::lock(_mu, _mu2);
std::lock_guard<std::mutex> guard(_mu, std::adopt_lock);
std::lock_guard<std::mutex> guard2(_mu2, std::adopt_lock);
f << msg << id << endl;
cout << msg << id << endl;
}
void shared_print2(string msg, int id) {
std::lock(_mu, _mu2);
std::lock_guard<std::mutex> guard(_mu2, std::adopt_lock);
std::lock_guard<std::mutex> guard2(_mu, std::adopt_lock);
f << msg << id << endl;
cout << msg << id << endl;
}
};
void function_1(LogFile& log) {
for(int i=0; i>-1000; i--)
{
log.shared_print2(string("From t1: "), i);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
int main()
{
LogFile log;
std::thread t1(function_1, std::ref(log));
for(int i=0; i<1000; i++)
{
log.shared_print(string("From main: "), i);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
t1.join();
return 0;
}
#endif
产生原因:
竞争资源。(竞争不可剥夺资源,竞争临时资源)
进程间推进顺序非法
预防死锁
资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
程序预防
建议尽量同时只对一个互斥锁上锁。
不要在互斥锁保护的区域使用用户自定义的代码,因为用户的代码可能操作了其他的互斥锁。
如果想同时对多个互斥锁上锁,要使用std::lock()。
给锁定义顺序(使用层次锁,或者比较地址等),每次以同样的顺序进行上锁。