c++多线程记录

多线程笔记

本记录基于b站多线程讲解视频

笔记pdf版链接 提取码:1234
md文档版链接 提取码:1234

正文如下:
vs快捷键:

​ 执行快捷键:ctrl+F5 。

​ 加断点:F9 => 执行: F5 => 下一步:F10

第一节 并发基本概念及实现、进程、线程基本概念

(1)并发、进程、线程基本概念和综述

并发

两个及以上的任务(独立活动)同时进行(发生);(一个程序同时执行多个独立任务)

单核cpu,某时刻只可以执行一个任务,由操作系统调度,每秒进行多次“任务切换”,形成多任务同时进行(并发)的错觉。(又称为上下文切换,需要保存任务的上下文,有时间开销)

多核cpu,能够实现真正的并发(目的是为了提高性能)。

可执行程序

磁盘上的一个文件(Windows:.exe文件;linux(x执行权限))

进程

执行可执行程序的方法(windows:双击;linux:./文件名)

进程:可执行程序执行起来了,就是创建了一个进程。(就是运行起来的可执行程序)

线程

用来执行代码的。(代码的执行道路,一个进程可以有多个线程)

a)每个进程(运行起来的可执行程序),有且仅有一个主线程(0号线程)。

b) 当运行程序后,产生进程,与此同时,主线程也启动了。

c)执行程序,实际上是主线程来执行(调用)这个main函数中的代码。

d)每个线程都需要一个单独的堆栈空间。(线程并不是越多越好,线程间切换需要时间)

多线程程序可以同时干很多事儿,效率高。

2)并发的实现方法

并发:两个及以上的任务(独立活动)同时进行(发生)

实现并发的手段:

​ a)多个进程实现并发。

​ b)单独进程中,创建多个线程实现并发。(写代码实现)

多进程并发

wps启动后一个进程,ie浏览器一个进程,多个进程并发。

进程(内存块是私有的)之间的通信:(同一个电脑上通过:管道、文件、消息队列、共享内存)(不同电脑之间通过socket通信技术)实现;

多线程并发

单个进程中,创建多个线程(轻量级进程),可以共享相同的内存空间。

共享内存带来的问题:数据一致性问题。

(3)c++新标准线程库

c++11新标准开始,代码可移植。

第二节 线程启动、结束,创建线程多法、join,detach

(1)范例演示线程运行的开始和结束

thread(创建)

[ thread {子线程名}(线程调用函数名,参数1,参数……); ]

标准库类,创建线程,线程入口是xxx函数

join(阻塞主线程)

[ {子线程名}.join( ); ]

join是要阻塞主线程等子线程结束的,也就是说子线程结束前主线程什么事情也不能干

void myprint()
{
	cout << "线程开始执行" << endl;
	cout << "线程执行结束" << endl;
}

int main()//主线程
{
	//可调用对象:myprint(创建线程,线程入口是myprint函数)
	thread mytobj(myprint);//子线程,一条线被阻塞,另一条线也不会被阻塞,独立的
	//加入或汇合,说白了就是阻塞,(阻塞主线程),让主线程等待子线程执行完毕,然后子线程和主线程汇合
	mytobj.join();//主线程阻塞,等待myprint执行完毕
	cout << "I love China!" << endl;
	return 0;
} 
detach(不阻塞主线程)

[ {子线程名}.detach( ); ]

主线程不与子线程汇合,彼此独立,你干你的,我干我的,主线程也可提前执行完毕,主线程结束后,子线程剩余部分在系统后台运行,执行完成后,c++运行时库负责清理相关资源。(守护线程)

mytobj.detach();
joinable(判断)

[ {子线程名}.joinable( ); ]

返回值:True / Flase

判断是否可以成功使用join()或者detach()的;

if(mytobj.joinable())
{
    cout << "可用join()" << endl;
    mytobj.join();
}
else
{
    cout << "不可用join()" << endl;
}

(2)其他创建线程的手法

用类作为可调用对象

a)void operator()(){}

​ 重载()可以使对象像函数那样使用,常常被称为函数对象。(这里用作线程对象的构造)

b)thread mytobj(ta);

​ 生成线程并且调用拷贝构造函数,复制ta对象,使得在主线程结束后,也可执行子线程内容。

c)mytobj.join();

​ 执行ta内容,执行完后,析构复制的ta对象

class TA
{
public:
    int m_i;
    TA(int i):m_i(i) {}
    TA(const TA &ta):m_i(ta.i) {}
    //这里重载()可以使对象像函数那样使用,常常被称为函数对象。(这里用作线程对象的构造)
    //必须将对象转化为函数对象
    void operator()()
    {
        cout<< "1m_i的值为: " <<  m_i <<endl;
        cout<< "2m_i的值为: " <<  m_i <<endl;
    }
};

int main()
{
    int myi = 6;
    TA ta(myi);//创建对象并调用构造函数
	thread mytobj(ta);//生成线程并且调用拷贝构造函数
	//mytobj.join();//执行ta,执行完成后析构拷贝的ta对象
    //若主线程已经执行完毕,myi(传入引用的话)内存被回收,子线程输出的m_i结果将不可预料
    //若主线程执行结束,主线程ta对象被销毁,但是对象实际已经被{[复制]入子线程}的ta对象依旧存在
    mytobj.detach();
	cout << "I love China!" << endl;
	return 0;
} 
用lambda表达式

auto <lambda表达式名> = [<捕获函数的局部变量>](<参数列表>){ } ;

auto f = []{
    cout << "线程开始执行" << endl;
	cout << "线程执行结束" << endl;
};
thread mytobj(f);
mytobj.join();

第三节 线程传参详解,detach()大坑,成员函数做线程函数

(1)传递临时对象作为线程参数第一讲

需要避免的陷阱①

a) char* p;cout << p << endl;

​ *p输出p指向地址的字母,而p输出整个字符串。

​ 相当于c语言中(printf语句打印%s的时候,提供字符串首地址,打印整个字符串)

b)陷阱一:detach,thread传值用[ 引用 ]

​ (引用)myi实际上是值传递,用detach也是安全的。

c)陷阱二:detach,thread传值用[ 指针 ]

​ 不安全,不可用!因为指针指向地址相同,会被销毁。

d)改进:char myp[]; thread mythread(myprint, myp); void myprint(string& p){}

​ 使用const string&代替char*。

void myprint(const int i, const string &p)
{
    cout << i << endl;
    cout << p.c_str() << endl;
    return;
}

int main()
{
    int myi = 1;
    int& myii = myi;
    char myp[] = "I love China!";
    thread mythread(myprint, myi, myp);// ② 陷阱一
    mythread.join();
    return 0;
}
需要避免的陷阱②

a) 陷阱一:(detach)事实上存在,主线程执行完了,系统才将myp转string。[ 验证方法见附录1 ]

b) 改进:(detach)使用临时对象在thread出进行转换。

//改进方法:
thread mythread(myprint, myi, string(myp));

[ thread {子线程名}(线程调用函数名,{数据类型}(参数1),参数……); ]

总结

a)使用int等简单类型参数,建议使用值传递,避免节外生枝。

b) 如果传递类对象,避免隐式类型转换。全部都在thread构建临时对象!在函数参数出用引用接收,避免在进行一次临时对象的构造,浪费!

###(2)传递临时对象作为线程参数第二讲

线程id概念

[ std::this_thread::get_id() ]

不同线程具有不同id。

临时对象构造时机抓捕

(3)传递类对象、智能指针作为线程参数

类对象

mutable:在const的函数里面修改与类状态无关的数据成员,用mutable来修饰。

std::ref 函数:目的:创建一个能够主、子线程能同步修改的数据(即引用)。且不再需要const和mutable。

class TA
{
public:
    int m_i;
    TA(int i):m_i(i) {}
    TA(const TA &ta):m_i(ta.i) {}
};

void myprint(A &a)// 用引用就必须带const
{
    a.m_i = 199; // 线程中修改数据,主线程中不会修改,因为是a对象是拷贝过来的
    cout << "a.m_i" << a.m_i;
    return;
}

int main()
{
    int myi = 6;
    TA ta(myi);
	thread mytobj(myprint,std::ref(ta));// ta就是主线程中的ta,未调用拷贝构造函数。
    mytobj.join();
    cout << "myi" << myi;
	return 0;
} 
智能指针

[ 智能指针相关解释见附录2 ]

unique_str不能拷贝问题:

std::move:[ thread mytobj( myprint, std::move(myp) ); ]

将myp置为空,将指针所有权交给q。

void myprint(unique_str<int> q){/*……*/}  //将myp置为空,将指针所有权交给q。

int main()
{
    unique_str<int> myp(new int(100));
	thread mytobj(myprint,std::move(myp));// unique_str不支持拷贝,但子线程传值需要拷贝
    mytobj.join();
    cout << "myi" << myi;
	return 0;
} 

(4)用成员函数指针做线程函数

a)[ thread {子线程名}(&类名::成员函数,类对象,参数 ); ]

b)&ta==std::ref(ta)

​ &指取this指针。

class TA
{
public:
    int m_i;
    TA(int i):m_i(i) {}
    TA(const TA &ta):m_i(ta.i) {}
    void thread_work(int num){/*……*/}
    void operator()(){/*……*/}
};

int main()
{
    TA ta(10);
    thread mytobj(&ta::thread_work,std::ref(ta),100);//&ta==std::ref(ta),传入引用,不拷贝
	//thread mytobj(ta,100);// 以operator()函数为入口
    mytobj.join();
    cout << "myi" << myi;
	return 0;
} 

第四节 创建多个线程、数据共享问题分析、案例代码

(1)创建和等待多个线程

线程入口函数(某线程执行的函数)

a)多个线程混乱执行,与线程调度机制有关。

b)将thread放入容器,一次创建多个线程。

void myprint(int q)
{
    cout << "id" << std::this_thread::get_id() << endl;
    return;
}

int main()
{
    vector<thread> mythreads;
    for(int i = 1;i < 10; i++)
    {
        mythreads.push_back(thread(myprint,i));
    }
    for(auto iter = mythreads.begin(); iter != mythreads.end(); ++iter)
    {
        iter->join();
    }
    return 0;
}

(2)数据共享问题分析

只读的数据

安全稳定,不需特殊处理手段。

vector<int> g_v = {1};//只读的共享数据

void myprint()
{
	cout << "id:" << std::this_thread::get_id() << "的线程g_v值:" << g_v[0] << endl;
}

int main()
{
	thread mytobj(myprint);
	mytobj.join();
	cout << "I love China!" << endl;
	return 0;
} 
有读有写
// 2个线程写,8个线程读
// 要求读的时候不能写,写的时候不能读。
// 2个线程不能同时写,8个线程不能同时读。
其他案例
// 数据共享:十个窗口,两个窗口同时都要订99座。

(3)共享数据的保护案例代码

又读又写,程序出异常。

因此引入下一节互斥量的概念。

//网络游戏服务器
//两个自己创建的线程:
//  	一个线程:收集玩家命令,并把命令数据写到一个队列中。
//		另一个线程:从队列中取出玩家发送的命令,解析,然后执行玩家需要的动作。
//		(list: 频繁按顺序插入和删除数据时效率高	vector:随机插入和删除数据效率高)

class TA
{
public:
    //收集玩家命令线程
    //写数据
    void inMsgRecvQueue()
    {
        for(int i = 0; i < 10000; ++i)
        {
            cout << i <<" "<< std::this_thread::get_id() << endl;
            msgRecvQueue.push_back(i);//假设i为玩家命令
        }
    }
    //取出玩家发送的命令线程
    //读数据,删除数据
    void outMsgRecvQueue()
    {
        for(int i = 0; i < 10000; ++i)
        {
            if(!msgRecvQueue.empty())
            {
                cout << "消息队列不为空" << endl;
                int command = msgRecvQueue.front(); // 返回第一个元素。
                msgRecvQueue.pop_front();//处理完之后移除
            }
            else
            {
             	cout << "消息队列为空" << endl;
            }
        }
    }
private:
    std::list<int> msgRecvQueue;//收集玩家命令,并把命令数据写到一个队列中
};

int main()
{
    TA myobja;
    std::thread myOutMsgObj(&A::outMsgRecvQueue,&myobja);//使用引用,返回this指针,保证主线程和子线程使用的是同一个对象
    std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
    myOutMsgObj.join();
    myInMsgObj.join();
    return 0;
}

第五节 互斥量概念、用法、死锁演示及解决详解

(1)互斥量(mutex)的基本概念

互斥量:是一个类对象,理解成一把锁。只有一个线程可以锁住数据并使用。

声明了一个mutex变量a后,可以用a.lock(),a.unlock()进行加锁解锁,加锁和解锁的次数必须相等

加锁期间能保证当前线程的操作不会被打断。

(2)互斥量的用法

头文件包含:#include

mutex my_mutex;// 声名变量my_mutex
lock()unlock()

a)先lock(),再操作共享数据,unlock();

b)lock(),unlock()成对出现。

优点:灵活 缺点: lock(),unlock()不成对

std::lock_guard类模板

一个用类实现的,包装好了的锁声明一个mutex变量a后,可以初始化一个类模板

例:lock_guard obj(a);

obj对象在被初始化的时候自动加锁,能在离开当前作用域后,自动析构解锁

缺点:不灵活 。

a)可以使用 { } 改变他的作用域周期,例如:

{
cout<<  ……………… 略
	{
   		lock_guard<std::mutex> sbGuard(my_mutex);
   		msgRecvQueue.push_back(i);
	}
}
return;

b)相当于:

{
/* ……………… */ 
	{
   		my_mutex.lock();
   		msgRecvQueue.push_back(i);
    	my_mutex.unlock();
	}
/* ……………… */ 
}
return;

(3)死锁

有两个线程(A和B)和两个锁(c和d),A锁了c,还想要d的锁进行下一步操作,但这时B锁了d,但是想要c进行下一步操作。于是彼此互相锁死。

eg:张三在北京等深圳李四,李四在深圳等北京张三,互相等,彼此锁死。

死锁的演示
死锁的一般解决方案

1、保证两个互斥锁的上锁顺序一致
2、或用lock()这个函数模板,进行同时上锁。(只有当每个锁都是可锁的状态,才会真正一次性两个互斥量同时上锁,程序才会继续向下走)

要么两个都锁,要么两个都不锁。

mutex mutex2;
mutex mutex1;

/* 1、保证两个互斥锁的上锁顺序一致 */
	mutex1.lock();
	mutex2.lock();//其他代码块使用mutex1、mutex2也需要先1后2,避免死锁
/* …………  */
	mutex1.unlock();
	mutex2.unlock();
/* 2、或用lock()这个函数模板,进行同时上锁 */

std::lock()函数模板

目的:使用两个以上的锁,并且避免因为锁的执行顺序导致的死锁问题。

要么都锁,要么都不锁。

用lock()这个函数模板,进行同时上锁。(只有当每个锁都是可锁的状态,才会真正一次性两个互斥量同时上锁,程序才会继续向下走)

lock(a, b);
/*  ...  */
a.unlock();
b.unlock();

但此时仍需要考虑unlock()问题,因此引入std::lock_guard的std::adopt_lock参数。

std::lock_guard的std::adopt_lock参数

std::adopt_lock:(结构体对象)表示互斥量已被lock,无需再次加锁,只需要负责解锁即可。

lock(a, b);
lock_guard<mutex> obj1(a,adopt_lock);
lock_guard<mutex> obj2(b, adopt_lock);
/*  ...  */

第六节 unique_lock详解

(1)unique_lock取代lock_guard

一个用类实现的,包装好的锁,但相比lock_guard 功能更多更灵活没有额外参数的情况下,效果和lock_guard相同。(unique_lock obj(a);)

第一个参数:mutex对象。

(2)unique_lock第二个参数

std::adopt_lock

std::adopt_lock:(结构体对象)表示互斥量已被lock,无需再次加锁,只需要负责解锁即可。{例子见上节std::lock_guard的std::adopt_lock参数}

std::try_to_lock

尝试去锁,如果没锁成功也会返回,不会卡死在那,可用owns_lock()得到是否上锁的信息。

unique_lock<mutex> obj(mute,try_to_lock);
if (obj.owns_lock()) 
    {/*如果锁上了要怎么做…*/}
else 
    {/*没锁上也可以干别的事*/}
std::defer_lock

初始化:用一个还没上锁的mutex变量初始化一个对象。

加锁:自己可以在后续代码段中的某个位置加锁。

解锁:离开作用域时,能帮助我们解锁,当然我们也能提前手动a.unlock()解锁。

mutex a;
unique_lock<mutex> obj(a,defer_lock);
/* 一些代码 */
a.lock(); //也可结合条件判断语句使用a.try_lock() ,若成功锁上能返回true,否则返回false 

(3)unique_lock的成员函数

lock()
unlock()
{
    mutex a;
    unique_lock<mutex> obj(a,defer_lock);
    /* 一些代码 */
    a.lock(); //也可结合条件判断语句使用a.try_lock() ,若成功锁上能返回true,否则返回false 
    	/*处理共享代码*/

    	/*因为有一些非共享代码要处理,手动解锁*/
    obj.unlock();
    	/*处理非共享代码*/
    
    obj.lock();
    
    	/*处理共享代码*/
    
    //obj.unlock();/* 可以不写,因为defer_lock离开作用域时会帮助我们解锁 */
}
release()

返回他所管理的mutex对象指针,并释放所有权。

std::unique_lock<std::mutex> obj(my_mutex);
std::mutex *ptx = obj.release(); //解锁和上锁责任转移给ptx

/*处理非共享代码*/

ptx->unlock(); 
try_lock()

defer_lock参数与try_lock()搭配例子:

unique_lock<mutex> obj(mute,defer_lock);
if (obj.try_lock() == true) 
    {/*如果锁上了要怎么做…*/}
else 
    {/*没锁上也可以干别的事*/}
owns_lock()

用owns_lock()得到try_to_lock是否上锁的信息。

try_to_lock参数 和 owns_lock()成员函数配对使用。

unique_lock<mutex> obj(mute,try_to_lock);
if (obj.owns_lock()) 
    {/*如果锁上了要怎么做…*/}
else 
    {/*没锁上也可以干别的事*/}
一般来讲,锁住的代码越少,效率越高

(4)unique_lock所有权传递

unique_lock对mutex对象的所有权可转移,但不能复制。

unique_lock所有权传递方法一:

move( )

unique_str所有权传递:
p1.reset(p2.release()); 意为将p2指针所有权给p1,并将p2置为空。

unique_lock所有权传递:

unique_lock<std::mutex > obj1( move(obj2) );意为将obj2指针所有权给obj1,并将obj2置为空。

std::unique_lock<std::mutex> obj2(a,defer_lock);

std::unique_lock<std::mutex> obj1(std::move(obj2));//移动语义

unique_lock所有权传递方法二:

return unique_lock<std::mutex >

[ 涉及有关移动构造函数问题,见附录3 ]

unique_lock<mutex> temp()
{
    mutex my_mutex;
    unique_lock<mutex> temp(my_mutex);
    return temp;
    //返回对象temp会让系统生成临时unique_lock对象,并调用unique_lock的移动构造函数
}

unique_lock<mutex> obj = temp();

(5)总结

lock_guard 配合 adopt_lock使用

unique_lock配合 defer_lock使用

在两种情况都能使用时,推荐使用lock_guard,因为它更快并且使用的内存空间更少。

unique_lock支持所有权的传递,所以更加灵活。

第七节 单例设计模式共享数据分析、解决,call_once

(1) 设计模式大概谈

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。

使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。

(2) 单例设计模式

  • 1、单例类只能有一个实例。

  • 2、单例类必须自己创建自己的唯一实例。

  • 3、单例类必须给所有其他对象提供这一实例。

  • 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

  • 关键代码:构造函数是私有的。(私有构造函数只能在函数内部调用,外部不能实例化,所以私有构造函数可以防止该类在外部被实例化

  • 为什么不能再单例类析构函数中进行delete:

    ​ 因为你是需要delete调用析构函数,而不是调用析构函数才delete。

class MyCAS
{
private:
	MyCAS(){} //私有化了的构造函数
 
private:
	static MyCAS *m_instance; //静态成员变量
 
public:
	static MyCAS *GetInstance()
	{
		if (m_instance == NULL)
		{
			m_instance == new MyCAS();
			static CGarhuishou cl;
		}
		return m_instance;
	}
 
	class CGarbohuishou //类中套类,用来释放对象,Garbo意为垃圾工人
	{//类CGarbohuishou被定义为MyCAS的私有内嵌类,以防该类被在其他地方滥用。
	public:
		~CGarhuishou() //类的析构函数中
		{
			if (MyCAS::m_instance)
			{
				delete MyCAS::m_instance;
				MyCAS::m_instance = NULL;
			}
		}
	};
 
	void func()
	{
		cout << "测试" << endl;
	}
};
 
//类静态变量初始化(所有类对象时共享)
MyCAS  *MyCAS::m_instance = NULL; 
 
int main()
{
	MyCAS *p_a = MyCAS::GetInstance(); //创建一个对象,返回该类(MyCAS)对象的指针
	// MyCAS *p_b = MyCAS::GetInstance(); //第二次,返回的也是p_a;
    p_a->func();
	MyCAS::GetInstance()->func();  //该装载的数据装载
 
	return 0;
}

(3) 单例设计模式共享数据问题分析、解决

加锁问题:

if (m_instance == NULL),不代表m_instance一定没被new过;因为多个线程上下文切换问题,可能线程1要执行 m_instance == new MyCAS();行,但还没执行完,切换线程二,此时m_instance仍然等于NULL,线程2会new一次,切换回线程1仍然会new一次,因此为了不影响单例,需要加锁。

{
    std::unique_lock<std::mutex> mymutex(resource_mutex); //自动加锁,但效率非常低
    if (m_instance == NULL)
    {
        m_instance == new MyCAS();
        static CGarhuishou cl;
    }
}

但每次执行该函数,都会导致串行,效率非常低,因此我们利用if (m_instance == NULL),不代表m_instance一定没被new过,再加一个判断,则在提高效率同时,又保证单例。

static MyCAS *GetInstance()
{
    //提高效率 if (m_instance == NULL)
    //a)如果if (m_instance != NULL) 条件成立,则肯定表示m_instance已经被new过了;
    //b)如果if (m_instance == NULL),不代表m_instance一定没被new过;因为多个线程上下文切换问题,可能
    if (m_instance == NULL) //双重锁定(双程检查)
    { 
        std::unique_lock<std::mutex> mymutex(resource_mutex); //自动加锁,但效率非常低
        if (m_instance == NULL)
        {
            m_instance == new MyCAS();
            static CGarhuishou cl;
        }
    }
    return m_instance;
}

(4) std::call_once

第二个参数是一个函数名func();

std::call_once()功能是能够保证函数func()只被调用一次

具备互斥量这种能力,而且效率上比互斥量消耗的资源更小;

需要与标记std::once_flag一起使用:

​ 通过这个标记决定对应函数a()是否执行,调用call_once()成功后,std::call_once()就把这个标记设置为一 种已调用状态,后续继续调用std::call_once(),只要once_flag被设置为了“已调用”状态,那么对应的函数a() 就不会再被执行了。

std::once_flag g_flag; // 标记
/*  函数func()  */
//两个线程同时执行到这里,其中一个线程要等另外一个线程执行完毕CreateInstance(),这里可以把g_flag看作一把锁
std::call_once(g_flag,func());
// sleep 20s
std::chrono::microseconds dura(20000);//std::chrono::duration 表示一段时间
std::this_thread::sleep_for(dura);

第八节 条件变量condition_variable

(1)条件变量std:: condition_variable、wait()、notify_one()

condition_variable是一个类,与条件相关,需要等待一个条件的达成,和互斥量来配合工作的

具有成员函数:wait()、notify_one()等等。

线程A:等待一个条件满足

线程B:专门在消息队列中扔消息,线程B触发了这个条件(notify_one()等),A就满足条件了(可以解锁wait()),继续执行

std::condition_variable my_cond;  // 条件变量对象 

wait()

第一个参数是:互斥量。

第二个参数是:lambda表达式。

​ 如果lambda表达式返回值是false,那么wait将解锁第一个参数(互斥量)(让出CPU),并堵塞到本行。
​ 堵塞到什么时候呢?堵塞到其他某个线程调用notify_one()成员函数为止。

​ 如果返回true,那么wait()直接返回。
​ 如果没有第二个参数,就跟默认第二个参数返回false效果一样。

my_cond.wait(sbGuardOUT,[this]{
    if(!msgRecvQueue.empty()) return true;
    return false;
});// 此句之后,这个互斥锁一定是锁着的  同时msgRecvQueue至少有一条数据的

notify_one()

尝试把wait的线程唤醒,执行完这行,wait就被唤醒了

如果要唤醒所有wait的线程,使用notify_ all()。

 my_cond.notify_one();
//  my_cond.notify_ all();

假如现在A线程正在处理一个事物 ,需要一段时间,并没有卡在wait等你唤醒

而B线程已经调用过notify_one了,那notify_one就没效果了。

A线程notify_one之后,可能会接着lock,而另一个线程可能也在lock,两个线程谁先拿到锁不一定,所以死锁的问题就出现了。

(2)代码深思考

解释在代码注释里。

class A
{
public:
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 10000; i++)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
 
			std::unique_lock<std::mutex> sbguard(my_mutex);
			msgRecvQueue.push_back(i);
 
			//假如outMsgRecvQueue()正在处理一个事务,需要一段时间,而不是正卡在wait()那里等待你的唤醒,
			//那么此时这个notify_one()这个调用也许就没效果;
			my_cond.notify_one();//我们尝试把wait()线程唤醒,执行完这行,那么outMsgRecvQueue()里面的wait()就会被唤醒
			                       //唤醒之后的事情后续研究;
		}
	}
 
	void outMsgRecvQueue()
	{
		int command = 0;
		
		while (true)
		{
			std::unique_lock<std::mutex> sbguard(my_mutex);
 
			//但其他线程用notify_one()将本wait(原来是睡着/堵塞)的状态唤醒后,wait()就开始恢复干活了,那恢复后的
			//wait()干什么活?
			//a)wait()不断的尝试重新获取互斥量锁,如果获取不到,那么流程就卡在wait()这里等着获取,如果获取到了互斥锁,
			//那么wait()就继续执行b
			//b)上锁(实际上获取锁了就等于上了锁)
			//b.1)如果wait有第二个参数(lamdba),就判断这个lamdba表达式,如果表达式为false,
			//那么wait()又对互斥量解锁然后又休眠,这里继续等待再次被notify_one()唤醒
			//b.2)如果lamdba表达式为true,则wait()返回,流程走下来(此时互斥锁被锁着)
			//b.3)如果wait()没有第二个参数,则wait()返回,流程走下来
            //为防止虚假唤醒:wait()中要有第二个参数(lambda)并且这个lambda中要正确处理公共数据是否存在
			my_cond.wait(sbguard, [this] {         //一个lambda就是一个可调用对象(函数)
				if (!msgRecvQueue.empty())
					return true;
				return false;
			});
			//流程能走到 这里来,这个互斥锁一定是锁着的。同时msgRecvQueue至少有一条数据的
			command = msgRecvQueue.front();
			msgRecvQueue.pop_front();      
			sbguard.unlock();              //因为unique_lock的灵活性,所以我们可以随时解锁,以免锁住太长时间
 
			cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
		}//end while
	}
 
private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex;//创建一个互斥量(一把锁)
	std::condition_variable my_cond;//生成一个条件变量对象
};

(3)notify_all()

notify_one()只能通知一个线程;

notify_ all()通知所有线程;

第九节 async、future、packaged_task、promise

(1)std::async std::future创建后台任务并返回值

目的:希望线程返回一个结果

async:(函数模板)

头文件:#include< future >

目的:启动一个异步任务(自动创建一个线程并开始执行对应的线程入口函数)

返回值:std::future对象(调用std::future对象的成员函数get()来获取结果)

参数:

​ 第一个参数是std::launch::的枚举类型,包含std::launch::deferred、std::launch::async。后面的参数同lock参数(若是类的成员函数做对象,第二个参数是类的成员函数,第三个参数是类对象,第四个参数及以后是成员函数的参数等等……)

​ 参数 std::launch::deferred:

​ a)表示线程入口函数调用被延迟到std::future的wait()或者get()函数调用时才执行;

​ b)延迟调用:没有创建线程,是在主线程中调用的线程入口函数 ;

​ c)如果wait()或者get()没有没调用,就不会执行。

参数 std::launch::async:

​ a)若std::async()没有第一个参数,默认使用(std::launch::async | std::launch::deferred)

​ b)在调用async函数的时候就开始创建线程

std::future<int> result = std::async(std::launch::async, &A::mythread2, &a, tmppar);

std::future:(类模板)需要两步,一绑定,二返回值。

成员函数:

​ get():不拿到返回值,就堵塞线程。//本函数只能调用一次。

​ wait():等待线程返回,本身并不返回结果。

share_future:(类模板)

​ share_future的get()函数复制数据。

​ future的get()函数转移数据。

class A
{
public:
	int mythread2(int mypar) //线程入口函数
	{
		cout << mypar << endl;
		return mypar;
	}
};

int main()
{
	A a;
	int tmppar = 12;
	//std::future<int> result = std::async(mythread);//创建一个线程并开始执行,绑定关系,流程并不卡在这里
	std::future<int> result = std::async(std::launch::async, &A::mythread2, &a, tmppar);
	cout << result.get()<< endl;//卡在这里等待mythread()执行完毕,拿到结果;只能调用一次;
	//result.wait();//等待线程返回,本身并不返回结果
	return 0;
}

(2)std::packaged_task模板类

把对象(函数、lamdba表达式等等)包装起来,方便将来作为线程入口函数来调用。

[ packaged_task<函数返回值类型(函数参数类型)> packaged_task变量名(线程入口函数名); ]

成员函数:

​ get_future():返回std::future值,调用future的get()函数得到返回值。

//线程入口函数mythread
std::packaged_task<int(int)> mypt(mythread);//我们把函数mythread通过packaged_task包装起来
std::thread t1(std::ref(mypt),1); //线程直接开始执行,第二个参数作为线程入口函数的参数
t1.join();
std::future<int> result = mypt.get_future();//std::future对象里包含有线程入口函数的返回结果,这里result保存mythread返回的结果
cout <<result.get() << endl;

lamdba表达式:将函数写成lamdba表达式做参数。

vector < std::packaged_task<int(int)>> mytasks;//容器
 
int main()
{
	cout << "main" << "threadid= " << std::this_thread::get_id() << endl;
 
	std::packaged_task<int(int)> mypt([](int mypar) {
		cout << mypar << endl;
		std::chrono::milliseconds dura(5000); //定一个5秒的时间
		std::this_thread::sleep_for(dura);  //休息一定时常		
		return 5;
	});//将函数写成lamdba表达式做参数。
 
	mytasks.push_back(std::move(mypt)); //容器,这里用了移动语义std::move
    //将权限给mytasks,并释放自己的权限
 
	std::packaged_task<int(int)> mypt2;
	auto iter = mytasks.begin();
	mypt2 = std::move(*iter); //移动语义
	mytasks.erase(iter); //删除一个元素,迭代器已经失效了,所以后续代码不可以再使用iter;
 
	mypt2(105);//直接调用,相当于函数调用,调用lamdba
	std::future<int> result = mypt2.get_future();
	cout << result.get() << endl;
 
	return 0;
}

(3)std::promise类模板

std::promise 与 std::future绑定 通过std::promise对象的get_future()

在某个线程中给它赋值,然后我们可以在其他线程中把这个值取出来用。

通过promise保存一个值,在将来我们通过把一个future绑定到这个promise上来得到这个绑定的值。

函数成员:set_value(),将需要传回future的值放入其中。

void mythread(std::promise<int>&tmpp, int calc)
{
	/*…………………………*/
	calc++;
    /*…………………………*/
	int result = calc; //保存结果
	tmpp.set_value(result);  //结果保存到了tmpp这个对象中
} 
 
void mythread2(std::future<int> &tmpf) 
{
	auto result = tmpf.get();//不get到就一直阻塞,因此不会出现数据异常
	cout <<"mythread result = " << result<<endl;
}
 
int main()
{
	std::promise<int> myprom; //声明一个std::promise对象myprom,保存的值类型为int;
	std::thread t1(mythread,std::ref(myprom),180);
	t1.join();
 
	//获取结果值
	std::future<int> fu1 = myprom.get_future();//promise与future绑定,用于获取线程返回值
 
	std::thread t2(mythread2,std::ref(fu1));
	t2.join(); //等mythread2执行完毕
 
	return 0;
}

第十节 future其他成员函数、shared_future、atomic

(1)std::future其他成员函数

valid():返回get()返回的std::future对象是否有意义。

wait_for:等待函数。

枚举类型:std::future_status::timeout(超时)、ready、deferred(递 延)

std::future<int> result = std::async(mythread);
std::future_status status = result.wait_for(std::chrono::seconds(1));// 时间表达式
//std::chrono::milliseconds dura(5000); 
//std::this_thread::sleep_for(dura); 
if(status == std::future_status::timeout){  
 //超时:我想等待1s,希望你返回数值给我,但是你没有返回,所以超时
 //表示线程mythread还没执行完,
    cout << "超时,线程还没执行完" << endl;
}
else if(status == std::future_status::ready){
    cout << " 线程成功返回 " << endl;
}
else if(status == std::future_status::deferred){
    cout << "线程被延迟执行" << endl;
    // 这个线程函数是在主线程执行的,相当于没有创建子线程
    cout << result.get() << endl;
}

(2)std::shared_future类模板

解决多个线程都想得到结果。

share_future的get()函数复制数据。

future的get()函数转移数据。

std::packaged_task<int(int)> mypt(s7::myThread2);
std::thread t1(std::ref(mypt),1);
t1.join();
//std::future<int> result = mypt.get_future();
//std::shared_future<int> result_s(std::move(result));//result_s(result.share());//移动语义
//bool ifcanget = result.valid();//返回值是否为空
		// 等价于 //
//通过get_future 构造一个shared_future 对象
std::shared_future<int> result_s(mypt.get_future()); 

(3)原子操作std::atomic

原子操作概念引出范例

互斥量:多线程编程中,保护共享数据:先锁,操作共享数据,开锁

有两个线程对一个变量进行操作atomVal共享变量,一个线程读、一个线程写,即使这样也会出问题

原子操作不需要用到互斥量加锁(是无锁)技术的多线程并发编程方式

在多线程中,不会被打断的程序执行片段,效率比互斥量高,原子操作是不可分割的状态

互斥量往往是针对一个代码段,而原子操作一般对某个变量操

std::atomic

类模板为了封装某个类型的值

// 读线程atomVal表示多个线程之间共享的变量;

int temVal = atomVal;

//写线程

atomVal = 6;

//myMetux.lock();
//count++;
//myMetux.unlock();//如果不加锁,由于汇编代码可能是多句,上下文切换,会导致结果不稳定
   //  等价于  //
 std::atomic<int> my_conut = 0;
 my_conut++;//原子操作不会被打断

一般用于计数或者统计或者做标识(bool)

第十一节 std::atomic续谈、std::async深入谈

(1)原子操作std::atomic续谈

atomic 针对 ++、–,+=,&=、|=、^=等运算符是可以的,有些操作不是原子操作。

std::atomic<int> my_test = 0;
my_test += 1;//结果正确
my_test = my_test + 1;//结果正确

load():以原子方式读atomic对象的值

store():以原子方式写入内容

atomic<int> atm;
atm = 0;
cout << atm << endl; //读原atm是原子操作子,但是整个这一行代码并不是原子操作
auto atm2(atm.load());//以原子方式读atomic对象的值
atm.store(12);//以原子方式写入内容

(2)std::async深入谈

std::async参数详解

如果没有参数默认使用(std::launch::async | std::launch::deferred)

使用场景,若线程紧张,就使用deferred,不创建线程继续执行,防止程序崩溃。

std:: async与std::thread的区别

std::async()与std::thread()最明显的不同,就是async并不一定创建新的线程

std::thread() 如果系统资源紧张,那么可能创建线程失败,整个程序可能崩溃。

std::thread()创建线程的方式,如果线程返回值,你想拿到这个值也不容易;

std::async()创建异步任务,可能创建也可能不创建线程;并且async调用方式很容易拿到线程入口函数的返回值。

由于系统资源限制:

(1)如果使用std::thread()创建的线程太多,则可能创建线程失败,系统报告异常,崩溃;

(2)如果用std::async,一般就不会报异常崩溃,因为如果系统资源紧张导致无法创建新线程的时候,std::async这种不加额外参数的调用就不会创建新线程,而是后续谁调用了future::get()来请求结果,那么这个异步任务就运行在执行这条get()语句所在的线程上。

(3)如果你强制std::async创建新线程,那么就必须使用std::launch::async,承受的代价就是系统资源紧张时,可能程序崩溃。经验:一个程序里,线程的数量不易超过100-200,与时间片有关,详情参考操作系统。

std:: async不确定性问题的解决

如果没有参数默认使用(std::launch::async | std::launch::deferred),则由系统资源是否紧张决定,创不创建线程,这个会导致一些不确定性问题。

int mythread() //线程入口函数
{
	cout << "mythread start" << "threadid= " << std::this_thread::get_id() << endl; //打印线程id
 
	std::chrono::milliseconds dura(5000); //定一个5秒的时间
	std::this_thread::sleep_for(dura);  //休息一定时常
 
	cout << "mythread end" << "threadid= " << std::this_thread::get_id() << endl; //打印线程id
 
	return 5;
}
int main()
{
	cout << "main" << "threadid= " << std::this_thread::get_id() << endl;
	std::future<int> result = std::async(mythread);//流程并不卡在这里
	cout << "continue....." << endl;
 
	//枚举类型
	std::future_status status = result.wait_for(std::chrono::seconds(1));//等待一秒
	
	if (status == std::future_status::deferred)
	{
		//线程被延迟执行了,系统资源紧张
		cout << result.get() << endl; //此时采取调用mythread()
	}
	else if (status == std::future_status::timeout)//
	{
		//超时:表示线程还没执行完;我想等待你1秒,希望你返回,你没有返回,那么 status = timeout
		//线程还没执行完
		cout << "超时:表示线程还没执行完!" << endl;
	}
	else if (status == std::future_status::ready)
	{
		//表示线程成功返回
		cout << "线程成功执行完毕,返回!" << endl;
		cout << result.get() << endl;
	}
	return 0;
}

第十二节 Windows临界区、各种mutex互斥量

(1)Windows临界区

Windows临界区与互斥量用法非常相似;但也有些差别

在“同一个线程”(不同线程中会卡住等待)中, Windows中的“相同临界区变量”代表的临界区的进入(EnterCriticalSection)可以被多次调用,但是调用了几次EnterCriticalSection(),就得调用几次EnterCriticalSection()

而在C++11中,std::mutex不允许同一个线程中lock同一个互斥量多次,否则报异常;

多线程 互斥量与Windows临界区对比代码如下:

#define _WINDOWSJQ_
 
class A
{
public:
	//把收到的消息入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 10000; i++)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
 
#ifdef _WINDOWSJQ_
			EnterCriticalSection(&my_winsec);  //进入临界区(加锁)
			msgRecvQueue.push_back(i);
			LeaveCriticalSection(&my_winsec); //离开临界区(解锁)
			
#else
			std::lock_guard<std::mutex> sbguard(my_mutex);
			msgRecvQueue.push_back(i); //假设这个数字i就是收到的命令,直接弄到消息队列里边来;
#endif
		}
	}
 
	bool outMsgLULProc(int &command)
	{
#ifdef _WINDOWSJQ_
		EnterCriticalSection(&my_winsec);
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
			msgRecvQueue.pop_front();//移除第一个元素。但不返回;
			LeaveCriticalSection(&my_winsec);
			return true;
		}
		LeaveCriticalSection(&my_winsec);
#else
		my_mutex.lock();	
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
			msgRecvQueue.pop_front();//移除第一个元素。但不返回;
			my_mutex.unlock();  //所有分支都必须有unlock()
			return true;
		}
		my_mutex.unlock();
#endif
		return false;
	}
	//把数据从消息队列取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 10000; i++)
		{
			bool result = outMsgLULProc(command);
 
			if (result == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
				//处理数据
			}
			else
			{
				//消息队列为空
				cout << "inMsgRecvQueue()执行,但目前消息队列中为空!" << i << endl;
			}
		}
		cout << "end!" << endl;
	}
	A()
	{
#ifdef _WINDOWSJQ_
		InitializeCriticalSection(&my_winsec);//用临界区之前先初始化
#endif
	}
 
private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex;//创建一个互斥量(一把锁)
 
#ifdef _WINDOWSJQ_
	CRITICAL_SECTION my_winsec; //windows中的临界区,非常类似C++11中的互斥量,声名
#endif
};
``

### (2)自动析构技术

```c++
//本类用于自动释放windows下的临界区,防止忘记LeaveCriticalSection导致死锁的情况发生,
//类似于C++11中的std::lock_guard<std::mutex>
class CWinLock  //叫RAII类  (resource Acquisition is initialization)“资源获取即初始化” eg 智能指针类,容器 都属于RAII类
{
public:
	CWinLock(CRITICAL_SECTION *pCritmp)
	{
		m_Pcitical = pCritmp;
		EnterCriticalSection(m_Pcitical);
	}
 
	~CWinLock()
	{
		LeaveCriticalSection(m_Pcitical);
	}
private:
	CRITICAL_SECTION *m_Pcitical;
};

(3)recursive_mutex递归的独占互斥量

2.1 recursive_mutex递归的独占互斥量
recursive_mutex 递归的独占互斥量:允许同一个线程,同一个互斥量多次被lock,但效率上比mutex更低;递归次数据说有限制,递归太多可能有异常。

(4)超时互斥量std::timed_mutex和超时递归互斥量recursive_timed_mutex

2.2超时互斥量std::timed_mutex
std:: timed_mutex:带超时功能的独占互斥量

try_lock_for():等待一段时间,如果拿到了锁,或者等待超时,没有拿到锁,就走下来

try_lock_until();参数是一个未来的时间点,这个未来的时间没到的时间内,如果我拿到了锁头,那么就走下来;如果时间到了,没拿到锁,程序的流程也走下来;

2.3超时递归互斥量recursive_timed_mutex
std:: recursive_timed_mutex:带超时功能的递归独占互斥量(允许同一个线程多次获取这互斥量),用法与std::timed_mutex相同。

附录

1

(detach) thread mythread(myprint, myi, string(myp));

验证方法:使用[ 类 ]验证,调试,观察主线程执行完毕时,myp是否转换了类型。

class A
{
public:
    int m_i;
    A(int a):m_i(a){cout<<"构造函数执行"<<endl;}
    A(const A &a) :m_i(a.m_i){cout<<"拷贝构造函数执行"<<endl;}
    ~A(){cout<<"析构函数执行"<<endl;}
};

void myprint(const int i, const A &p)
{
    cout << i << endl;
    cout << &p << endl;//打印p的地址
    return;
}

int main()
{
    int myi = 1;
    int& myii = myi;
    char myp[] = "I love China!";
    thread mythread(myprint, myi, A(myp));// ② 陷阱一
    mythread.detach();
    return 0;
}

2

智能指针(c++11

确保动态分配内存的对象在应该被释放时,指向它的智能指针可以确保自动的释放它。

定义在memory头文件里的三种智能指针类型:

shared_ptr:允许多个指针指向同一个对象。

初始化方法:[ shared_ptr<数据类型>{变量名}= make_shared<数据类型>({数值});]

unique_str:“独占”所指向的对象。

不支持拷贝,也不支持拷贝构造函数。

指针所有权传递:[ p1.reset(p2.release()); 意为将p2指针所有权给p1,并将p2置为空 ]

weak_ptr:弱引用,指向shared_ptr管理的对象。

3

移动构造函数

移动构造函数是C++11中新增加的一种构造函数

目的:为了解决以往只能复制,而不能转移而造成的性能消耗。

class A {
    public:
        /*...*/
        A(A && a){ // 移动构造函数 &&:传入右值
            std::cout << "A move construct ..." << std::endl;
            ptr_ = a.ptr_;
            a.ptr_ = nullptr;
        }
       /* ...*/
};

move( )函数:将括号中的数据改为右值

意义:使例子中的vector内部通过移动构造函数创建A对象,减少了对堆空间的频繁操作。

vector<A> vec;
vec.push_back(move( A() ));

右值和左值(c++11)

右值:指的的临时值或常量(更准确的说法是保存在CPU寄存器中的值为右值)

左值:是保存在内存中的值。

int a = 5;// 5是右值,a是左值
// int && e = a; 报错,因为a是左值,无法赋值给右值
int && e = std::move(a); // 正确,使用move()将左值改编为右值

通用引用

一,必须是T&&的形式;

二,T类型要可以推导,

所以 auto && b = 5/a; 都可以。(通用引用既可接收左值,又可接收右值)但加了const就不再是通用引用了

疑惑 内嵌类析构

需补充单例模式reorder问题

参考(2条消息) 单例模式出现内存reorder,以及解决_陈九修的博客-CSDN博客

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
多线程修改sleep的一个常见方法是使用条件变量来控制线程的执行顺序。通过引入一个状态变量和两个条件变量,可以实现线程之间的同步和顺序执行。具体步骤如下: 1. 定义一个互斥锁mutex和两个条件变量cv_1、cv_2。 2. 定义一个静态变量point来记录线程间的状态。 3. 在add函数中,线程1通过互斥锁获取锁资源,并在状态为0时执行相应的操作。如果point的值能够被5整除,则修改状态为1,并唤醒等待在cv_1上的线程2。 4. 在print函数中,线程2通过互斥锁获取锁资源,并在状态为1时执行相应的操作。输出point的值后,修改状态为0,并唤醒等待在cv_1上的线程1。 5. 在主函数中创建两个线程t1和t2,并分别执行add和print函数。等待两个线程执行完毕后退出。 代码示例: ```c #include <iostream> #include <thread> #include <mutex> #include <condition_variable> using namespace std; mutex mt; condition_variable cv_1; static int point = 0; int status = 0; void add() { int times = 100; while (times--) { unique_lock<mutex> lk(mt); while (status != 0) { cv_1.wait(lk); } point++; if (point % 5 == 0) { status = 1; cv_1.notify_one(); } } } void print() { int times = 100 / 5; while (times--) { unique_lock<mutex> lk(mt); while (status != 1) { cv_1.wait(lk); } cout << "point: " << point << endl; status = 0; cv_1.notify_one(); } } int main() { thread t1 = thread(add); thread t2 = thread(print); t1.join(); t2.join(); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值