C11并发与多线程笔记
C++11并发与多线程笔记
C++11并发与多线程笔记(5)互斥量概念、用法、死锁演示及解决详解
第五节 互斥量概念、用法、死锁演示及解决详解
一、互斥量(mutex)的基本概念
互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。
互斥量使用要小心,保护数据不多也不少,少了达不到效果,多了影响效率。
二、互斥量的用法
包含#include 头文件
2.1 lock(),unlock()
步骤:1.lock(),2.操作共享数据,3.unlock()。
lock()和unlock()要成对使用
2.2 lock_guard类模板
lock_guard sbguard(myMutex);取代lock()和unlock()
lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock()
三、死锁
3.1 死锁演示
死锁至少有两个互斥量mutex1,mutex2。
a.线程A执行时,这个线程先锁mutex1,并且锁成功了,然后去锁mutex2的时候,出现了上下文切换。
b.线程B执行,这个线程先锁mutex2,因为mutex2没有被锁,即mutex2可以被锁成功,然后线程B要去锁mutex1.
c.此时,死锁产生了,A锁着mutex1,需要锁mutex2,B锁着mutex2,需要锁mutex1,两个线程没办法继续运行下去。。。
3.2 死锁的一般解决方案:
只要保证多个互斥量上锁的顺序一样就不会造成死锁。
3.3 std::lock()函数模板
std::lock(mutex1,mutex2……); 一次锁定多个互斥量(一般这种情况很少),用于处理多个互斥量。
如果互斥量中一个没锁住,它就等着,等所有互斥量都锁住,才能继续执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)
3.4 std::lock_guard的std::adopt_lock参数
std::lock_guardstd::mutex my_guard(my_mutex,std::adopt_lock);
加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock();
adopt_guard为结构体对象,起一个标记作用,表示这个互斥量已经lock(),不需要在lock()。
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A{
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
//lock_guard<mutex> sbguard(myMutex1, adopt_lock);
lock(myMutex1, myMutex2);
//myMutex2.lock();
//myMutex1.lock();
msgRecvQueue.push_back(i);
myMutex1.unlock();
myMutex2.unlock();
}
}
}
bool outMsgLULProc()
{
myMutex1.lock();
myMutex2.lock();
if (!msgRecvQueue.empty())
{
cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素" << msgRecvQueue.front() << endl;
msgRecvQueue.pop_front();
myMutex2.unlock();
myMutex1.unlock();
return true;
}
myMutex2.unlock();
myMutex1.unlock();
return false;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
if (outMsgLULProc())
{
}
else
{
cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
}
}
}
private:
list<int> msgRecvQueue;
mutex myMutex1;
mutex myMutex2;
};
int main()
{
A myobja;
mutex myMutex;
thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutMsgObj.join();
myInMsgObj.join();
return 0;
}
C++11并发与多线程笔记(6) unique_lock(类模板)详解
第六节 unique_lock(类模板)详解
1.unique_lock取代lock_guard
unique_lock比lock_guard灵活很多(多出来很多用法),效率差一点。
unique_lock myUniLock(myMutex);
2.unique_lock的第二个参数
2.1 std::adopt_lock:
表示这个互斥量已经被lock(),即不需要在构造函数中lock这个互斥量了。
前提:必须提前lock
lock_guard中也可以用这个参数
2.2 std::try_to_lock:
尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里;
使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方
前提:不能提前lock();
owns_lock()方法判断是否拿到锁,如拿到返回true
2.3 std::defer_lock:
如果没有第二个参数就对mutex进行加锁,加上defer_lock是始化了一个没有加锁的mutex
不给它加锁的目的是以后可以调用unique_lock的一些方法
前提:不能提前lock
3.unique_lock的成员函数(前三个与std::defer_lock联合使用)
3.1 lock():加锁。
unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
不用自己unlock();
3.2 unlock():解锁。
unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
//处理一些共享代码
myUniLock.unlock();
//处理一些非共享代码
myUniLock.lock();
//处理一些共享代码
因为一些非共享代码要处理,可以暂时先unlock(),用其他线程把它们处理了,处理完后再lock()。
3.3 try_lock():尝试给互斥量加锁
如果拿不到锁,返回false,否则返回true。
3.4 release():
unique_lock
myUniLock(myMutex);相当于把myMutex和myUniLock绑定在了一起,release()就是解除绑定,返回它所管理的mutex对象的指针,并释放所有权
mutex* ptx =
myUniLock.release();所有权由ptx接管,如果原来mutex对象处理加锁状态,就需要ptx在以后进行解锁了。
lock的代码段越少,执行越快,整个程序的运行效率越高。
a.锁住的代码少,叫做粒度细,执行效率高;
b.锁住的代码多,叫做粒度粗,执行效率低;
4.unique_lock所有权的传递
unique_lock myUniLock(myMutex);把myMutex和myUniLock绑定在了一起,也就是myUniLock拥有myMutex的所有权
- 使用move转移
myUniLock拥有myMutex的所有权,myUniLock可以把自己对myMutex的所有权转移,但是不能复制。
unique_lock myUniLock2(std::move(myUniLock));
现在myUniLock2拥有myMutex的所有权。
- 在函数中return一个临时变量,即可以实现转移
unique_lock<mutex> aFunction()
{
unique_lock<mutex> myUniLock(myMutex);
//移动构造函数那里讲从函数返回一个局部的unique_lock对象是可以的
//返回这种局部对象会导致系统生成临时的unique_lock对象,并调用unique_lock的移动构造函数
return myUniLock;
}
// 然后就可以在外层调用,在sbguard具有对myMutex的所有权
std::unique_lock<std::mutex> sbguard = aFunction();
C++11并发与多线程笔记(7) 单例设计模式共享数据分析、解决,call_onc
第七节 单例设计模式共享数据分析、解决,call_once
1.设计模式
程序灵活,维护起来可能方便,用设计模式理念写出来的代码很晦涩,但是别人接管、阅读代码都会很痛苦
老外应付特别大的项目时,把项目的开发经验、模块划分经验,总结整理成设计模式
中国零几年设计模式刚开始火时,总喜欢拿一个设计模式往上套,导致一个小小的项目总要加几个设计模式,本末倒置
设计模式有其独特的优点,要活学活用,不要深陷其中,生搬硬套
2.单例设计模式:
整个项目中,有某个或者某些特殊的类,只能创建一个属于该类的对象。
单例类:只能生成一个对象。
3.单例设计模式共享数据分析、解决
面临问题:需要在自己创建的线程中来创建单例类的对象,这种线程可能不止一个。我们可能面临GetInstance()这种成员函数需要互斥。
可以在加锁前判断m_instance是否为空,否则每次调用Singleton::getInstance()都要加锁,十分影响效率。
#include <iostream>
#include <mutex>
using namespace std;
mutex myMutex;
//懒汉模式
class Singleton
{
public:
static Singleton * getInstance() {
//双重锁定 提高效率
if (instance == NULL) {
lock_guard<mutex> myLockGua(myMutex);
if (instance == NULL) {
instance = new Singleton;
}
}
return instance;
}
private:
Singleton() {}
static Singleton *instance;
};
Singleton * Singleton::instance = NULL;
//饿汉模式
class Singleton2 {
public:
static Singleton2* getInstance() {
return instance;
}
private:
Singleton2() {}
static Singleton2 * instance;
};
Singleton2 * Singleton2::instance = new Singleton2;
int main(void)
{
Singleton * singer = Singleton::getInstance();
Singleton * singer2 = Singleton::getInstance();
if (singer == singer2)
cout << "二者是同一个实例" << endl;
else
cout << "二者不是同一个实例" << endl;
cout << "---------- 以下 是 饿汉式 ------------" << endl;
Singleton2 * singer3 = Singleton2::getInstance();
Singleton2 * singer4 = Singleton2::getInstance();
if (singer3 == singer4)
cout << "二者是同一个实例" << endl;
else
cout << "二者不是同一个实例" << endl;
return 0;
}
如果觉得在单例模式new了一个对象,而没有自己delete掉,这样不合理。可以增加一个类中类CGarhuishou,new一个单例类时创建一个静态的CGarhuishou对象,这样在程序结束时会调用CGarhuishou的析构函数,释放掉new出来的单例对象。
class Singelton
{
public:
static Singleton * getInstance() {
if (instance == NULL) {
static CGarhuishou huishou;
instance = new Singelton;
}
return instance;
}
class CGarhuishou {
public:
~CGarhuishou()
{
if (Singleton::instance)
{
delete Singleton::instance;
Singleton::instance = NULL;
}
}
};
private:
Singleton() {}
static Singleton *instance;
};
Singleton * Singleton::instance = NULL;
4.std::call_once():
函数模板,该函数的第一个参数为标记,第二个参数是一个函数名(如a())。
功能:能够保证函数a()只被调用一次。具备互斥量的能力,而且比互斥量消耗的资源更少,更高效。
call_once()需要与一个标记结合使用,这个标记为std::once_flag;其实once_flag是一个结构,call_once()就是通过标记来决定函数是否执行,调用成功后,就把标记设置为一种已调用状态。
多个线程同时执行时,一个线程会等待另一个线程先执行。
once_flag g_flag;
class Singleton
{
public:
static void CreateInstance()//call_once保证其只被调用一次
{
instance = new Singleton;
}
//两个线程同时执行到这里,其中一个线程要等另外一个线程执行完毕
static Singleton * getInstance() {
call_once(g_flag, CreateInstance);
return instance;
}
private:
Singleton() {}
static Singleton *instance;
};
Singleton * Singleton::instance = NULL;
补充:单例模式
懒汉模式
mutex myMutex;
class Singleton{
public:
static Singleton * getInstance(){
if(instance == NULL){
lock_guard<mutex> myLockGua(myMutex);
if(instace ==NULL){
instance = new Singleton();
}
}
return instance;
}
private:
Singleton(){}
//将构造函数私有化是确保类不能被外部直接实例化的关键步骤。在单例模式中,我们希望类的实例只能通过类的静态方法(通常是getInstance方法)来获取,以确保全局只有一个实例。如果构造函数是公开的,那么外部代码就可以绕过getInstance方法直接创建新的实例,从而破坏单例模式的规则。
static Singleton * instance;
//在单例模式中,内部指针设置为静态有两个主要原因:跨多个对象实例共享:静态成员变量属于类本身,而不是类的任何特定实例。这意味着无论创建多少个Singleton类的引用(尽管实际上只有一个实例),它们都将共享同一个静态成员变量(即内部指针)。这保证了整个程序中只有一个实例被引用。确保全局唯一性:静态成员变量在程序的生命周期内只会被初始化一次,并且在程序的任何地方都是可访问的。通过将内部指针设置为静态,我们确保了无论在哪里调用getInstance方法,它都会返回同一个实例的引用。
};
Singleton* Singleton::instance = NULL;
改进方式
mutex myMutex;
//懒汉模式
class Singleton
{
public:
static Singleton * getInstance(){
//双重锁定 提高效率
if(instance == NULL){
lock_guard<mutex> myLockGua(myMutex);
if(instance == NULL){
static CGarhuishou huihsou;
instance = new Singleton();
}
}
return instance;
}
class CGarhuishou{
public:
~CGarhuishou(){
if(Singleton::instance){
delete Singleton::instance;
Singleton::instance = NULL;
}
}
};
private:
Singleton(){}
static Singleton* instance;
};
Singleton* Singleton::instance = NULL;
在您的
Singleton
类实现中,
static CGarhuishou huihsou;
这个成员被声明为静态(
static
)的原因是为了确保在程序的生命周期内,
这个局部对象(在
getInstance
函数内部)只被创建和销毁一次,从而能够在程序结束时正确地清理
Singleton
类的实例
。
在懒汉模式中,
实例的创建被延迟到实际需要时
。即,在第一次调用
getInstance
方法时才创建实例。这种模式可以减少不必要的资源消耗,
但如果多线程环境下没有正确同步,可能会导致多个实例被创建。在你的代码中,使用了双重检查锁定(Double-Checked Locking)模式来优化性能并确保线程安全。
通过将构造函数私有化,我们防止了外部代码直接实例化类;通过将内部指针设置为静态,我们确保了全局只有一个实例,并且这个实例可以在程序的任何地方被访问。
双重锁定
双重锁定(Double-Checked Locking)模式在单例模式的懒汉式实现中用于提高性能,同时确保线程安全。这种模式的核心思想是在访问单例实例之前进行两次检查,以减少对锁的依赖,从而提高效率。下面详细解释为什么这样设置:
问题背景
在单例模式的懒汉式实现中,如果不使用任何同步机制,那么在多线程环境下可能会创建多个实例,从而违反单例模式的原则。使用一个简单的锁(如
mutex
)可以解决这个问题,但每次调用
getInstance
方法时都进行加锁和解锁操作,会大大降低性能,尤其是在单例对象已经创建之后。
双重锁定的解决方案
双重锁定模式通过以下步骤来解决这个问题:
- 第一次检查(无锁) :在获取锁之前,首先检查实例是否已经被创建。如果实例已经存在,那么就没有必要再获取锁,直接返回实例即可。这一步可以显著提高性能,因为大多数情况下实例已经存在,不需要进行同步操作。
- 获取锁 :如果实例不存在,那么进入同步块,通过锁来保证只有一个线程能够进入这个块。这一步确保了多线程环境下的线程安全。
- 第二次检查(有锁) :在同步块内部,再次检查实例是否已经被创建。这是必要的,因为从第一次检查到获取锁之间可能存在时间差,在这段时间内可能有其他线程已经创建了实例。这一步避免了不必要的实例创建。
- 创建实例 :如果实例确实不存在,那么在这里创建实例。
- 释放锁 :一旦实例被创建,同步块就会结束,锁会被自动释放。
- 返回实例 :最后,返回单例实例。
优点
- 性能提升 :通过第一次无锁检查,避免了在实例已经存在时的同步开销。
- 线程安全 :通过锁和第二次检查,确保了多线程环境下只会创建一个实例。
饿汉模式
class Singleton2{
public:
static Singleton2* getInstance(){
return instance;
}
private:
Singleton2(){}
static Singleton2* instance;
};
Singleton2* Singleton2::instance = new Singleton2();
在饿汉模式中, 实例在类被加载时就立即创建。这保证了线程安全 ,因为无需在运行时进行同步,但可能会浪费资源,因为实例可能在程序运行期间从未被使用。
(8) condition_variable、wait、notify_one、notify_all
std::condition_variable实际上是一个类,是一个和条件相关的类,说白了就是等待一个条件达成。
std::mutex mymutex1;
std::unique_lock<std::mutex> sbguard1(mymutex1);
std::condition_variable condition;
condition.wait(sbguard1, [this] {if (!msgRecvQueue.empty())
return true;
return false;
});
condition.wait(sbguard1);
wait()用来等一个东西
如果第二个参数的lambda表达式返回值是 false , 那么wait()将解锁互斥量,并阻塞到本行
如果第二个参数的lambda表达式返回值是 true,那么wait()直接返回并继续执行。
阻塞到什么时候为止呢? 阻塞到其他某个线程调用notify_one()成员函数为止 ;
如果没有第二个参数,那 么效果跟第二个参数lambda表达式返回false效果一样
wait()将解锁互斥量,并阻塞到本行,阻塞到其他某个线程调用notify_one()成员函数为止。
当其他线程用notify_one()将本线程wait()唤醒后,这个wait恢复后
1、 wait()不断尝试获取互斥量锁,如果获取不到那么流程就卡在wait()这里等待获取,如果获取到了,那么wait()就继续执行,获取到了锁
2.1、如果wait有第二个参数就判断这个lambda表达式。
a) 如果表达式为false,那wait又对互斥量解锁,然后又休眠,等待再次被notify_one()唤醒
b)如 果lambda表达式为true,则wait返回,流程可以继续执行(此时互斥量已被锁住) 。
2.2、如果wait没有第二个参数,则wait返回,流程走下去。
流程只要走到了wait()下面则互斥量一定被锁住了。对。即使false也会阻塞,阻塞完之后就执行再次加锁互斥量
示例:
#include <thread>
#include <iostream>
#include <list>
#include <mutex>
#include <atomic>
#include <condition_variable>
using namespace std;
class A {
public:
void inMsgRecvQueue() {
for (int i = 0; i < 3; i++) {
cout << "inMagRecvQueue : 插入数字: " << i << endl;
{
std::lock_guard<std::mutex> guard(mymutex1);
msgRecvQueue.push_back(i);
}
condition.notify_one(); // 将数字插入队列后通知消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟工作
}
}
void outMsgRecvqueue() {
while (running) {
std::unique_lock<std::mutex> lock(mymutex1);
condition.wait(lock, [this] {
return !msgRecvQueue.empty() || !running;
});
if (!msgRecvQueue.empty()) {
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
cout << "outMsgRecvQueue(), 删除数字: " << command << endl;
}
}
}
void stop() {
running = false;
condition.notify_one(); // 通知消费者停止等待
}
private:
std::list<int> msgRecvQueue;
std::mutex mymutex1;
std::condition_variable condition;
std::atomic<bool> running{true};
};
int main() {
A myobj;
std::thread myoutobj(&A::outMsgRecvqueue, &myobj); // 消费者线程
std::thread myinobj(&A::inMsgRecvQueue, &myobj); // 生产者线程
myinobj.join();
myobj.stop();
myoutobj.join();
return 0;
}
在你的代码中,调用顺序
myinobj.join(); myobj.stop(); myoutobj.join();
是正确的,并且不会导致
myoutobj.join();
在处理完之前被
myobj.stop();
叫停。下面是详细的解释:
myinobj.join();
等待生产者线程完成:- 生产者线程
myinobj
会插入三个数字到msgRecvQueue
并通知消费者线程。 - 在调用
myinobj.join();
后,主线程会等待生产者线程完成所有插入操作。
- 生产者线程
myobj.stop();
停止标志:myobj.stop();
设置running
为false
并通知消费者线程停止等待。- 由于
myinobj
已经完成,生产者线程不再插入新的数字。
myoutobj.join();
等待消费者线程完成:在
stop()
调用后,condition.notify_one();
会唤醒消费者线程。如果队列中有数据,消费者线程会继续处理队列中的数据,直到队列为空。
一旦队列为空且
running
为false
,消费者线程将退出循环并结束。也就是说myobj.stop();调用的时候msgRecvQueu已经插入了最后一个数据
ChatGPT
是的,
myobj.stop();
调用的时候,生产者线程myinobj
已经插入了最后一个数据并完成了它的工作。以下是详细的执行流程:
- 生产者线程
myinobj
的执行:- 生产者线程开始运行,向
msgRecvQueue
插入数据,并在每次插入后调用condition.notify_one()
通知消费者线程。 - 生产者线程执行完所有插入操作(插入三个数字)后结束。
- 生产者线程开始运行,向
- 主线程等待生产者线程完成:
myinobj.join();
等待生产者线程完成。这确保在myobj.stop();
被调用之前,生产者线程已经插入了最后一个数据。
- 停止标志:
myobj.stop();
被调用,设置running
为false
并调用condition.notify_one()
通知消费者线程。
- 消费者线程
myoutobj
的执行:- 消费者线程在等待条件变量时会被唤醒。
- 消费者线程检查
msgRecvQueue
是否为空。如果不为空,则处理队列中的数据。 - 由于生产者线程已经完成了插入操作,消费者线程会处理所有插入的数据。
- 当队列为空且
running
为false
时,消费者线程退出循环并结束。
因此,在调用
myobj.stop();
时,msgRecvQueue
中已经插入了所有需要处理的数据。消费者线程myoutobj
会处理完队列中的所有数据后再结束,确保没有数据丢失或未处理。- 生产者线程
async、future、packaged_task、promise
本节内容需要包含头文件#include
一、std::async、std::future创建后台任务并返回值
std::async是一个函数模板,用来启动一个异步任务,启动起来一个异步任务之后,它返回一个std::future对象,这个对象是个类模板。
什么叫“启动一个异步任务”?就是自动创建一个线程,并开始 执行对应的线程入口函数,它返回一个std::future对象,这个std::future对象中就含有线程入口函数所返回的结果,我们可以通过调用future对象的成员函数get()来获取结果。
“future”将来的意思,也有人称呼std::future提供了一种访问异步操作结果的机制,就是说这个结果你可能没办法马上拿到,但是在不久的将来,这个线程执行完毕的时候,你就能够拿到结果了,所以,大家这么理解:future中保存着一个值,这个值是在将来的某个时刻能够拿到。
std::future对象的get()成员函数会等待线程执行结束并返回结果,拿不到结果它就会一直等待,感觉有点像join()。但是,它是可以获取结果的。
std::future对象的wait()成员函数,用于等待线程返回,本身并不返回结果,这个效果和 std::thread 的join()更像。
#include <iostream>
#include <future>
using namespace std;
class A{
public:
int mythread(int mypar){
cout<<mypar<<endl;
return mypar;
}
};
int mythread(){
cout<<"mythread() start"<<": thread_id = "<<std::this_thread::get_id();
std::chrono::milliseconds dura(5000);
std::this_thread::sleep_for(dura);
cout<<"mythread() end "<<"thread_id = "<<std::this_thread::get_id();
return 8;
}
int main(){
A a;
int tmp = 12;
cout<<"main "<<"thread_id = "<<std::this_thread::get_id()<<endl;
std::future<int> result = std::async(mythread);//创建了一个异步进程
cout<<"continue------"<<endl;
cout<<result.get()<<endl;
std::future<int> result2 = std::async(&A::mythread,&a,tmp);//创建了一个异步进程,和创建其他进程一样
cout << result2.get() << endl;
cout<<"good luck "<<endl;
return 0;
}
我们通过向std::async()传递一个参数,该参数是 std::launch类型(枚举类型) ,来达到一些特殊的目的:
1、std::lunch::deferred:
(defer推迟,延期)表示线程入口函数的调用会被延迟**,一直到std::future的wait()或者get()函数被调用时**(由主线程调用)才会执行;如果wait()或者get()没有被调用,则不会执行。
实际上根本就没有创建新线程。std::launch::deferred意思时延迟调用,并没有创建新线程,是在主线程中调用的线程入口函数。
2、std::launch::async,
在调用async函数的时候就开始创建新线程。但是只有在调用future和get的时候才会执行线程
二、std::packaged_task:打包任务,把任务包装起来。
类模板,它的模板参数是各种可调用对象,通过packaged_task把各种可调用对象包装起来,方便将来作为线程入口函数来调用。
#include <iostream>
#include <future>
#include <thread>
using namespace std;
int mythread(int mypar){
cout<<mypar<<endl;
cout<<"mythread() start"<<": thread_id = "<<std::this_thread::get_id();
std::chrono::milliseconds dura(5000);
std::this_thread::sleep_for(dura);
cout<<"mythread() end "<<"thread_id = "<<std::this_thread::get_id();
return 5;
}
int main(){
cout<<"main "<<"thread_id = "<<std::this_thread::get_id()<<endl;
std::packaged_task<int(int)> mypt(mythread);
std::future<int> result = mypt.get_future(); // 在创建线程之前调用 get_future()
std::thread t1(std::move(mypt),1);// 注意这里使用 std::move,因为 mypt 的所有权将被转移到线程中
t1.join();
cout<<result.get()<<endl;// 现在可以安全地获取结果
return 0;
}
因为
std::packaged_task
不可复制但可移动
。当你将
std::packaged_task
传递给
std::thread
时,你应该使用
std::move
来转移其所有权。
然而,你的代码中有一个逻辑错误。你尝试在
t1.join();
之后调用
mypt.get_future().get();
来获取结果,但这可能不会按你期望的方式工作。原因是
std::packaged_task
的
get_future()
方法必须在任何线程开始执行之前被调用,以便正确设置
std::future
对象来接收结果。在你的代码中,
get_future()
是在
t1.join();
之后调用的,此时可能已经太晚了,因为
mythread
可能已经完成了执行,并且
std::packaged_task
可能已经处于无效状态。
为了修复这个问题,你应该在创建
std::thread
之前调用
get_future()
,并保存返回的
std::future
对象,以便稍后在主线程中检索结果。
lambda表达式
#include <iostream>
#include <future>
#include <thread>
using namespace std;
int main(){
cout<<"main "<<"thread_id = "<<std::this_thread::get_id()<<endl;
std::packaged_task<int(int)> mypt([](int mypar){
cout<<mypar<<endl;
cout<<"mythread() start"<<": thread_id = "<<std::this_thread::get_id();
std::chrono::milliseconds dura(3000);
std::this_thread::sleep_for(dura);
cout<<"mythread() end "<<"thread_id = "<<std::this_thread::get_id();
return 15;
});
std::future<int> result = mypt.get_future();
std::thread t1(std::move(mypt),1);
t1.join();
cout<<result.get()<<endl;
return 0;
}
#include <iostream>
#include <future>
#include <thread>
using namespace std;
int main(){
cout<<"main "<<"thread_id = "<<std::this_thread::get_id()<<endl;
std::packaged_task<int(int)> mypt([](int mypar){
cout<<mypar<<endl;
cout<<"mythread() start"<<": thread_id = "<<std::this_thread::get_id();
std::chrono::milliseconds dura(3000);
std::this_thread::sleep_for(dura);
cout<<"mythread() end "<<"thread_id = "<<std::this_thread::get_id();
return 15;
});
mypt(1);
std::future<int> result = mypt.get_future();
cout<<result.get()<<endl;
return 0;
}
在这个例子中, 没有创建新线程 来执行 lambda 表达式。lambda 表达式是在主线程中同步执行的。
#include <iostream>
#include <future>
#include <thread>
#include <vector>
using namespace std;
void mythread(std::promise<int> &tmp,int clac){
cout<<"mythread() start"<<": thread_id = "<<std::this_thread::get_id();
std::chrono::milliseconds dura(3000);
std::this_thread::sleep_for(dura);
cout<<"mythread() end "<<"thread_id = "<<std::this_thread::get_id();
int result = clac;
tmp.set_value(result);
return;
}
int main(){
std::promise<int> myprom;
std::future<int> fu1 = myprom.get_future();
std::thread t1(mythread,std::ref(myprom),100);
t1.join();
auto result = fu1.get();
cout<<"result = "<<result<<endl;
return 0;
}
考虑使用ref还是move
使用 std::ref
std::ref
的作用是创建一个
std::reference_wrapper
,它可以安全地将对象作为引用传递给函数。这是必要的,因为标准线程库中的线程构造函数会对传递的参数进行复制,而你希望将
myprom
作为引用传递,以便在
mythread
中能够修改
myprom
。
如果不使用
std::ref
,而是直接传递
myprom
,那么会发生参数复制,
mythread
中接收到的将是
myprom
的一个拷贝,而不是引用。这样的话,在
mythread
中对
tmp
的修改将不会影响到
main
函数中的
myprom
。
使用 std::move
std::move
用于将对象的所有权转移,而不是创建引用。如果使用
std::move(myprom)
来传递
myprom
,那么
myprom
将被移动,而在
main
函数中将不再能访问
myprom
。此外,
std::move
并不适用于传递引用,因为它会导致
myprom
被转移而非引用传递。
总结
在你的代码中,使用
std::ref
是必要的,因为你需要在新线程中引用并修改
myprom
,而不是创建它的一个拷贝或将其所有权转移。因此,
std::ref
可以确保
myprom
作为引用被传递,而
mythread
中的修改能够影响到
main
函数中的
myprom
。
(10) future其他成员函数、shared_future、atomic
#include <iostream>
#include <future>
#include <thread>
using namespace std;
int mythread(){
cout<<"mythread() start"<<": thread_id = "<<std::this_thread::get_id();
std::chrono::milliseconds dura(3000);
std::this_thread::sleep_for(dura);
cout<<"mythread() end "<<"thread_id = "<<std::this_thread::get_id();
return 5;
}
int main(){
cout<<"main "<<"thread_id = "<<std::this_thread::get_id()<<endl;
std::future<int> result = std::async(mythread);
cout<<"continue......."<<endl;
//cout<<result.get()<<endl;
std::future_status status = result.wait_for(std::chrono::seconds(1));
if(status == std::future_status::timeout){
cout<<"超时了,线程还没有执行完"<<endl;
}
return 0;
}
代码使用了
std::async
来启动一个异步任务,并返回一个
std::future
对象。你正确地使用了
std::future
对象来获取异步任务的结果。但是在你打印结果之前已经调用了
result.get()
,这会阻塞主线程,直到异步任务完成。因此,检查状态
std::future_status
之后的代码不会执行,因为异步任务已经完成。
#include <iostream>
#include <future>
#include <thread>
using namespace std;
int mythread(){
cout<<"mythread() start"<<": thread_id = "<<std::this_thread::get_id();
std::chrono::milliseconds dura(2000);
std::this_thread::sleep_for(dura);
cout<<"mythread() end "<<"thread_id = "<<std::this_thread::get_id();
return 5;
}
int main(){
cout<<"main "<<"thread_id = "<<std::this_thread::get_id()<<endl;
std::future<int> result = std::async(mythread);
//std::future<int> result = std::async(mythread);
cout<<"continue......."<<endl;
//cout<<result.get()<<endl;
std::future_status status = result.wait_for(std::chrono::seconds(1));
if(status == std::future_status::timeout){
cout<<"超时了,线程还没有执行完"<<endl;
}else if(status == std::future_status::ready){
cout<<"线程执行成功,返回 "<<endl;
cout<<result.get()<<endl;
}else if(status == std::future_status::deferred){
cout<<"线程延迟执行"<<endl;
cout<<result.get()<<endl;
}
return 0;
}
get()只能使用一次,因为get()函数的设计是一个移动语义,相当于将result中的值移动到了a中,再次get就报告了异常。
三、std::atomic原子操作
3.1 原子操作概念引出范例:
互斥量:多线程编程中 用于保护共享数据:先锁住, 操作共享数据, 解锁。
有两个线程,对一个变量进行操作,一个线程读这个变量的值,一个线程往这个变量中写值。
即使是一个简单变量的读取和写入操作,如果不加锁,也有可能会导致读写值混乱(一条C语句会被拆成3、4条汇编语句来执行,所以仍然有可能混乱)
#include <iostream>
#include <thread>
using namespace std;
int g_count = 0;
void mythread(){
for(int i=0;i<1000000;i++){
g_count++;
}
}
int main(){
std::thread t1(mythread);
std::thread t2(mythread);
t1.join();
t2.join();
cout<<" 实际是: "<<g_count<<endl;
return 0;
}
使用mutex解决这个问题
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int g_count = 0;
std::mutex mymutex;
void mythread1() {
for (int i = 0; i < 1000000; i++) {
std::unique_lock<std::mutex> u1(mymutex);
g_count++;
}
}
int main() {
std::thread t1(mythread1);
std::thread t2(mythread1);
t1.join();
t2.join();
cout << "正常情况下结果应该是200 0000次,实际是" << g_count << endl;
}
3.2 基本的std::atomic用法范例
大家可以把原子操作理解成一种:不需要用到互斥量加锁(无锁)技术的多线程并发编程方式。
原子操作:在多线程中不会被打断的程序执行片段。
从效率上来说,原子操作要比互斥量的方式效率要高。
互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量。
原子操作,一般都是指“不可分割的操作”;也就是说这种操作状态要么是完成的,要么是没完成的,不可能出现半完成状态。
std::atomic来代表原子操作,是个类模板。其实std::atomic是用来封装某个类型的值的
需要添加#include 头文件
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
std::atomic<int> g_count(0);//封装了一个为int的对象,只能用()不能赋值
void mythread(){
for(int i=0;i<1000000;i++){
g_count++;
}
}
int main(){
std::thread t1(mythread);
std::thread t2(mythread);
t1.join();
t2.join();
cout<<" 实际是: "<<g_count<<endl;
return 0;
}
总结:
1、原子操作一般用于计数或者统计(如累计发送多少个数据包,累计接收到了多少个数据包),多个线程一起统计,这种情况如果不使用原子操作会导致统计发生混乱。
2、写商业代码时,如果不确定结果的影响,最好自己先写一小段代码调试。或者不要使用。
11 std::atomic续谈、std::async深入谈
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
std::atomic<int> g_count(0);
void mythread(){
for(int i =0;i<10000;i++){
g_count = g_count +1;
//虽然g_count使用了原子操作模板,但是这种写法既读又写,
//会导致计数错误
//一般atomic原子操作,针对++,--,+=,-=,&=,|=,^=是支持的,其他操作不一定支持。
}
}
int main(){
std::thread t1(mythread);
std::thread t2(mythread);
t1.join();
t2.join();
cout<<"结果是: "<<g_count<<endl;
return 0;
}
二、std::async深入理解
2.1 std::async参数详述,async 用来创建一个异步任务
延迟调用参数 std::launch::deferred【延迟调用】,std::launch::async【强制创建一个线程】
std::async()我们一般不叫创建线程(他能够创建线程),我们一般叫它创建一个异步任务。
std::async和std::thread最明显的不同,就是 async 有时候并不创建新线程。
① 如果用std::launch::deferred 来调用async?
延迟到调用 get() 或者 wait() 时执行,如果不调用就不会执行
② 如果用std::launch::async来调用async?
强制这个异步任务在新线程上执行,这意味着,系统必须要创建出新线程来运行入口函数。
③ 如果同时用 std::launch::async | std::launch::deferred
这里这个 | 意味着async的行为可能是 std::launch::async 创建新线程立即执行, 也可能是 std::launch::deferred 没有创建新线程并且延迟到调用get()执行,由系统根据实际情况来决定采取哪种方案
④**不带额外参数 std::async(mythread),**只给async 一个入口函数名,此时的系统给的默认值是 std::launch::async | std::launch::deferred 和 ③ 一样,有系统自行决定异步还是同步运行。
2.2 std::async和std::thread()区别:
std::thread()如果系统资源紧张可能出现创建线程失败的情况,如果创建线程失败那么程序就可能崩溃,而且不容易拿到函数返回值(不是拿不到)
std::async()创建异步任务。可能创建线程也可能不创建线程,并且容易拿到线程入口函数的返回值;
由于系统资源限制:
①如果用std::thread创建的线程太多,则可能创建失败,系统报告异常,崩溃。
②如果用std::async,一般就不会报异常,因为如果系统资源紧张,无法创建新线程的时候,async不加额外参数的调用方式就不会创建新线程。而是在后续调用get()请求结果时执行在这个调用get()的线程上。
如果你强制async一定要创建新线程就要使用 std::launch::async 标记。承受的代价是,系统资源紧张时可能崩溃。
③根据经验,一个程序中线程数量 不宜超过100~200 。
12windows临界区、其他各种mutex互斥量
一和二、windows临界区
Windows临界区,同一个线程是可以重复进入的,但是进入的次数与离开的次数必须相等。
C++互斥量则不允许同一个线程重复加锁。
windows临界区是在windows编程中的内容,了解一下即可,效果几乎可以等同于c++11的mutex
包含#include <windows.h>
windows中的临界区同mutex一样,可以保护一个代码段。但windows的临界区可以进入多次,离开多次,但是进入的次数与离开的次数必须相等,不会引起程序报异常出错。
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
#include <windows.h>
#define _WINDOWSJQ_
using namespace std;
class A{
public:
void inMsgRecvQueue(){
for(size_t i = 0;i<3;i++){
cout<<"收到消息,并放入队列"<<i<<endl;
#ifdef _WINDOWSJQ_
EnterCriticalSection(&my_winsec);//进入临界区
EnterCriticalSection(&my_winsec);//再次进入临界区,程序不会出错
msgRecvQueue.push_back(i);
LeaveCriticalSection(&my_winsec);//离开临界区
LeaveCriticalSection(&my_winsec);//再次离开临界区
#elif
my_mutex.lock();
msgRecvQueue.push_back(i);
my_mutex.unlock();
#endif
}
cout<<"消息入队结束"<<endl;
}
//从队列中取出消息
void outMsgRecvQueue(){
for(size_t i=0;i<10;i++){
#ifdef _WINDOWSJQ_
EnterCriticalSection(&my_winsec);
if(!msgRecvQueue.empty()){
//队列不为空
int num = msgRecvQueue.front();
cout<<"从消息队列中取出"<<num<<endl;
msgRecvQueue.pop_front();
}else{
cout<<"消息队列为空"<<endl;
}
LeaveCriticalSection(&my_winsec);
#elif
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();
}
#endif
}
cout<<"消息出队结束"<<endl;
}
A(){
#ifdef _WINDOWSJQ_
InitializeCriticalSection(&my_winsec);//用临界区之前要初始化
#endif
}
private:
list<int> msgRecvQueue;
mutex my_mutex;
#ifdef _WINDOWSJQ_
CRITICAL_SECTION my_winsec; //windows中的临界区,类似mutex
#endif
};
int main(){
A myobj;
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobj);
std::thread myOutMsgObj(&A::outMsgRecvQueue,&myobj);
myInMsgObj.join();
myOutMsgObj.join();
//getchar();
return 0;
}
三、自动析构技术
C++:lock_guard防止忘了释放信号量,自动释放
windows:可以写个类自动释放临界区:
class CWinLock {
public:
CWinLock(CRITICAL_SECTION *pCritmp)
{
my_winsec =pCritmp;
EnterCriticalSection(my_winsec);
}
~CWinLock()
{
LeaveCriticalSection(my_winsec)
};
private:
CRITICAL_SECTION *my_winsec;
};
上述这种类RAII类(Resource Acquisition is initialization),即资源获取及初始化。容器,智能指针属于这种类。
四、递归独占互斥量 std::recursive_mutex
std::mutex 独占式互斥量
std::recursive_mutex: 允许在同一个线程中同一个互斥量多次被 lock() ,(但是递归加锁的次数是有限制的,太多可能会报异常),效率要比mutex低。
如果你真的用了 recursive_mutex 要考虑代码是否有优化空间,如果能调用一次 lock()就不要调用多次。
五、带超时的互斥量 std::timed_mutex 和 std::recursive_timed_mutex
5.1 std::timed_mutex:是待超时的独占互斥量
- try_lock_for():
等待一段时间,如果拿到了锁,或者超时了未拿到锁,就继续执行(有选择执行)如下:
std::chrono::milliseconds timeout(100);
if (my_mymutex.try_lock_for(timeout)){
//......拿到锁返回ture
}
else{
std::chrono::milliseconds sleeptime(100);
std::this_thread::sleep_for(sleeptime);
}
- try_lock_until():
参数是一个未来的时间点,在这个未来的时间没到的时间内,如果拿到了锁头,流程就走下来,如果时间到了没拿到锁,流程也可以走下来。
std::chrono::milliseconds timeout(100);
if (my_mymutex.try_lock_until(chrono::steady_clock::now() + timeout)){
//......拿到锁返回ture
}
else{
std::chrono::milliseconds sleeptime(100);
std::this_thread::sleep_for(sleeptime);
}
两者的区别就是一个参数是时间段,一个参数是时间点
5.2 std::recursive_timed_mutex:是待超时的递归独占互斥量
(13) 补充知识、线程池浅谈、数量谈、总结
一、补充一些知识点
1.1 虚假唤醒:
notify_one或者notify_all唤醒wait()后,实际有些线程可能不满足唤醒的条件,就会造成虚假唤醒,可以在wait中再次进行判断解决虚假唤醒。
解决:wait中要有第二个参数(lambda),并且这个lambda中要正确判断所处理的公共数据是否存在。
2.2 atomic:
std::atomic<int> atm = 0;
cout << atm << endl;
这里只有读取atm是原子操作,但是整个这一行代码 cout « atm « endl; 并不是原子操作,导致最终显示在屏幕上的值是一个“曾经值”。
std::atomic<int> atm = 0;
auto atm2 = atm; //不可以
原子操作实质上是: 不允许在进行原子对象操作时进行CPU的上下文切换。
二、浅谈线程池:
场景设想:服务器程序, 每来一个客户端,就创建一个新线程为这个客户提供服务。
问题:
1、2万个玩家,不可能给每个玩家创建一个新线程,此程序写法在这种场景下不通。
2、程序稳定性问题:编写代码中,“时不时地突然”创建一个线程,这种写法,一般情况下不会出错,但是不稳定的;
线程池:把一堆线程弄到一起,统一管理。这种统一管理调度,循环利用的方式,就叫做线程池。
实现方式:程序启动时,一次性创建好一定数量的线程。这种方式让人更放心,觉得程序代码更稳定。
三、线程创建数量谈:
1、线程创建的数量极限的问题
一般来讲,2000个线程基本就是极限;再创建就会崩溃。
2、线程创建数量建议
a、采用某些计数开发程序提供的建议,遵照建议和指示来确保程序高效执行。
b、创建多线程完成业务;考虑可能被阻塞的线程数量,创建多余最大被阻塞线程数量的线程,如100个线程被阻塞再充值业务,开110个线程就是很合适的
c、线程创建数量尽量不要超过500个,尽量控制在200个之内;