在学C++多线程时,会提到线程同步概念。下面举个例子说明线程同步的概念。
线程同步
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。这里的同步千万不要理解成那个同时进行,应是指协同、协助、互相配合。线程同步是指多线程通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步)也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的!
为什么要同步?
下面看一个火车站售票例子
#include<iostream>
using namespace std;
#include<thread>
int Count = 100;
void func(string name)
{
while(Count>0)
{
cout << name << "卖出了倒数第" << Count << "张票" << endl;
Count--;
this_thread::sleep_for(chrono::milliseconds(200));
}
}
int main()
{
thread t1(func, "窗口A");
thread t2(func, "窗口B");
thread t3(func, "窗口C");
t1.join();
t2.join();
t3.join();
return 0;
}
先不谈打印结果比较乱的情况,我们发现有时候会由多个窗口同时卖一张票,这显然是很危险的。我们来剖析一下为什么会发生这种情况。
这是因为几个线程需要同时分时复用CPU时间片,这是假如A线程执行过程期间就丢失了CPU时间片,线程A的数据没有同步到物理内存中,线程B运行时取到了就的数据,而线程A再次得到CPU时间片转换成运行状态时,第一件事就是更新上次没提交的数据,这就会导致线程B更新的数据被覆盖,此时就会乱套。
怎么解决这种现象,那就必须得提到线程同步。
同步方式
互斥锁
互斥锁的原理是当某个子线程执行一段代码块时,将其锁住,这样就避免了并行处理这是其他线程只有等待其执行完解锁后才能执行。
互斥锁的使用
将火车站售票程序修改
#include<iostream>
using namespace std;
#include<thread>
#include<mutex>
mutex mtx;
int Count = 100;
void func(string name)
{
while(Count>0)
{
if (Count > 0)
{
mtx.lock();
cout << name << "卖出了倒数第" << Count << "张票" << endl;
Count--;
mtx.unlock();
this_thread::sleep_for(chrono::milliseconds(200));
}
}
}
int main()
{
thread t1(func, "窗口A");
thread t2(func, "窗口B");
thread t3(func, "窗口C");
t1.join();
t2.join();
t3.join();
return 0;
}
此时
死锁问题
什么是死锁?
死锁就是两个或两个以上线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去
例如
#include<iostream>
#include<thread>
#include<mutex>
mutex mtx1;
mutex mtx2;
void funcB(string name);
void funcA(string name)
{
cout << name << "申请加锁A" << endl;
mtx1.lock();
cout << name << "申请加锁A成功" << endl;
//确保线程1申请锁A成功
this_thread::sleep_for(chrono::seconds(1));
funcB(name);
mtx1.unlock();
}
void funcB(string name)
{
cout << name << "申请加锁B" << endl;
mtx2.lock();
cout << name << "申请加锁B成功" << endl;
//确保线程2申请锁B成功
this_thread::sleep_for(chrono::seconds(1));
funcA(name);
mtx2.unlock();
}
int main()
{
thread t1(funcA, "线程1");
thread t2(funcB, "线程2");
t1.join();
t2.join();
return 0;
}
线程1获得锁A,线程2获得锁B,此时线程1想获取锁B,而此时线程2想获取锁A,此时都想获取对方的锁,此时程序进入僵死状态。
死锁产生的原因
死锁的产生需要满足以下 4 个条件:
1. 互斥条件:指运算单元(进程、线程或协程)对所分配到的资源具有排它性,也就是说在 一段时间内某个锁资源只能被一个运算单元所占用。
2. 请求和保持条件:指 运算单元已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它运算单元占有,此时请求运算单元阻塞,但又对自己已获得的其它资源保持不放。
3. 不可剥夺条件:指运算单元已获得的资源,在未使用完之前,不能被剥夺。
4. 环路等待条件:指在发生死锁时,必然存在运算单元和资源的环形链,即 运算单元正在等待另一个运算单元占用的资源,而对方又在等待自己占用的资源,从而造成环路等待的情况。
所以我们想要解决死锁问题只要破坏死锁产生原因的一个就行。
上述程序这么修改便可解决死锁问题
void funcA(string name)
{
cout << name << "申请加锁A" << endl;
mtx1.lock();
cout << name << "申请加锁A成功" << endl;
//确保线程1申请锁A成功
this_thread::sleep_for(chrono::seconds(1));
mtx1.unlock();
funcB(name);
}
void funcB(string name)
{
cout << name << "申请加锁B" << endl;
mtx2.lock();
cout << name << "申请加锁B成功" << endl;
//确保线程2申请锁B成功
this_thread::sleep_for(chrono::seconds(1));
mtx2.unlock();
funcA(name);
}
造成死锁的几个场景
加锁后忘记解锁
重复加锁
条件变量
条件变量是 C++11 提供的另外一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来使用,C++11 提供了两种条件变量:
condition_variable需要和unique_lock<mutex>配合使用进行wait操作
condition_variable_any可以和四种互斥锁进行配合
生产者消费者模型
这里实现一对一,缓存空间是一个循环队列,开始由生产者生产,当队列满时,由消费者消费完,然后再由生产者生产.......
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include<string>
using namespace std;
#define size 4 //缓存空间
#define NUMS 30
mutex mtx;
condition_variable cv; //条件变量
template<class T>
class Queue
{
int front = 0;
int rear = 0;
int count = 0;
T arr[size] = { 0 };
public:
Queue()
{
}
void Push(T value)
{
if (count == size)
{
cout << "队列满了" << endl;
return;
}
arr[rear] = value;
rear = (rear + 1) % size;
count++;
}
void Pop()
{
if (count == 0)
{
cout << "队列为空" << endl;
return;
}
count--;
front = (front + 1) % size;
}
bool Full()
{
return count == size;
}
bool Empty()
{
return count == 0;
}
T Front()
{
return arr[front];
}
~Queue()
{
}
};
Queue<int> m_q;
void producer()
{
for (int i = 1; i <= NUMS; ++i)
{
unique_lock<mutex> lock(mtx);
//如果容器不为空并且满了,就等待消费者消费完,再生产
while (!m_q.Empty() && m_q.Full())
{
cv.wait(lock);
}
m_q.Push(i);
cout << "生产了产品:" << i << "号" << endl;
cv.notify_all();
this_thread::sleep_for(chrono::milliseconds(10));
}
}
void consumer(string name)
{
for (int i = 1; i <= NUMS; ++i)
{
unique_lock<mutex> lock(mtx);
//如果容器为空,代表没有产品可以消费,等待生产者生产
while (m_q.Empty())
{
cv.wait(lock);
}
int value = m_q.Front();
m_q.Pop();
cout << name << "消费了产品:" << value << "号" << endl;
cv.notify_all();
this_thread::sleep_for(chrono::milliseconds(10));
}
}
int main()
{
thread t1(producer);
thread t2(consumer, "消费者A");
t1.join();
t2.join();
return 0;
}