转载自:C++ - 线程安全访问控制 - MyRedstone
共享资源,包括全局变量,静态变量,共享内存,共享文件等。
#include
#include
#include
#include
#include
using namespace std;
class A
{
public:
//收集数据的函数
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
std::lock_guard guard_lock(my_mutex);
//my_mutex.lock(); //给共享数据msgRecvQueue上锁(防止多个线程同时访问共享数据msgRecvQueue)
msgRecvQueue.push_back(i);
//my_mutex.unlock(); //给共享数据msgRecvQueue解锁,其他线程才可以访问这个共享数据
}
}
bool outMsgLULPro(int& command)
{
std::lock_guard sbguard(my_mutex);
//my_mutex.lock(); //因为这里又要访问共享数据msgRecvQueue,所以要在这里赶紧加把锁
if (!msgRecvQueue.empty()) //如果队列不为空的时
{
//取数据 & 去除这个数据
int commond = msgRecvQueue.front();
msgRecvQueue.pop_front();
//my_mutex.unlock();
return true;
}
//my_mutex.unlock(); //else的,如果队列为空
return false;
}
//取出数据的线程
void outMsgRescQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULPro(command); //这个command是传引用的方式,所以也会跟着变
if (result == true) //若完成了从list中取出数据并从原来的list中剔除这个数据的动作
{
cout << "outMsgRescQueue()执行,取出一个元素" << command << endl;
}
else
{
cout << "outMsgRescQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::listmsgRecvQueue; //接收命令的list
std::mutex my_mutex; //私有互斥量
};
int main()
{
A myobja;
std::thread myOutMsgobj(&A::outMsgRescQueue, &myobja);
std::thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
myOutMsgobj.join();
myInMsgobj.join();
cout << "main over" << endl;
return 0;
}
比如在这个代码中(我们加锁完毕,是线程安全的了),
msgRecvQueue就是在类中的全局可见的,属于类内的共享内存,因此本身是线程不安全的,可以被多个线程同时访问。
但是:
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
这句代码是线程不安全的,其有两端代码组成:
cout << "inMsgRecvQueue()执行,插入一个元素" << i
以及cout <
这个<<输出运算是全局的,因此也是本身线程不安全的,可以被多个线程访问,因此我们常常发现用cout打印输出,控制台的输出顺序是混乱的。
当然这个解决办法要么对cout加锁(之前的博客有),要么用printf函数替代!
|建议封装像智能指针一样的对象来对锁进行管理,比如我们就封装了一个auto_lock,在构造时申请锁,析构中释放锁,保证不会忘记解锁。
每个锁只锁一个唯一共享资源,这样才能保证锁应用的单一,也能确保加锁的范围尽量小。对于共享全局资源,应该根据实际需要,每类或者每个资源有一把锁。这样,这把锁只锁对当前这个资源进行访问的代码,通常这样的代码都会是比较简单的资源操作代码,不会是复杂的函数调用等。相反,如 果我们对几类或几个资源共用一把锁。这把锁的责任范围就大了,使用复杂,很难理清锁之间的关系(有 没有释放锁,或者锁之间的嵌套加锁等),容易导致死锁问题。
使用锁时,尽量减少锁的使用范围。我们使用锁,为了方便会大范围的加锁如:直接锁几个函数调用。这种使用,一方面会导致多线程执行效率的低下,容易变成串行执行(将几个函数的调用变成串行了,不过话说回来,我们的真实需求是有时候就是有串行的需求的,比如推理和后处理,肯定这个推理跟后处理是串行的,不然后处理不报错了么?);另一方面,容易出现锁未释放,或者锁的代码中再加锁的场景,最后导致死锁。 所以,对锁操作的最好办法,就是只锁简单资源操作代码。对应资源访问完后,马上释放锁。尽量在 函数内部靠近资源操作的地方加锁而不是靠近线程、函数外部加锁。
加上一把锁后,在释放之前,不能再加锁。
典型的锁中加锁的场景:
代码中对几个容器的同时遍历,每个容器一把锁,就导致需要加多把锁。
这种场景的解决方法:先加一把锁,对一个容器遍历,选择出合乎要求的数据,并保存在临时变量中; 再加另一把锁,使用临时变量,再对其他容器遍历。
锁中加锁。必须保证加锁的顺序是一样的,比如先加的锁后解锁, Lock1 Lock2 Unlock2 Unlock1 ,则其他地方的加锁顺序,必须与这里的顺序一样,避免死锁,不允许出现: Lock2 Lock1 Unlock2 Unlock1 。
以下是一个示例代码,演示了如何创建一个会导致死锁的情况,其中两个线程分别尝试遍历两个容器,但以不一致的锁顺序来锁定互斥锁:
#include
#include
#include
#include
std::vector container1;
std::vector container2;
std::mutex mutex1;
std::mutex mutex2;
void thread1()
{
std::lock_guard lock1(mutex1);
for (const auto& item : container1) {
// 处理 container1 中的数据
std::cout << "Thread 1 processing container1: " << item << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟一些工作
std::lock_guard lock2(mutex2);
for (const auto& item : container2) {
// 处理 container2 中的数据
std::cout << "Thread 1 processing container2: " << item << std::endl;
}
std::cout << "Thread 1 completed." << std::endl;
}
void thread2()
{
std::lock_guard lock2(mutex2);
for (const auto& item : container2) {
// 处理 container2 中的数据
std::cout << "Thread 2 processing container1: " << item << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟一些工作
std::lock_guard lock1(mutex1);
for (const auto& item : container1) {
// 处理 container1 中的数据
std::cout << "Thread 2 processing container2: " << item << std::endl;
}
std::cout << "Thread 2 completed." << std::endl;
}
int main()
{
container1.push_back(1);
container1.push_back(2);
container2.push_back(3);
container2.push_back(4);
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
运行结果如下:
可以看到控制台并未结束,一直停留着!
好的,那么分析一下这个死锁是如何发生的:
首先我们的容器container1有两个元素:1和2。容器container2有两个元素3和4。
然后开启线程,进入线程函数,这个进入thread1和thread2线程函数的顺序是与主函数中的定义顺序无关的,这只与系统调度有关系。根据上面控制台截图来看,程序先进入线程函数thread2中:
那我们先看线程函数thread2中:
std::lock_guard
这句话直接的作用范围就是下面的所有语句,明显是对下面的:
std::cout << "Thread 2 processing container1: " << item << std::endl;中的cout<
以及container2两个共享资源进行加锁了
当然对于后面的container1和std::cout << "Thread 2 processing container2: " << item << std::endl;中的cout<
那么从thread2函数的上到下来看,迭代访问取值container2的元素,因此控制台先输出了:
"Thread 2 processing container2: 3"
这个时候注意了,线程函数thread1可没闲着,这个时候我们进入线程函数thread1看看:
std::lock_guard lock1(mutex1);
for (const auto& item : container1) {
// 处理 container1 中的数据
std::cout << "Thread 1 processing container1: " << item << std::endl;
}
明显是对整个函数的共享资源进行了加锁,这个时候由于线程函数thread2还有个睡眠1s,所以这个时候线程函数thread1拿到了mutex1的锁头进行了加锁,这个时候因此就打印出了:
"Thread 1 processing container1: 1"
由于两个线程函数是同时的,因此这个时候在线程函数thread2中,由于mutex1已经在线程函数thread1中被锁定了,所以在线程函数thread2中拿不到mutex1的锁头了,因此这个时候是被在
阻塞,程序无法执行下去:
同理:
这个时候在线程函数thread1中,由于mutex2已经在线程函数thread2中被锁定了,所以在线程函数thread1中拿不到mutex2的锁头了,因此这个时候是被在
阻塞,程序无法执行下去:
当然每个线程函数,thread1()会因为里面的循环进行打印出:
"Thread 1 processing container1: 1"
"Thread 1 processing container1: 2"
thread1()会因为里面的循环进行打印出:
"Thread 2 processing container2: 3"
"Thread 2 processing container2: 4"
这样就出现了两个线程函数,都有堵塞,相互等待,程序运行不下去!
那么怎么修改呢,摆脱这个死锁呢?下面给出代码:
#include
#include
#include
#include
std::vector container1;
std::vector container2;
std::mutex mutex1;
std::mutex mutex2;
void thread1()
{
std::lock_guard lock1(mutex1);
for (const auto& item : container1) {
// 处理 container1 中的数据
std::cout << "Thread 1 processing container1: " << item << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟一些工作
std::lock_guard lock2(mutex2);
for (const auto& item : container2) {
// 处理 container2 中的数据
std::cout << "Thread 1 processing container2: " << item << std::endl;
}
std::cout << "Thread 1 completed." << std::endl;
}
void thread2()
{
std::lock_guard lock1(mutex1);
for (const auto& item : container1) {
// 处理 container1 中的数据
std::cout << "Thread 2 processing container1: " << item << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟一些工作
std::lock_guard lock2(mutex2);
for (const auto& item : container2) {
// 处理 container2 中的数据
std::cout << "Thread 2 processing container2: " << item << std::endl;
}
std::cout << "Thread 2 completed." << std::endl;
}
int main()
{
container1.push_back(1);
container1.push_back(2);
container2.push_back(3);
container2.push_back(4);
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
这个代码分析如下:
同时启动线程函数thread1和thread2,这时按照上面控制台的输出。恰好线程2先运行快一点,然后拿到了mutex1互斥锁的锁头,这个时候由于mutex1已经在线程函数thread2中被锁定了,所以在线程函数thread1中拿不到mutex1的锁头了,因此这个时候是被阻塞,程序无法执行下去:
因此我们在线程函数2就可以看到全部的输出(这个时候线程函数1一直被阻塞在第一句):
"Thread 2 processing container1: 1"
"Thread 2 processing container1: 2"
"Thread 2 processing container2: 3"
"Thread 2 processing container2: 4"
"Thread 2 completed."
然后全部线程函数2运行完毕了,这个时候跳出了mutex1和mutex2两个锁的作用范围了,因此这个时候这两把锁也就是被释放了,那么线程函数1的阻塞就被解除了,进而就完成了对mutex1和mutex2的加锁,因此线程函数1就得到了全部输出:
"Thread 1 processing container1: 1"
"Thread 1 processing container1: 2"
"Thread 1 processing container2: 3"
"Thread 1 processing container2: 4"
"Thread 1 completed."
关于死锁的部分,我们下一篇博文再专门介绍一下!!!!
由于文件在不同进程间访问,无法保证互斥。当然,可以在进程间加进程锁,但只受限于我们能加锁的进程,对于第三方进程等无法保证。这样,当多个进程同时对文件进行写操作时,将会导致文件数据破坏,或文件写失败等问题。
当数据库系统本身的访问接口带有互斥机制时,当多个进程同时访问时,可以保证数据库数据的完整。
共享内存,只限制于使用共享内存的几个进程,需要我们对这些访问共享内存的进程加锁。但由于共享内存,第三方进程等无法访问,这也能比较好的保护数据,避免文件系统存在的问题。
Socket 消息机制,由操作系统 Socket 通讯机制保证互斥,在多个进程间,通过消息来保证数据的互斥。 进程的消息都是操作系统转发而来的独立数据,属于进程私有数据,不存在进程间并行访问的问题。
支持多线程并行访问的函数称之为可重入函数。设计可重入函数时,尽量使用局部变量和函数参数来传递数据,在多线程并行访问时,互相之间不会受影响。相反,如果使用全局变量、静态变量,就需要同步。一些库函数也是非线程安全,调用时可能会出现多线程并发访问问题。
可重入函数和不可重入函数,需要阅读下面的博文:
浅谈可重入函数与不可重入函数-CSDN博客
这条规则是对加锁范围尽量小(只锁对应资源操作代码)规则的补充。不能把调用函数也加到加锁范围中。因为被调用函数的内部到底做了什么事情,是如何做的,调用者可能不是很清楚。尤其是 当被调用函数内部又加锁的情况,就容易导致两个锁互锁,导致死锁。而且这种死锁 情况还是比较难分析。因为我们调用函数,很多时候只关注函数实现的功能 ,而忽略函数内部的具体实现。其次,锁中调用函数,也会把对资源操作的代码扩大化,不利于并行效率。更主要的是,这种操作, 由于加锁的范围变大,引起死锁的可能就增大。
跳转语句包含 return、break、continue、goto 等。如果锁中有宏调用的代码,要特别注意,分析宏中是否存在隐含的跳转语句。 在函数返回时忘记把锁释放,特别是存在很多分支都可能返回的时候,可能一些分支会忘记释放锁。