>博客是一个拾忆的过程,无形中节约了你的生命<
两个或更多的任务(独立的活动)同时发生(进行):一个程序同时执行多个独立的任务。
#include
#include
#include
class MyThread {
public:
MyThread() {};
~MyThread() {};
//线程相关
void ThreadFunction() {//线程入口函数
int i = 0;
while (true) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "th_prt i = " << ++i<<",th_id = " << std::this_thread::get_id() << "\r\n";
//退出循环判断
if (mtx.try_lock()) {
if (!th_status) {
mtx.unlock();
break;
}
mtx.unlock();
}
}
std::cout << "thread exit" << "\r\n";
}
std::mutex mtx; //定义互斥量
bool th_status = true; //线程状态
private:
};
int main()
{
MyThread th;
std::thread mytobjob(&MyThread::ThreadFunction, &th);//创建线程并运行
std::this_thread::sleep_for(std::chrono::milliseconds(500));//延时0.5s
//一、让线程退出循环
th.mtx.lock();
th.th_status = false;
th.mtx.unlock();
//二、阻塞主线程并等待子线程退出<因为子线程循环已结束,这里的阻塞时间几乎为零>
mytobjob.join();
std::cout << "END\r\n";
}
/***************************************************************************************/
说明:
01、thread ->是标准库里面的类。
02、std::thread mytobjob(func); ->创建线程mytobjob并执行,传入函数名作为线程开始入口。
03、mytobjob.join(); ->阻塞主线程并等待子线程结束(注意它是个结束线程的函数)。
04、线程的函数执行完毕,线程并没有完全退出,只是停止了而已。
05、子线程安全退出步骤总结:
设置标志位让子线程函数退出循环->调用join()函数结束子线程。
06、必须先让子线程退出循环再调用join()函数,因为join()函数会阻塞主线程。
07、主线程默认使用main()作为入口函数。
08、mytobjob.detach(); ->子线程与主线程分离,两者不在关联,主线程退出后,由后台接管。(注意这句
话,可能会让人误解,主线程结束时会退出进程,detach()的线程也会被系统回收,除非你阻止主线程
退出进程。总之进程一旦结束,其下的线程都会被回收)
09、mytobjob.joinable(); ->返回true和false,判断是否可以.join(),比如线程已经退出,就会返回false。
/***************************************************************************************/
<---------------------------------------------------------------------------->
1、mythread.h
#ifndef _MYTHREAD_H
#define _MYTHREAD_H
#include
#include
class MyThread {
private:
public:
MyThread();
~MyThread();
//线程相关
std::mutex mtx;
bool thread_flag = true; //线程跳出循环
//重载运算符作为线程入口
void operator()()//注意此函数执行时会拷贝一份类对象,所以并不与类公用成员变量
{ //而是复制出来的,一般不用这种方式
int i = 0;
while (true){
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "thraad prt i =" << i << "\r\n";
i++;
//线程退出循环判断
this->mtx.lock();
if (this->thread_flag == false){
this->mtx.unlock();
break;
}
this->mtx.unlock();
}
std::cout << "thraad exit"<< "\r\n";
}
};
#endif
<---------------------------------------------------------------------------->
2、mythread.cpp
#include "mythread.h"
//构造函数
MyThread::MyThread(){
std::cout<<"constructor prt:"<<"pcls = "<
3、main.cpp
#include
#include
#include "mythread.h"
int main()
{
MyThread th;
std::thread mytobjob(th);
//一、让线程退出循环
th.mtx.lock();
th.thread_flag = false; //注意此句并不能让线程退出循环
//因这个是对象的成员,线程函数使用的是它自己复制出来的
//所以程序的运行现象会一直阻塞
th.mtx.unlock();
//二、阻塞主线程并退出
mytobjob.join();
std::cout << "END\r\n";
}
<---------------------------------------------------------------------------->
01、要想使线程操做原对象的成员变量,可以用引用或指针,让引用在构造函数时绑定成员变量,因为引用复制后它绑定的还是原来的那个变量。(指针复制一份,它指向的内存不变)
02、注意,用指针和引用在修改原对象的成员变量时,必须保证原对象没有被析构,否则会产生严重后果。
03、可以自己定义拷贝构造函数来确定怎么拷贝。
04、重点:std::thread mytobjob(std::ref(th));//以引用的形式作为形参,可以解决上述问题
这种写法只能用mytobjob.join();退出,因为子线程中使用的是主线程中的对象
05、引用和std::ref(th)在这里起到的效果不一样
引用:先复制一份原对象,引用绑定到赋值出来的对象身上
std::ref(th):返回reference_wrapper类型,注意这个和引用&是有区别的,其可以阻止中间变量的
产,使用方法和引用一样,也可以赋值给引用类型。
生,感兴趣的可以查查相关资料。
#include
#include
#include "mythread.h"
int main()
{
auto mylamthread = []{
std::cout << "我的lam线程开始执行\r\n";
//.......
std::cout << "我的lam线程执行结束\r\n";
};
std::thread mytobjoblam(mylamthread);
mytobjoblam.join();
return 0;
}
//单独一个函数作为线程入口的方法与此相同
#include
#include
#include "mythread.h"
int main()
{
auto mylamthread = [](const int &i,char *pmybuf){
std::cout <<"形参 i = "<
A:看似没有问题的传递,但暗藏玄机,其实线程函数i绑定的并不是mvar,而是先拷贝一份mvar,而i绑定的拷贝的那个变量。(线程函数一般会搞复制这一套,其实是为了避免主线程退出之后,释放了变量,而访问了不可知的变量=>引用不是真引用、但保证了线程的安全)
B:需要注意的是第二个指针类型形参pmybuf和传入却是一个地址,因为无论指针怎么拷贝,指针指向的地址是不变的,但同时这种操作很危险,一定要要确保主线程结束之后,再释放指针指向的变量,注意指针就是一个变量而已。(=>指针是真指针,但不能保证线程的安全)
C:变量的传递就不用说了,因为我们都知道,变量的传参就是拷贝而已,而不是传入本身。
C:断点调试使用说明:shift+F9
#include
#include
#include
#include "mythread.h"
int main()
{
auto lamfunc = [](const int& i,const std::string &strx) {
std::cout << "形参 i = " << i << "\r\n";
std::cout << "形参 pmybuf = " << strx.c_str() << "\r\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
};
int mvar = 1;
int& mvary = mvar;
char str[] ="this is a test";
std::thread mythread(lamfunc, mvar, str);
mythread.join();
std::cout << "The main thread end!\r\n";
}
//1、str的数组传入,类型不匹配就会被隐式转换产生中间变量,所以str和strx的地址不一样,线程是安全的。
//2、这个程序看似没问题,但是还是存在一个隐藏很深的bug........
隐式转换是需要时间的,如果转换还没完成,str就被回收了,这个时候就会出现不可预料的结果。
陷阱二的解决办法
#include
#include
#include
#include "mythread.h"
int main()
{
auto lamfunc = [](const int& i,const std::string &strx) {
std::cout << "形参 i = " << i << "\r\n";
std::cout << "形参 pmybuf = " << strx.c_str() << "\r\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
};
int mvar = 1;
int& mvary = mvar;
char str[] ="this is a test";
std::thread mythread(lamfunc, mvar, std::string(str)); //创建的时候就转换
mythread.join();
std::cout << "The main thread end!\r\n";
}
/*
*创建的时候就转换,让类型转换在主线程中完成。
*陷阱二产生的根本原因,是转换在子线程中完成,而转换的条件是被转换的对象是存在的。
*如果先转换,后执行线程,就不会出现此类问题。
*总结1:线程的传的参数要在线程执行前准备完成,因为创建线程到线程成功运行中间应该是
一个很短的时间,所以在主线程和从线程没共享资源的条件下,应尽快让两者脱离关系,不要
在创建的时候拖泥带水,搞一堆事。(隐式转换就属于拖泥带水,非要在哪个间隙处理问题,资
源提前准备好不可以吗?)
*总结2:存在类型转换时,在创建线程时构造临时对象,形参使用引用类型。(重点、重点、重点)
*/
C++标准库函数提供了线程ID的获取方法:std::this_thread::get_id()
示例代码一:
#include
#include
#include
#include "mythread.h"
class A {
public:
int m_i;
A(int a) :m_i(a) {
std::cout << "[A::A(int a)构造执行]," << this << ",thread_id=" << std::this_thread::get_id() << "\r\n";
}
A(const A& a) :m_i(a.m_i) {
std::cout << "[A::A(const A)拷贝构造执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
}
~A() {
std::cout << "[A::~A()析构执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
}
};
int main()
{
std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
auto lamfunc = [](const A &clsx) {
std::cout << "子线程的参数clsx的地址 = "<<&clsx << ",thread_id =" << std::this_thread::get_id() << "\r\n";
};
int mvar = 1;
std::thread mythread(lamfunc,mvar); //mvar隐式类型转换成A类型,会调用构造函数进行类型转换
mythread.join();
std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
示例代码二
#include
#include
#include
#include "mythread.h"
class A {
public:
int m_i;
A(int a) :m_i(a) {
std::cout << "[A::A(int a)构造执行]," << this << ",thread_id=" << std::this_thread::get_id() << "\r\n";
}
A(const A& a) :m_i(a.m_i) {
std::cout << "[A::A(const A)拷贝构造执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
}
~A() {
std::cout << "[A::~A()析构执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
}
};
int main()
{
std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
auto lamfunc = [](const A &clsx) {
std::cout << "子线程的参数clsx的地址 = "<<&clsx << ",thread_id =" << std::this_thread::get_id() << "\r\n";
};
int mvar = 1;
std::thread mythread(lamfunc,A(mvar)); //mvar显示类型转换成A类型,创建临时对象,临时对象被拷贝一份给引用绑定,然后临时对象析构释放,
mythread.join();
std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
/*
注意1:示例中可以得出结论,拷贝是安全的(C语言形参传递也是拷贝),类型转换不安全的。
所以注意尽量避免在子线程中进行类型转换。(重点、重点、重点)
注意2:关于我对线程传参的理解,如果你觉得认可就认可,不认可以去翻看linux操作系统源码,主线程不会把自己的变量直接进行传递,主线程会复制一份变量,把复制的这份变量交给子线程,子线程在执行的时候用这份复制的变量进行参数传递。
1、主线程变量a,拷贝一份 a_x :此时a与a_x属于主线程的作用域。
2、创建线程时,让a_x的作用域主线程分离 :此时a_x属于子线程作用域。
3、子线程执行线程函数,首先要把a_x传入,(注意变量传入会再次拷贝)。
4、所以形参一般设置成引用,这样可以避免再次拷贝一次。
5、a_x的生命周期是其在线程函数执行结束才会被析构掉。
以上的作用域只是本人便于理解乱说的,但这样更容易理解。
1、为了线程安全,系统默认不允许修改拷贝的那份变量,所以引用类型不加const程序会报错。
2、临时对象不可修改,这是一个很强硬的原则问题。(修改也没啥意义,它都脱离主线程控制了)
3、临时对象虽然不能修改,但可以在传参时禁止主线程产生临时对象,方法如下:
#include
#include
#include
#include "mythread.h"
class A {
public:
int m_i;
A(int a) :m_i(a) {
std::cout << "[A::A(int a)构造执行]," << this << ",thread_id=" << std::this_thread::get_id() << "\r\n";
}
A(const A& a) :m_i(a.m_i) {
std::cout << "[A::A(const A)拷贝构造执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
}
~A() {
std::cout << "[A::~A()析构执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
}
};
int main()
{
std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
auto lamfunc = [](A &clsx) {
std::cout << "子线程的参数clsx的地址 = " << &clsx << ",thread_id =" << std::this_thread::get_id() << "\r\n";
};
int mvar = 1;
A v(mvar);
std::thread mythread(lamfunc,std::ref(v));//方法std::ref()可避免向子线程传递变量时产生临时对象
mythread.join();
while (1);
std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
//注意:如果禁止产生临时对象,引用形参操作的就是主线的作用域的变量,几乎等于操作全局变量。
//注意资源共享问题,这可是一个真引用。
//std::ref(v):此方法可阻止线程传参时产生临时对象。
std::unique_ptr
智能指针指向的是一块内存,如果让两个智能指针直接相等是语法上不允许的,因为一块内存不能被释放两次,需要先将指针转换为右值,才能使用,例如以下写法是错误的(编译器报错):
int main()
{
std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
auto lamfunc = [](std::unique_ptr pzn) {
std::cout << "子线程的参数clsx的地址 = " << *pzn << ",thread_id =" << std::this_thread::get_id() << "\r\n";
};
std::unique_ptr myp(new int(100)); //创建一个独占式智能指针myp指向一个new出来的int空间。
std::thread mythread(lamfunc,myp);
mythread.join();
std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
为说明右值问题,如下先进行一次C++11右值基础的复习,加深印象。
左值:(lvalue) => 存放在内存空间,有确切的地址。
右值:(rvalue) => 不存放在内存空间,没有确切的地址。
std::move(变量);//返回传入参数的右值引用。对与左值和右值,请参考C++基础。
//注意std会对变量造成破坏,故使用std::move后,变量尽量不要使用了。
复习零、类初始化和赋值是完全不同的两种概念
class A{} //定义一个类
A cls1(); //初始化操作(调用的是构造函数)
A cls2 = cls1; //初始化操作(调用的是构造函数)
cls2 = cls1; //赋值操作(调用的是赋值运算符)
对于引用,初始化(也叫绑定)和赋值也是不同的概念,而且引用必须初始化。初始化之后的其他操作都是对其被绑定对象的操作
以上我感觉对于学C++的人来说,意识到这点很重要。
复习一、三五法则
对于C++11之前的版本,适用三法则,C++11,及其之后的版本适用五法则。(法则请百度)
C++11的五法则做如下说明:
默认赋值运算符函数是浅拷贝。移动构造函数一旦创建,默认赋值运算符函数将不复存在。
移动构造函数要和赋值运算符构造函数一起创建(不使用赋值操作,可以忽略其函数不写)
一般拷贝构造函数写深拷贝代码,移动构造函数写浅拷贝代码(但无论怎样,他们只能参与初始化操作)
大白话:浅拷贝就是把对象的资源掏空,被掏空的对象就不要在使用了。
复习二、左值和右值
int a = 1; //a是一个左值,1是一个右值
int b = 2; //b是一个左值,2是一个右值
int c = a + b; //+需要右值,所以a和b都转换成右值,并且返回一个右值
函数和匿名对象都属于右值,右值也叫将亡值。
左值能转换为右值,右值却不能转换为左值
int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10; //p+1是一个右值,但是*(p+1)是一个左值
p+1 = 10; //错误
复习三、左值引用和右值引用
const int 与 int const 没有区别
const int & 与 int const & 没有区别
交换次序只有在指针时才有区别=>(int const * 和const int *有区别)
int & 只能绑定左值
int && 只能绑定右值,(注意右值也叫即将销毁的对象)
const int & 左值和右值都可以绑定(const int &会将右值的生命周期延长到和自己的一样)
右值引用是为了移动构造函数产生的,其实就是为了浅拷贝一个右值的资源,右值作为将亡值,毕竟属于即将销毁的值,但有时候我们不希望销毁,比如函数返回值,所以只能将其拷贝到一个变量中,然后其被析构,这中间的拷贝其实是一种浪费,移动构造函数可以避免拷贝带来的浪费。但移动构造也能拷贝左值,注意左值不是将亡值,如果对其进行移动构造,之后将以不要在使用这个左值。
复习五、匿名对象被扶正的问题
匿名对象,使用匿名对象做初始化时,不执行拷贝,而是被扶正(匿名对象和函数都属于右值)
例如:std::string str = std::string("haut school"); //右边的匿名对象会被扶正
复习六、移动构构造函数
C++11新特性,右值引用类型 int &&,右值引用顾名思义是可以绑定右值的引用。
旧版本的某些情况下,对象在拷贝之后直接销毁了,出现了很多的不必须要的拷贝等问题。
所以,C++11引入了移动操作,右值引用就是为了支持对象移动而产生的。
重点:右值引用其实就是为了偷一个对象的值,而非复制它。
移动构造函数对传入的对象会造成一定程度的破坏。
复习七、指针
定义指针的时候一定养成初始化的习惯,例如:char*p = nullptr;
删除指针内存够一定养成初始默认的习惯,例如:if(p!=nullptr){ delete p;p=nullptr; }
代码示例:
#include
class Buffer {
public:
int i = 99;
char* buf = nullptr;
Buffer() {//构造1、无参构造函数
buf = new char[10];
buf[0] = 0; buf[1] = 1; buf[2] = 2;
std::cout << "Buffer() this = " << this << "\r\n";
}
Buffer(const Buffer& cls_in):i(cls_in.i) {//构造2、拷贝构造(深拷贝)
this->buf = new char[strlen(cls_in.buf)+1];
strcpy_s(this->buf, strlen(cls_in.buf) + 1,cls_in.buf);
std::cout << "Buffer(Buffer& param)" << this << "\r\n";
}
Buffer(Buffer&& cls_in) noexcept :i(cls_in.i) {//构造3、移动构造(移动拷贝)(注意此构造会删除默认赋值运算符函数)
this->buf = cls_in.buf;
cls_in.buf = nullptr;
std::cout << "Buffer(Buffer&& param)" << this << "\r\n";
}
Buffer& operator=(const Buffer& cls_r) {//拷贝赋值运算符函数
if (this == &cls_r) return *this;
this->i = cls_r.i;
this->buf = new char[strlen(cls_r.buf) + 1];
strcpy_s(this->buf, strlen(cls_r.buf) + 1, cls_r.buf);
std::cout << "Buffer& operator=(const Buffer& cls_r)" << "\r\n";
return *this;
}
Buffer& operator=(Buffer&& cls_r) noexcept {
if (this == &cls_r) return *this;
this->i = cls_r.i;
delete[] this->buf; //斩断自己
this->buf = cls_r.buf; //指向别人
cls_r.buf = nullptr;//斩断别人
return *this;
}
~Buffer() {//析构函数
std::cout << "~Buffer() = " << this << "\r\n";
if (buf != nullptr){
delete[] buf;
buf = nullptr;
}
}
};
int main()
{
Buffer cls; cls.i = 9;
Buffer cls1;
cls1 = std::move(cls);
std::cout << cls1.i << "\r\n";
return 0;
}
经过上面的复习,大致可以得出解决办法,那就是使用std::move将原指针掏空就可以了。
int main()
{
std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
auto lamfunc = [](std::unique_ptr pzn) {
std::cout << "子线程的参数clsx的地址 = " << *pzn << ",thread_id =" << std::this_thread::get_id() << "\r\n";
};
std::unique_ptr myp(new int(100)); //创建一个独占式智能指针myp指向一个new出来的int空间。
std::thread mythread(lamfunc,std::move(myp));
mythread.join();
std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
注意,new出来的变量依然是在主线程中new的,属于主线程,子线程中的智能指针依然指向的是属于主线程的
内存,所以这里依然要保持子线程先退出,主线程才能退出。因为主线程退出会释放属于自己的变量。故不能使用mythread.detach();
前四章是单个线程的创建方法和传参时的一些注意点,接下来在以上的基础上进行多个线程创建。
创建十个线程,并放入到容器中
#include
#include
#include
void myprint(int inum) {
std::cout << "Run:线程编号=" << inum <<",线程ID="<< std::this_thread::get_id() << std::endl;
std::cout << "Stop:线程编号=" << inum << ",线程ID=" << std::this_thread::get_id() << std::endl;
}
int main()
{
std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
//--------------------------------------------------------------------------------------------//
std::vector mythreads;//容器
//创建10个线程,入口函数都使用void myprint(int inum)
for (int i = 0; i < 10; i++) {
mythreads.push_back(std::thread(myprint,i));
}
for (auto iter = mythreads.begin(); iter!= mythreads.end(); ++iter) {
iter->join();
}
//--------------------------------------------------------------------------------------------//
std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
//所谓通用型互斥量,就是经常使用的独占式互斥量
#include
#include
#include
#include
#include
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
// 两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
// 另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
//list和vector区别:<百度一下>
class A {
public:
//线程1:接受到玩家命令,插入到一个队列中(写)
void inMsgRecvQueue() {
for (int i = 0; i < 10000; ++i) {
std::cout << "inMsgRecvQueue()执行,插入一个元素" << i << std::endl;
mtx.lock();
msgRecvQueue.push_back(i); //i就是收到的命令
mtx.unlock();
}
}
bool outMsgLULProc(int& command) {
//std::lock_guard sbguard(mtx);//模板类,在析构的时候会调用mtx.unlock()
//可以智能释放锁,但是没有mtx.lock();和mtx.unlock();灵活(具体用法可以百度)
mtx.lock();
if (!msgRecvQueue.empty()) {
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
mtx.unlock();
return true;
}
mtx.unlock();
return false;
}
//线程2:把数据从消息列表中取出的线程(读,删)
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 10000; ++i) {
bool ret = outMsgLULProc(command);
if (ret == true) {//链表非空->取元素成功
std::cout << "outMsgRecvQueue()执行,取出一个元素" << command << std::endl;
}
else {//链表空->取元素失败
std::cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << std::endl;
}
}
}
private:
std::mutex mtx;
std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令
};
int main()
{
std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
//--------------------------------//
A myobj;
std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobj);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
myOutnMsgObj.join();
myInMsgObj.join();
//--------------------------------//
std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
//同一个线程的同一个通用型互斥变量不能锁两次,但现实中有需要锁两次的场景,示例代码如下:
class A {
public:
void testfun1() {
std::lock_guard sbguard(mtx);
//访问共享资源
}
void testfun2() {
std::lock_guard sbguard(mtx);
//访问共享资源
testfun1(); //此处会引发多次上锁异常
}
private:
std::mutex mtx;
};
//单独调用testfun1()和testfun2()时没有问题,但当两个函数互调时就会出现多次上锁的情况,此时程序
//会出现异常,为了解决此场景的问题,引入了递归式互斥量,示例代码如下:
class A {
public:
void testfun1() {
std::lock_guard sbguard(mtx);
//访问共享资源
}
void testfun2() {
std::lock_guard sbguard(mtx);
testfun1(); //此处不会触发异常
}
private:
std::recursive_mutex mtx;
};
//说明1:std::recursive_mutex虽然可以多次锁定,但其执行效率没有std::mutex高。
//说明2:std::recursive_mutex的递归上锁次数有限制,超过限制时会触发异常。
//说明3:std::lock_guard<>是个类模板,用于构建区域锁,即对象创建构造时对传入的互斥量上锁,对象析构时对传入的互斥量解锁,用于实现自动释放锁的逻辑。
//所谓时间式互斥量就是带有超时功能的互斥量
//其有两个重要的方法:
//1、try_lock_for(); //参数是一个超时时间
//2、try_lock_unti(); //参数是未来的一个时间点
/***************************************************************************************/
//示例1:try_lock_for()的用法
#include
#include
#include
#include
#include
class A {
public:
//线程1:接受到玩家命令,插入到一个队列中(写)
void inMsgRecvQueue() {
for (int i = 0; i < 10000; ++i) {
std::chrono::milliseconds timeout(100);
if (tim_mtx.try_lock_for(timeout)) {//100ms内上锁成功
std::cout << "in...()拿锁成功,插入元素" << i <<"成功"<< "\r\n";
msgRecvQueue.push_back(i);
tim_mtx.unlock();
}
else {//100ms内上锁未成功,超时了
std::cout << "in...()拿锁失败,未插元素" << i << "失败" << "\r\n";
std::chrono::milliseconds dura(100);
std::this_thread::sleep_for(dura);
}
}
}
bool outMsgLULProc(int& command) {
tim_mtx.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
if (!msgRecvQueue.empty()) {
command = msgRecvQueue.front();
//执行相应操作....
msgRecvQueue.pop_front();
tim_mtx.unlock();
return true;
}
tim_mtx.unlock();
return false;
}
//线程2:把数据从消息列表中取出的线程(读,删)
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 10000; ++i) {
bool ret = outMsgLULProc(command);
if (ret == true) {//链表非空->取元素成功
std::cout << "out...()执行,取出元素" << command << std::endl;
}
else {//链表空->取元素失败
std::cout << "out...()执行,但目前消息队列为空"<< std::endl;
}
}
}
private:
std::timed_mutex tim_mtx;//时间式互斥量
std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令
};
int main()
{
std::cout << "The main start,thread_id = " << std::this_thread::get_id() << "\r\n";
//--------------------------------//
A myobj;
std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobj);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
myOutnMsgObj.join();
myInMsgObj.join();
//--------------------------------//
std::cout << "The main end,thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
/***************************************************************************************/
//示例2:try_lock_unti()的用法
#include
#include
#include
#include
#include
class A {
public:
//线程1:接受到玩家命令,插入到一个队列中(写)
void inMsgRecvQueue() {
for (int i = 0; i < 10000; ++i) {
std::chrono::milliseconds timeout(100);
if (tim_mtx.try_lock_until(std::chrono::steady_clock::now()+ timeout)) {//到时间点(当前时间偏移100ms)、拿锁成功
std::cout << "in...()拿锁成功,插入元素" << i <<"成功"<< "\r\n";
msgRecvQueue.push_back(i);
tim_mtx.unlock();
}
else {//100ms内上锁未成功,超时了
std::cout << "in...()拿锁失败,未插元素" << i << "失败" << "\r\n";
std::chrono::milliseconds dura(100);
std::this_thread::sleep_for(dura);
}
}
}
bool outMsgLULProc(int& command) {
tim_mtx.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
if (!msgRecvQueue.empty()) {
command = msgRecvQueue.front();
//执行相应操作....
msgRecvQueue.pop_front();
tim_mtx.unlock();
return true;
}
tim_mtx.unlock();
return false;
}
//线程2:把数据从消息列表中取出的线程(读,删)
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 10000; ++i) {
bool ret = outMsgLULProc(command);
if (ret == true) {//链表非空->取元素成功
std::cout << "out...()执行,取出元素" << command << std::endl;
}
else {//链表空->取元素失败
std::cout << "out...()执行,但目前消息队列为空"<< std::endl;
}
}
}
private:
std::timed_mutex tim_mtx;//时间式互斥量
std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令
};
int main()
{
std::cout << "The main start,thread_id = " << std::this_thread::get_id() << "\r\n";
//--------------------------------//
A myobj;
std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobj);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
myOutnMsgObj.join();
myInMsgObj.join();
//--------------------------------//
std::cout << "The main end,thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
有两个锁A、B:
线程1的执行流程: =>锁A=>锁B=>执行代码=>释放锁
线程1的执行流程: =>锁B=>锁A=>执行代码=>释放锁
结果:两个线程都在无限期的等待对方释放锁,导致死锁。
示例一:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include
#include
#include
#include
#include
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
// 两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
// 另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
class A {
public:
//线程1:接受到玩家命令,插入到一个队列中(写)
void inMsgRecvQueue() {
for (int i = 0; i < 10000; ++i) {
std::cout << "inMsgRecvQueue()执行,插入一个元素" << i << std::endl;
mtx1.lock();
mtx2.lock();
msgRecvQueue.push_back(i); //i就是收到的命令
mtx1.unlock();
mtx2.unlock();
}
}
bool outMsgLULProc(int& command) {
//std::lock_guard sbguard(mtx);//模板类,在析构的时候会调用mtx.unlock()
//可以智能释放锁,但是没有mtx.lock();和mtx.unlock();灵活(具体用法可以百度)
mtx2.lock();
mtx1.lock();
if (!msgRecvQueue.empty()) {
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
mtx1.unlock();
mtx2.unlock();
return true;
}
mtx1.unlock();
mtx2.unlock();
return false;
}
//线程2:把数据从消息列表中取出的线程(读,删)
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 10000; ++i) {
bool ret = outMsgLULProc(command);
if (ret == true) {//链表非空->取元素成功
std::cout << "outMsgRecvQueue()执行,取出一个元素" << command << std::endl;
}
else {//链表空->取元素失败
std::cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << std::endl;
}
}
}
private:
std::mutex mtx1;
std::mutex mtx2;
std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令
};
int main()
{
std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
//-------------------//
A myobj;
std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobj);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
myOutnMsgObj.join();
myInMsgObj.join();
//-------------------//
std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
示例2:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
std::lock_guard sbguard(mtx);//这是一个模板类,<区域锁>
对象sbguard会在构造函数时调用mtx.lock()
对象sbguard会在构造函数时调用mtx.unlock()
此类可以省去在哪个位置解锁,但是由于其释放锁是在析构时,所以不是很灵活。
但是可以使代码更整洁,其实时牺牲灵活性和效率,来保证代码可读性。
死锁解决办法1:两个线程按照相同的顺序对互斥量上锁。
线程1:
mtx1.lock();
mtx2.lock();
//执行....
mtx1.unlock();
mtx2.unlock();
线程2:
mtx1.lock();
mtx2.lock();
//执行....
mtx1.unlock();
mtx2.unlock();
死锁解决办法2:使用std::lock()让程序自动决定上锁顺序。
线程1:
std::lock(mtx1,mtx2);
//执行....
mtx1.unlock();
mtx2.unlock();
线程2:
std::lock(mtx1,mtx2);
//执行....
mtx1.unlock();
mtx2.unlock();
死锁解决办法3:当程序的分支过多时unlock容易误写,使用模板类解决。
#include
#include
#include
#include
#include
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
// 两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
// 另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
class A {
public:
//线程1:接受到玩家命令,插入到一个队列中(写)
void inMsgRecvQueue() {
for (int i = 0; i < 10000; ++i) {
std::cout << "inMsgRecvQueue()执行,插入一个元素" << i << std::endl;
std::lock(mtx1,mtx2);
std::lock_guard sbguard1(mtx1,std::adopt_lock);//std::adopt_lock构造函数执行时,不调用lock
std::lock_guard sbguard2(mtx2,std::adopt_lock);
msgRecvQueue.push_back(i); //i就是收到的命令
//mtx1.unlock();
//mtx2.unlock();
}
}
bool outMsgLULProc(int& command) {
//std::lock_guard sbguard(mtx);//模板类,在析构的时候会调用mtx.unlock()
//可以智能释放锁,但是没有mtx.lock();和mtx.unlock();灵活(具体用法可以百度)
std::lock(mtx1, mtx2);
std::lock_guard sbguard1(mtx1, std::adopt_lock);
std::lock_guard sbguard2(mtx2, std::adopt_lock);
if (!msgRecvQueue.empty()) {
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
//mtx1.unlock();
//mtx2.unlock();
return true;
}
//mtx1.unlock();
//mtx2.unlock();
return false;
}
//线程2:把数据从消息列表中取出的线程(读,删)
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 10000; ++i) {
bool ret = outMsgLULProc(command);
if (ret == true) {//链表非空->取元素成功
std::cout << "outMsgRecvQueue()执行,取出一个元素" << command << std::endl;
}
else {//链表空->取元素失败
std::cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << std::endl;
}
}
}
private:
std::mutex mtx1;
std::mutex mtx2;
std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令
};
int main()
{
std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
//--------------------------//
A myobj;
std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobj);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
myOutnMsgObj.join();
myInMsgObj.join();
//--------------------------//
std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
总结:std::lock(mtx1, mtx2); //一次锁定多个互斥量,并自动决定上锁顺序(很少用)
std::lock_guard
std::lock_guard
建议:还是使用mtx.lock();、mtx.unlock();的形式。
互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域,也就是使用细粒度锁。
std::lock_guard:区域锁
std::unique_lock:独占式锁
两者具体区别和优缺点可以自行百度,unique_lock可以完全取代区域锁,但是有缺点。
用法一、lock_guard与unique_lock的替换
std::lock_guard sbguard(mtx);
可以替换为.....
std::unique_lock sbguard(mtx);
/*==================================================================*/
用法二、lock_guard与unique_lock的替换
mtx.lock();
std::lock_guard sbguard(mtx,std::adopt_lock);
可以替换为.....
mtx.lock();
std::unique_lock sbguard(mtx, std::adopt_lock);
//注意:std::adopt_lock:代表传入的互斥量已经被加锁了
/*==================================================================*/
用法三、引出问题
线程1:
mtx.lock();
std::lock_guard sbguard(mtx,std::adopt_lock);
线程2:
mtx.lock();
std::lock_guard sbguard(mtx,std::adopt_lock);
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
//注意:线程2的延时5s,不快速释放锁,导致线程1的上锁阻塞了5s
//解决办法,线程1先尝试上锁,上锁不成功,立即返回,不阻塞。<见用法四>
/*==================================================================*/
用法四、使用try_to_lock形参=>尝试上锁
线程1:
std::unique_lock sbguard(mtx, std::try_to_lock);//尝试加锁
if (sbguard.owns_lock()) {//加锁成功
//操作共享数据...
}
else {//加锁失败
//处理其他的事...
}
线程2:
mtx.lock();//上锁
std::unique_lock sbguard(mtx,std::adopt_lock);//区域锁
std::this_thread::sleep_for(std::chrono::milliseconds(10000));//延时10s
//操作共享资源...
//注意:使用try_to_lock时,自己不能提前去加锁,否则会卡死
/*==================================================================*/
用法五、使用defer_lock形参
线程1:
std::unique_lock sbguard(mtx, std::defer_lock);//绑定一个未初始化的锁
sbguard.lock();//需要手动初始化
//操作共享资源...
线程2:
mtx.lock();//上锁
//操作共享资源...
mtx.unlock();//解锁
//注意:这些类模板的作用,都是绑定互斥量,在原来互斥量的基础上加了一些方法而已
//比如类模板会在对象析构时对传入的互斥量解锁等等,也能通过对象手动的对传入的互斥量上锁解锁等
/*==================================================================*/
用法六、调用方法try_lock(),用法类似形参try_to_lock
//在用法五的基础上修改
线程1:
std::unique_lock sbguard(mtx,std::defer_lock);
if (sbguard.try_lock() == true) {
//操作共享资源...
}
else {
//处理其他的事...
}
线程2:
mtx.lock();//上锁
//操作共享资源...
mtx.unlock();//解锁
//注意:不用模板类绑定互斥量,原互斥量也可以使用此方法(注意使用unlock())
/*==================================================================*/
用法七、调用方法release(),返回模板类对象绑定的mutex指针,<解除unique_lock与mutex的绑定>
//如果mutex已经加锁,接管过来之后,还是加锁状态
线程1:
std::unique_lock sbguard(mtx);//sbguard与mtx绑定
std::mutex* p_mtx = sbguard.release();//sbguard与mtx分离
//操作共享资源...
p_mtx->unlock();
线程2:
mtx.lock();//上锁
//操作共享资源...
mtx.unlock();//解锁
/*==================================================================*/
说明1:std::adopt_lock:标识传入的互斥量已经上锁了。
说明2:std::defer_lock:标识传入的互斥量还没有上锁。
说明3:std::try_to_lock:标识对传入的互斥量尝试上锁,需要手工加锁。
说明4:lock()和unlock()之间的代码的段的大小,叫做锁的粒度,粒度越细越好。
说明5:同一个mutex不能绑定两个模板类对象。
说明6:换绑定,可以使用std::move将一个unique_lock对象里的mutex掏出来与另一个绑定在一起。
说明7:std::unique_lock类型的函数,返回值时,调用的是移动拷贝构造函数。
<前言:单例模式只是设计模式的一种,具体的其他模式还是需要学习的,因为是C++的知识,所以其他的设计模式,可以通过C++书籍或者百度等工具自行学习,这里只说单例模式>
所谓单例模式,就是设定一种类,其只能实例化一个对象,创建代码如下:
#include
#include
#include
#include
#include
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
// 两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
// 另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
class MyCAS {
public:
static MyCAS* GetInstance() {
if (m_instance == NULL) {
m_instance = new MyCAS();
static CGarhuishou cl;
}
return m_instance;
}
class CGarhuishou {//类中套类,用来释放对象
public:
~CGarhuishou() {
if (MyCAS::m_instance) {
delete MyCAS::m_instance;
MyCAS::m_instance = NULL;
}
}
};
void prt() {
std::cout << "hello" << std::endl;
}
private:
MyCAS() {//私有化构造函数
std::cout << "构造执行" << std::endl;
}
~MyCAS() {//私有析构函数
std::cout << "析构执行" << std::endl;
}
static MyCAS* m_instance;//静态成员变量
};
MyCAS* MyCAS::m_instance = NULL;//静态成员初始化
int main()
{
MyCAS* cls = MyCAS::GetInstance(); //cls与cls1指向的是同一个对象(只能实例化一个对象)
MyCAS* cls1 = MyCAS::GetInstance();
return 0;
}
多个线程中实例化单例类,会导致共享资源冲突,产生两个对象的产生,需要做如下保护:
#include
#include
#include
#include
#include
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
// 两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
// 另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
std::mutex resource_mutex;
class MyCAS {
public:
//GetInstance()函数是被频繁被调用的,而m_instance的冲突只会发生在第一次创建的时候,后续不需要再互斥了
//当第一次创建完成后,之后如果再调用GetInstance()都加锁的话,效率会低,故使用双重加锁模式,只在第一次创建时使用锁
static MyCAS* GetInstance() {
if (m_instance == NULL){//双重锁定<第一重>
std::unique_lock mymutex(resource_mutex);//
if (m_instance == NULL) {//双重锁定<第二重>
m_instance = new MyCAS();
static CGarhuishou cl;
}
}
return m_instance;
}
class CGarhuishou {//类中套类,用来释放对象
public:
~CGarhuishou() {
if (MyCAS::m_instance) {
delete MyCAS::m_instance;
MyCAS::m_instance = NULL;
}
}
};
void prt() {
std::cout << "hello" << std::endl;
}
private:
MyCAS() {//私有化构造函数
std::cout << "构造执行" << std::endl;
}
~MyCAS() {//私有析构函数
std::cout << "析构执行" << std::endl;
}
static MyCAS* m_instance;//静态成员变量
};
MyCAS* MyCAS::m_instance = NULL;//静态成员初始化
void mythread(){//线程入口函数
std::cout << "我的线程开始执行了" << std::endl;
MyCAS* pa = MyCAS::GetInstance();
std::cout << "我的线程执行完毕了" << std::endl;
}
int main()
{
std::thread mytobj1(mythread);//线程1
std::thread mytobj2(mythread);//线程2
mytobj1.join();
mytobj2.join();
return 0;
}
std::call_once():C++11引入,第二的形参是一个函数名,作用是保证传入的函数只被调用一次。
=>std::call_once()具备互斥的能力,而且效率上比互斥量消耗的资源更少,需要与一个标记std::once_flag结合使用,std::once_flag是一个结构,通过std::once_flag决定函数是否执行,函数调用成功后,std::once_flag会更改为另一个状态。示例代码如下:
std::once_flag g_flag;//定义一个未被置位的标记
class MyCAS {
private:
static void CreateInstance() {//只会被执行一次的函数
m_instance = new MyCAS();
static CGarhuishou cl;
}
public:
//GetInstance()函数是被频繁被调用的,而m_instance的冲突只会发生在第一次创建的时候,后续不需要再互斥了
//当第一次创建完成后,之后如果再调用GetInstance()都加锁的话,效率会低,故使用双重加锁模式,只在第一次创建时使用锁
static MyCAS* GetInstance() {
//if (m_instance == NULL){//双重锁定<第一重>
// std::unique_lock mymutex(resource_mutex);//
// if (m_instance == NULL) {//双重锁定<第二重>
// m_instance = new MyCAS();
// static CGarhuishou cl;
// }
//}
std::call_once(g_flag,CreateInstance);
return m_instance;
}
class CGarhuishou {//类中套类,用来释放对象
public:
~CGarhuishou() {
if (MyCAS::m_instance) {
delete MyCAS::m_instance;
MyCAS::m_instance = NULL;
}
}
};
void prt() {
std::cout << "hello" << std::endl;
}
private:
MyCAS() {//私有化构造函数
std::cout << "构造执行" << std::endl;
}
~MyCAS() {//私有析构函数
std::cout << "析构执行" << std::endl;
}
static MyCAS* m_instance;//静态成员变量
};
MyCAS* MyCAS::m_instance = NULL;//静态成员初始化
void mythread(){//线程入口函数
std::cout << "我的线程开始执行了" << std::endl;
MyCAS* pa = MyCAS::GetInstance();
std::cout << "我的线程执行完毕了" << std::endl;
}
int main()
{
std::thread mytobj1(mythread);//线程1
std::thread mytobj2(mythread);//线程2
mytobj1.join();
mytobj2.join();
return 0;
}
//说明1:std::call_once()类似于双重锁定保证函数只被执行一次,实现的机制还是互斥。
//说明2:std::call_once()的效率比直接互斥高,比双重锁定的互斥略低一点。
条件变量
<前言:以5.4中的程序为例,取出消息的线程中,循环检测队列中是否有消息,当没有消息时,线程再空转,实在是一种损耗。为了优化这个问题,让插入消息的线程通知取消息的线程就行了,没有收到消息,让其睡眠,有点像QT中的信号与槽机制>
std::condition_variable是一个模板类,其与互斥量组合使用。
std::unique_lock+std::mutex+std::condition_variable+wait()+notify_one()/notify_all()的使用,示例代码如下:
#include
#include
#include
#include
#include
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
// 两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
// 另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
class A {
public:
//线程1:接受到玩家命令,插入到一个队列中(写)
void inMsgRecvQueue() {
for (int i = 0; i < 10000; ++i) {
std::cout << "inMsgRecvQueue()执行,插入一个元素" << i << std::endl;
std::unique_lock sbguard(mtx);//sbguard与mtx绑定
msgRecvQueue.push_back(i); //再末尾插入一个元素,i就是收到的命令
my_cond.notify_one();//尝试唤醒调用my_cond.wait()的那个线程
}
}
void outMsgRecvQueue() {
int command = 0;
while (true) {
std::unique_lock sbguard(mtx);
//wait()用来等一个东西
//如果第二的参数即lambda表达式返回值是true,直接返回。
//如果第二的参数即lambda表达式返回值是false,wait将解锁互斥量,并阻塞到本行,直到其他的某个线程调用notify_one()将其唤醒。
//如果wait()没有第二个参数,wait将解锁互斥量,并直接阻塞到本行,并等待其他的某个线程调用notify_one()将其唤醒。
//没有第二参数时,当wait()被唤醒时,它首先会不断的尝试重新获取互斥量,直到获取到之后,跳出wait(),往下执行。
//当有第二参数时,当wait()被唤醒时,它首先会不断的尝试重新获取互斥量,直到获取到之后,执行lambda表达式...
//...如果lambda表达式返回false=>解锁,睡眠,如果lambda表达式返回true,跳出wait(),往下执行
my_cond.wait(sbguard, [this]{
if (!msgRecvQueue.empty())
return true;
return false;
});
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
sbguard.unlock();//由于unique_lock的灵活性,可以提前解锁
std::cout << "outMsgRecvQueue()执行,取出一个元素" << command << std::endl;
}
}
private:
std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令
std::mutex mtx;//生成一个互斥量
std::condition_variable my_cond;//生成一个条件变量对象
};
int main()
{
A myobj;
std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobj);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
myOutnMsgObj.join();
myInMsgObj.join();
return 0;
}
//说明1:线程1执行完毕退出会导致线程2一直卡在那进行死等,这个是有缺陷的,但是这个缺陷很容易解决
比如:规定一个command是退出命令,线程1在退出时插入这个退出命令并notify_one(),并线程二收到这个命令就退出循环。这样线程2就不会死等了。<解决死等方法很多,所以不是个事>
//说明2:如过第二个线程没有阻塞到wait(),线程1调用notify_one()其实是失效的,因为线程本身是唤醒的。<也可叫虚假唤醒>
//说明3:如果有如下两个线程,notify_one()一次只能唤醒其中的一个
std::thread myOutnMsgObj1(&A::outMsgRecvQueue, &myobj); //这两个线程用的一个函数
std::thread myOutnMsgObj2(&A::outMsgRecvQueue, &myobj);
//说明4:如果有如下两个线程,你想唤醒一次唤醒两个,可以用notify_all()
std::thread myOutnMsgObj1(&A::outMsgRecvQueue, &myobj);
std::thread myOutnMsgObj2(&A::outMsgRecvQueue, &myobj);
<1>、使用std::async()创建异步线程/任务:
//std::async、std::future:创建后台任务并返回值
//希望线程返回一个结果
//std::async 是个函数模板,用来启动一个异步任务,启动后返回一个std::future对象,std::future是个类模板。
//启动异步任务:自动创建一个线程并开始执行对应的县城入口函数,它返回一个std::future对象,
//.............这个std::future对象里边就含有县城入口函数所返回的结果(线程返回的结果),我们可以通过调用future对象的成员函数get()来获取结果。
//.............std::future提供了一种访问异步操作结果的机制,即这个结果你不能马上拿到,但在将来可以拿到。
//*************************************************************************************//
示例一:普通函数做std::async()的参数
#include
#include
#include
#include
#include
#include
int mythread() {
std::cout << "mythread() Start th_id = " <
#include
#include
#include
#include
#include
class A {
public:
int mythread(int mypar) {
std::cout << "=" << mypar << std::endl;
std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
std::chrono::milliseconds dura(5000);//延时5s
std::this_thread::sleep_for(dura);
std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
return 5;
}
};
int main()
{
A myobj;
int tmpar = 12;
std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
std::future result = std::async(&A::mythread,&myobj, tmpar);//创建一个线程并开始执行(result与线程绑在了一起)
std::cout << "continue...!" << std::endl;
int def=0;
std::cout <<"=" << result.get() << std::endl;//阻塞到这里,并等待mythread执行完成返回
return 0;
}
//说明1:如果你不调用result.get(),主线程依然会等待mythread执行完毕后再退出(why? 因为类析构)
//*************************************************************************************//
示例三、枚举std::lunnch类型的使用(延迟创建线程)
#include
#include
#include
#include
#include
#include
class A {
public:
int mythread(int mypar) {
std::cout << "=" << mypar << std::endl;
std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
std::chrono::milliseconds dura(5000);//延时5s
std::this_thread::sleep_for(dura);
std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
return 5;
}
};
int main()
{
A myobj;
int tmpar = 12;
std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
//std::launch::deferred参数表示线程会被延迟创建,当你调用std::future的wait()护额get()时,线程才会创建并执行
//如果你不调用wait()和get()时,线程不会执行
//std::future result = std::async(&A::mythread, &myobj, tmpar);
//std::future result = std::async(std::launch::async,&A::mythread,&myobj, tmpar);
std::future result = std::async(std::launch::deferred,&A::mythread,&myobj, tmpar);
std::chrono::milliseconds dura(1000);
std::this_thread::sleep_for(dura);
std::cout << "continue...!" << std::endl;
int def=0;
std::cout <<"=" << result.get() << std::endl;//阻塞到这里,并等待mythread执行完成返回
return 0;
}
//说明1:std::launch::deferred其实是没啥用的,当你使用它时,线程根本不创建,而且即使你调用了wait()和get(),也不会创建新线程,是在主线程中直接串行执行,不信的话可以打印线程ID查看。
//说明2:std::launch::async是缺省输入,如果第一个参数不设置,就默认使用这个参数。
<2>、std::async()和std::thread创建线程的区别
std::async()创建的线程,一般叫创建异步任务,有时候并不创建线程,比std::launch::deferred
形参不仅会延迟执行,而且还不创建新的线程。重点是缺省时,创不创建新线程由系统的决定权由系统决定,这样系统在资源不够的情况下,不创建新线程(同步线程)。注意缺省时的默人参数是std::launch::asyn|std::launch::deferred,即创不创建新线程由系统决定。
std::thread创建线程时,如果系统的资源紧张,创建线程可能失败,造成系统崩溃的情况。
std::thread创建线程的返回值不好获取,没有std::async()创建的线程的返回值好获取。
一个程序中,线程数量不要超100-200个,因为不是线程越多,效率就越高,有一个极值点。
std::async()创建的任务到底是同步的还是异步的,可以通过std::future_status对象获取。
//std::packaged_task:打包任务,把任务包装起来,方便将来作为线程入口函数。
//std::packaged_task是个类模板,他的模板参数是各种可调用对象。
MFC中的控件常常与一个对象绑定到一起,操作对象就可以更改控件的显示,QT中控件直接是一个对象,这些概念看似很普通,但都是围绕一个"万物皆对象的"思想,即面向对象编程,在C++线程中,线程函数等等也可以绑定到一个对象中(也叫封装到对象中),用操作对象的方式操作函数。
//示例1:把线程函数封装到std::packaged_task的对象中
#include
#include
#include
#include
#include
#include
//std::packaged_task:打包任务,把任务包装起来,方便将来作为线程入口函数。
//std::packaged_task是个类模板,他的模板参数是各种可调用对象。
int mythread(int mypar) {
std::cout << "=" << mypar << std::endl;
std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
std::chrono::milliseconds dura(5000);//延时5s
std::this_thread::sleep_for(dura);
std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
return 5;
}
int main()
{
//把线程函数与一个<对象/变量>绑定,用操作<对象/变量>的方式使用线程函数,也算是符合万物皆是对象的概念
std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
std::packaged_task mypt(mythread);//把线程函数包装到一个packaged_task对象中
std::thread t1(std::ref(mypt),1);
t1.join();
std::future result = mypt.get_future();
std::cout << "result = " << result.get() << std::endl;
return 0;
}
/***************************************************************************************/
//示例2:把lambda表达式封装到std::packaged_task的对象中
#include
#include
#include
#include
#include
#include
int main()
{
std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
std::packaged_task mypt([](int mypar) {
std::cout << "=" << mypar << std::endl;
std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
std::chrono::milliseconds dura(5000);//延时5s
std::this_thread::sleep_for(dura);
std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
return 5;
});
std::thread t1(std::ref(mypt),1);
t1.join();
std::future result = mypt.get_future();
std::cout << "result = " << result.get() << std::endl;
return 0;
}
/***************************************************************************************/
示例3:std::packaged_task的对象也可以直接被调用
#include
#include
#include
#include
#include
#include
int main()
{
std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
std::packaged_task mypt([](int mypar) {
std::cout << "=" << mypar << std::endl;
std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
std::chrono::milliseconds dura(5000);//延时5s
std::this_thread::sleep_for(dura);
std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
return 5;
});
mypt(105); //std::packaged_task对象也可以直接被调用(相当于函数),不创建线程
std::future result = mypt.get_future();
std::cout << "result = " << result.get() << std::endl;
return 0;
}
/***************************************************************************************/
示例4:把std::packaged_task对象放入容器中
#include
#include
#include
#include
#include
#include
std::vector < std::packaged_task > mytasks; // 定义一个容器
int main()
{
std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
std::packaged_task mypt([](int mypar) {
std::cout << "=" << mypar << std::endl;
std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
std::chrono::milliseconds dura(5000);//延时5s
std::this_thread::sleep_for(dura);
std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
return 5;
});
//把对象移动到容器mytasks中
mytasks.push_back(std::move(mypt));
//从容器中mytasks取出
std::packaged_task mypt2;
auto iter = mytasks.begin();
mypt2 = std::move(*iter);
mytasks.erase(iter);//删除iter指向的元素,此时iter失效,后续不可以再使用iter
//执行
mypt2(521);
std::future result = mypt2.get_future();
std::cout << "result = " << result.get() << std::endl;
return 0;
}
/***************************************************************************************/
一个线程等待获取另一个线程的计算结果:
#include
#include
#include
#include
#include
#include
//std::promise,类模板,可实现在某个线程中给其赋值,在其他线程中读它。
void mythread(std::promise &tmpp,int calc) {
//做一系列复杂的操作
calc++;
calc *= 10;
std::chrono::milliseconds dura(5000);//延时5s
std::this_thread::sleep_for(dura);
//计算出结果了
int result = calc;//读取结果
tmpp.set_value(result);//把结果保存到tmpp中
return;
}
void mythread2(std::future& tmpf,std::thread &th) {
auto result = tmpf.get();
std::cout << "result = " << result << ",id=" << std::this_thread::get_id() << std::endl;
th.join();
return;
}
int main()
{
std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
//线程1
std::promise myprom;
std::thread th(mythread, std::ref(myprom),1);//创建线程1
std::future ful = myprom.get_future();//promise和future绑定,用于获取线程返回值
//线程2
std::thread th2(mythread2,std::ref(ful),std::ref(th));//创建线程2,在线程2中等待线程1的结果
th2.join();
return 0;
}
一个线程获取另一个线程的运算结果,必须等另一个线程执行完才能获取到,现实情况中,由于系统是非实时的,所以其每次执行完毕的所耗时间是不定的、浮动的,如果线程一直迟迟运行不完,我们也不能一直等待(等待是阻塞的),所以用wait_for()方法,每等待相应的时间就判断一下对方的状态,这样更灵活的等待对方的结果。也可以在对方是否超时做出相应处理。示例代码如下:
//示例:以std::async()为例
#include
#include
#include
#include
#include
#include
int mythread() {
std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
std::chrono::milliseconds dura(5000);//延时5s
std::this_thread::sleep_for(dura);
std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
return 5;
}
int main()
{
std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
std::future result = std::async(mythread);//创建一个线程并开始执行(result与线程绑在了一起)
std::cout << "continue...!" << std::endl;
int i = 0;
while (true) {
std::future_status status = result.wait_for(std::chrono::seconds(1));//周期1s唤醒自己,查看一次对方是否执行完毕
std::cout << "第" <<++i<<"次唤醒:";
if (status == std::future_status::ready) {
std::cout << "线程执行完毕=>";
std::cout << " result.get()= " << result.get() << std::endl;
break;
}
else if (status == std::future_status::timeout) {
std::cout << "线程未执行完毕" << std::endl;
}
else if (status == std::future_status::deferred) {
std::cout << "线程被延迟执行" << std::endl;
}
}
return 0;
}
在前面的历程中我们知道std::future<> 的get()方法只能被调用一次,因为其是一个移动语义,如果移动两次,肯定会出问题,这就限制了只能由一个线程中调用get()去获取某个线程的计算结果,为了解决这个限制(即多个线程都要获取某个线程的结果),引入了std::shared_future<>,代码如下:
//std::shared_future<>的get()方法使用的不是移动语义,而是复制语义,故可以get()两次
#include
#include
#include
#include
#include
#include
void mythread(std::promise& tmpp, int calc) {
//做一系列复杂的操作
calc++;
calc *= 10;
std::chrono::milliseconds dura(5000);//延时5s
std::this_thread::sleep_for(dura);
//计算出结果了
int result = calc;//读取结果
tmpp.set_value(result);//把结果保存到tmpp中
return;
}
void mythread2(std::shared_future& tmpf, std::thread& th) {
auto result = tmpf.get();//get()只能被调用一次,因为其是个移动语义,所以不能移动第二次
std::cout << "result = " << result << ",id=" << std::this_thread::get_id() << std::endl;
//printf("result=%d,th_id=%d\r\n", result, std::this_thread::get_id());
if (th.joinable()){th.join();}
}
void mythread3(std::shared_future& tmpf, std::thread& th) {
auto result = tmpf.get();//get()只能被调用一次,因为其是个移动语义,所以不能移动第二次
std::cout << "result = " << result << ",id=" << std::this_thread::get_id() << std::endl;
//printf("result=%d,th_id=%d\r\n", result, std::this_thread::get_id());
if (th.joinable()) { th.join(); }
}
int main()
{
std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
//线程1
std::promise myprom;
std::thread th(mythread, std::ref(myprom), 1);//创建线程1
std::shared_future sful = myprom.get_future();//promise和future绑定,用于获取线程返回值
//线程2、3
std::thread th2(mythread2, std::ref(sful), std::ref(th));//创建线程2,在线程2中等待线程1的结果
std::thread th3(mythread3, std::ref(sful), std::ref(th));//创建线程3,在线程3中等待线程1的结果
//join线程2、3
th2.join(); th3.join();
return 0;
}
//说明1:std::future也可以转为std::shared_future,示例如下
// std::future ful = myprom.get_future();
// std::shared_future sfulx(ful.share());//ful.share()也是移动语义
//说明2:std::future和std::shared_future的valid()方法可以判断其对象里的值是否有效
// 例如:if(sful.valid()){ }//判断sful变量是否被std::move()移动过
//示例1:整形原子操作
#include
#include
#include
#include
#include
#include
//原子操作:std::atomic(类模板)、不可分割的操作 <针对简单的操作>
//互斥量:多线程中,保护共享数据:锁=>保护共享数据=>开锁 <针对复杂的操作>
/*
* 原子1:使用原子汇编码,代码粒度小于中断的的代码。
* 原子2:使用开关中断,暂时屏蔽中断从而不让系统调度,就可以实现原子性。
* 以上是两种原子性。这样的代码的执行叫原子操作,它是一种无锁技术,效率上比互斥量好。
* 但原子操作是很简单的,能力一般。而互斥量可以互斥整个代码段,所以各有优缺点。
*/
std::atomic g_mycount = 0; //定义一个int原子对象
void mythread() {
for ( int i = 0;i < 1000000;i++){
g_mycount++; //原子操作
//g_mycount+=1; //原子操作
//g_mycount=g_mycount+1;//不是原子操作
//结论:不是所有的操作符都是原子性的,一般针对++、--、+=、&=、|=、^=是原子性的
}
int main()
{
std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
std::thread th1(mythread);//创建线程1
std::thread th2(mythread);//创建线程2
th1.join(); th2.join();
std::cout << "g_mycount=" << g_mycount << std::endl;
return 0;
}
//*************************************************************************************//
//示例2:布尔型原子操作
#include
#include
#include
#include
#include
#include
std::atomic g_ifend = true;//定义一个bool原子对象
void mythread() {
std::chrono::milliseconds dura(1000);
while (g_ifend) {//原子操作
std::cout << "thread_id = " << std::this_thread::get_id() << "运行中" << std::endl;
std::this_thread::sleep_for(dura);
}
std::cout << "thread_id = " << std::this_thread::get_id() << "运行结束" << std::endl;
}
int main()
{
std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
std::thread th1(mythread);//创建线程1
std::thread th2(mythread);//创建线程2
std::chrono::milliseconds dura(5000);//主线程休息5s
std::this_thread::sleep_for(dura);
g_ifend=false;//原子操作、让线程退出循环
th1.join(); th2.join();
std::cout << "主线程结束"<< std::endl;
return 0;
}
//说明1:原子操作项目中一般不常用。
//说明2:一般用于计数,比如数据包的统计。
//说明3:代码一定要稳定,给公司写项目时一定要用自己拿得准的的代码(稳定首位)。
//*************************************************************************************//
临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
互斥量:为协调共同对一个共享资源的单独访问而设计的。
信号量:为控制一个具有有限数量用户资源而设计。
事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
/***************************************************************************************/
//示例代码1:临界区的简单使用
#include
#include
#include
#include
#include
#include //不属于C++库,此库很大,一般使用预编译
#define __WINDOWSJQ_
//windows临界区
class A {
public:
//线程1:接受到玩家命令,插入到一个队列中(写)
void inMsgRecvQueue() {
for (int i = 0; i < 10000; ++i) {
std::cout << "inMsgRecvQueue()执行,插入一个元素" << i << std::endl;
#ifdef __WINDOWSJQ_
EnterCriticalSection(&my_winsec);//进入临界区
msgRecvQueue.push_back(i); //i就是收到的命令
LeaveCriticalSection(&my_winsec);//离开临界区
#else
mtx.lock();
msgRecvQueue.push_back(i); //i就是收到的命令
mtx.unlock();
#endif
}
}
bool outMsgLULProc(int& command) {
#ifdef __WINDOWSJQ_
EnterCriticalSection(&my_winsec);//进入临界区
if (!msgRecvQueue.empty()) {
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
LeaveCriticalSection(&my_winsec);//离开临界区
return true;
}
LeaveCriticalSection(&my_winsec);//离开临界区
#else
mtx.lock();
if (!msgRecvQueue.empty()) {
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
mtx.unlock();
return true;
}
mtx.unlock();
#endif
return false;
}
//线程2:把数据从消息列表中取出的线程(读,删)
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 10000; ++i) {
bool ret = outMsgLULProc(command);
if (ret == true) {//链表非空->取元素成功
std::cout << "outMsgRecvQueue()执行,取出一个元素" << command << std::endl;
}
else {//链表空->取元素失败
std::cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << std::endl;
}
}
}
//构造函数
A() {
#ifdef __WINDOWSJQ_
InitializeCriticalSection(&my_winsec);//初始化临界区
#endif
}
private:
std::mutex mtx;
std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令
//------创建windows临界区
#ifdef __WINDOWSJQ_ //windows中的临界区,非常类似于C++11中的mutex
CRITICAL_SECTION my_winsec;//临界区必须初始化,在构造函数中初始化
#endif
};
int main()
{
std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
//--------------------------------//
A myobj;
std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobj);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
myOutnMsgObj.join();
myInMsgObj.join();
//--------------------------------//
std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
return 0;
}
/***************************************************************************************/
//示例代码2:临界区的的多次进入
//windows的同一个线程中,相同的临界区变量,可以进入多次,但也离开相应的次数
{
EnterCriticalSection(&my_winsec);//进入临界区
EnterCriticalSection(&my_winsec);//进入临界区
//执行相应代码......
LeaveCriticalSection(&my_winsec);//离开临界区
LeaveCriticalSection(&my_winsec);//离开临界区
}
//说明1:C++11的互斥量中,相同的变量在同一个线程中不可以锁两次
//说明2:像智能指针,区域锁等这种自动释放的类,叫做RAII类,资源获取即初始化,其主要实现机理
//.......就是在构造中初始化资源,在析构中释放资源。
/***************************************************************************************/
更新中。。。
服务器-客户端程序,每来一个客户端,服务器端创建一个线程提供服务。
客户端少没有问题,如果一下子来上千、上万个客户端,创建线程就可能失败。
程序不稳定,编写的代码中,偶尔创建一个线程这种代码出现问题,让人不安。
为了应对上述不稳定的问题,提出了线程池的概念
<1>、线程池的概念:
把一堆线程弄到一起,统一管理。这种统一管理调度,循环利用线程的方式叫线程池。
<2>、实现方式:
在程序启动时,一次性的创建好一定数量的线程,要使用线程时,从线程池中抓过来一个进行使用,使用完毕后再放回到线程池中,程序运行过程中不再临时创建线程,使程序更稳定。
<3>、线程数量:
采用某些技术开发程序,api接口供应商有时会给出数量的建议,按照指示设置数量。
更新中。。。