多线程的同步和异步
文章目录
- 多线程的同步和异步
- 一 同步和异步概念
- 二 多个线程建立安全数据共享
- 三 互斥量
- 3.1 互斥量用法
- 3.2 std::lock_guard
- 四 死锁
- 4.1 死锁解决方法
- 4.2 std::adopt_lock
一 同步和异步概念
异步是当一个调用或请求发送被调用者,调用者不用等待其结果的返回而继续当前的处理。实现异步机制的方式有多线程、中断和消息等。
线程同步就是让多个线程正确且有序的共享数据,以一致的顺序执行一组操作。
示例:创建多个线程
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
// 线程入口函数
void myprint(int num)
{
cout << this_thread::get_id() << "thread start , num is " << num << endl;
// do others ...
cout << this_thread::get_id() << "thread end " << endl;
return;
}
int main()
{
// 创建并等待多个线程
vector <thread> mythreads;
for (size_t i = 0; i < 10; ++i)
{
mythreads.push_back(thread(myprint,i)); // 创建10个线程,同时,这10个线程已经开始执行
}
for (auto iter = mythreads.begin(); iter != mythreads.end(); ++iter)
{
iter->join(); // 等待10个线程都返回
}
cout << "I love China! " << endl;
return 0;
}
1 多个线程,每个线程执行顺序是乱的。先创建的线程不一定比后创建的线程执行快。这与操作系统中线程调度机制有关。
2 把thread对象放到一个容器中,方便对大量线程进行管理。
二 多个线程建立安全数据共享
如果是只读数据,多线程同时进行访问不需要特别的处理,直接读取,安全稳定。
如果是读写数据,读与写时应该分别加锁,为防止读与写同时进行。
示例:模仿消息队列读写命令
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A
{
public:
// 把收到的消息传入队列
void inMsgRecvQueue()
{
for (size_t i = 0; i < 1000; ++i)
{
cout << "收到消息,并放入队列 " << i << endl;
my_mutex.lock();
msgRecvQueue.push_back(i);
my_mutex.unlock();
}
cout << "消息入队结束" << endl;
}
// 从队列中取出消息
void outMsgRecvQueue()
{
for (size_t i = 0; i < 1000; ++i)
{
my_mutex.lock();
if (!msgRecvQueue.empty())
{
// 队列不为空
int num = msgRecvQueue.front();
cout << "从消息队列中取出 " << num << endl;
msgRecvQueue.pop_front();
my_mutex.unlock();
}
else
{
// 消息队列为空
cout << "消息队列为空 " << endl;
my_mutex.unlock();
}
}
cout << "消息出队结束" << endl;
}
private:
list<int> msgRecvQueue; // 容器,存放消息。 list容器频繁的插入和删除数据时效率较高,vector容器随机的插入与删除时效率较高。
mutex my_mutex; // 创建一个互斥量
};
int main()
{
A myobj;
thread myInMsgObj(&A::inMsgRecvQueue, &myobj); // 第二个参数是引用,才能保障线程中用的是同一对象
thread myOutMsgObj(&A::outMsgRecvQueue, &myobj);
myInMsgObj.join();
myOutMsgObj.join();
return 0;
}
三 互斥量
在了解互斥锁之前,需要了解一下临界资源与临界区的概念:
所谓临界资源,是一次仅一个线程使用的共享资源。对于临界资源,各线程应该互斥地对其访问。每个线程中访问临界资源的那段代码称为临界区。任何时候,处于临界区内的线程不可多于一个。若已有线程进入自己的临界区,则其他所有试图进入临界区的进程必须等待。进入临界区的线程要在有限时间内退出,以便其他线程能及时进入自己的临界区。如果进程不能进入自己的临界区,则应该让出CPU(阻塞),避免出现进程“忙等”的情况。
互斥量是一个类对象,理解成一把锁。多个线程尝试用lock()函数对这把锁加锁,只有一个线程能加锁成功。如果没有加锁成功,那么当前线程卡在lock()这,并不断尝试去加锁。它的功能就是,同一时刻,只允许一个线程对临界区进行访问。
3.1 互斥量用法
步骤:先lock(),再操作共享数据,unlock(),最后不用互斥锁的时候再销毁它。
note:
1 lock()与unlock()成对使用,大多数人会忘记unlock()
2 互斥量锁住的内容不能多,也不能少。 多则影响程序效率,少则不安全
3.2 std::lock_guard
std::lock_guard是一个类模板,它可以传入mutex对象。在构造函数中调用mutex.lock(),析构函数中调用mutex.unlock()。它可以用来防止忘记对mutex.unlock()的问题。通过 {} 作用域运算符来控制std::lock_guard的生命周期,从而控制加锁解锁。
示例:
void inMsgRecvQueue()
{
for (size_t i = 0; i < 1000; ++i)
{
cout << "收到消息,并放入队列 " << i << endl;
std::lock_guard<mutex> my_guard(my_mutex);
msgRecvQueue.push_back(i);
}
cout << "消息入队结束" << endl;
}
// 从队列中取出消息
void outMsgRecvQueue()
{
for (size_t i = 0; i < 1000; ++i)
{
std::lock_guard<mutex> my_guard(my_mutex);
if (!msgRecvQueue.empty())
{
// 队列不为空
int num = msgRecvQueue.front();
cout << "从消息队列中取出 " << num << endl;
msgRecvQueue.pop_front();
}
else
{
// 消息队列为空
cout << "消息队列为空 " << endl;
}
}
cout << "消息出队结束" << endl;
}
四 死锁
线程相互等待,形成死锁。也就是说,多个互斥量,在不同线程中,加锁的顺序不一致,导致进程卡死。如果在同一线程中对mutex加锁两次(可以用std::recursive_mutex代替),那也会造成死锁。
示例:
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A
{
public:
// 把收到的消息传入队列
void inMsgRecvQueue()
{
for (size_t i = 0; i < 1000; ++i)
{
cout << "收到消息,并放入队列 " << i << endl;
my_mutex1.lock();
my_mutex2.lock();
msgRecvQueue.push_back(i);
my_mutex1.unlock();
my_mutex2.unlock();
}
cout << "消息入队结束" << endl;
}
// 从队列中取出消息
void outMsgRecvQueue()
{
for (size_t i = 0; i < 1000; ++i)
{
my_mutex2.lock(); // 与入队列的线程加锁顺序相反
my_mutex1.lock();
if (!msgRecvQueue.empty())
{
// 队列不为空
int num = msgRecvQueue.front();
cout << "从消息队列中取出 " << num << endl;
msgRecvQueue.pop_front();
my_mutex1.unlock(); // 解锁顺序没有要求
my_mutex2.unlock();
}
else
{
// 消息队列为空
cout << "消息队列为空 " << endl;
my_mutex1.unlock();
my_mutex2.unlock();
}
}
cout << "消息出队结束" << endl;
}
private:
list<int> msgRecvQueue; // 容器,存放消息。 list容器频繁的插入和删除数据时效率较高,vector容器随机的插入与删除时效率较高。
mutex my_mutex1,my_mutex2; // 死锁至少需要两个互斥量
};
int main()
{
A myobj;
thread myInMsgObj(&A::inMsgRecvQueue, &myobj); // 第二个参数是引用,才能保障线程中用的是同一对象
thread myOutMsgObj(&A::outMsgRecvQueue, &myobj);
myInMsgObj.join();
myOutMsgObj.join();
return 0;
}
4.1 死锁解决方法
在多个线程中,确保对每个互斥量加锁的顺序是一致的,就可以避免死锁。
std::lock()函数模板,作用是一次锁住两个或者以上的互斥量。在加锁时,先加锁第一个互斥量,再加锁第二个,第三个… 如果加锁其中一个失败,则所有的互斥量都不加锁。但是,解锁过程必须自己添加。
示例:
// 把收到的消息传入队列
void inMsgRecvQueue()
{
for (size_t i = 0; i < 1000; ++i)
{
cout << "收到消息,并放入队列 " << i << endl;
std::lock(my_mutex1, my_mutex2); // 同时加锁两个互斥量,若加锁其中一个失败,则都不加锁
msgRecvQueue.push_back(i);
my_mutex1.unlock(); // 解锁不能少
my_mutex2.unlock();
}
cout << "消息入队结束" << endl;
}
// 从队列中取出消息
void outMsgRecvQueue()
{
for (size_t i = 0; i < 1000; ++i)
{
std::lock(my_mutex1, my_mutex2); // 同时加锁两个互斥量,若加锁其中一个失败,则都不加锁
if (!msgRecvQueue.empty())
{
// 队列不为空
int num = msgRecvQueue.front();
cout << "从消息队列中取出 " << num << endl;
msgRecvQueue.pop_front();
my_mutex1.unlock();
my_mutex2.unlock();
}
else
{
// 消息队列为空
cout << "消息队列为空 " << endl;
my_mutex1.unlock();
my_mutex2.unlock();
}
}
cout << "消息出队结束" << endl;
}
4.2 std::adopt_lock
如果解锁过程也想让它管理,可以使用std::adopt_lock;
std::adopt_lock 是个结构体对象,相当于一个标志。作用:告诉std::lock_guard构造时,不要对mutex进行lock(),但是析构时,还是会执行unlock();
// 把收到的消息传入队列
void inMsgRecvQueue()
{
for (size_t i = 0; i < 1000; ++i)
{
cout << "收到消息,并放入队列 " << i << endl;
std::lock(my_mutex1, my_mutex2);
lock_guard<mutex> my_guard1(my_mutex1, std::adopt_lock); // 将my_mutex1,my_mutex2托管给lock_guard,自动解锁
lock_guard<mutex> my_guard2(my_mutex2, std::adopt_lock);
msgRecvQueue.push_back(i);
}
cout << "消息入队结束" << endl;
}
// 从队列中取出消息
void outMsgRecvQueue()
{
for (size_t i = 0; i < 1000; ++i)
{
std::lock(my_mutex1, my_mutex2);
lock_guard<mutex> my_guard1(my_mutex1, std::adopt_lock);
lock_guard<mutex> my_guard2(my_mutex2, std::adopt_lock);
if (!msgRecvQueue.empty())
{
// 队列不为空
int num = msgRecvQueue.front();
cout << "从消息队列中取出 " << num << endl;
msgRecvQueue.pop_front();
}
else
{
// 消息队列为空
cout << "消息队列为空 " << endl;
}
}
cout << "消息出队结束" << endl;
}