1)线程的一些常识
1)进程:就是运行起来的可执行程序;
2)线程:一条代码的执行通路;
3)全局变量,指针,引用都可以在线程之间传递;
4)主线程从main函数开始,子线程也得从函数开始,一旦这个函数运行完毕,就代表我们这个线程结束!
5)在线程中,join是有【汇合】等待的意思,延伸为等待,就是主线程要等待子线程汇合后再执行,所以主线程要阻塞!
join这个函数很重要,决定了程序的执行顺序;
传统多线程程序是主线程要等待子线程执行完后,自己最后退出;
6)为什么引入detach(),因为一个主线程等待多个子线程执行完再继续执行,编程方法过于保守;
detach分离后,子线程就会驻留在后台运行,这个子线程相当于被C++运行时库接管,当子线程运行完,运行时库清理该子线程的资源!
驻留到后台的线程叫【守护线程】;
7) joinable()判断是否使用join()或者detach();
8) lambda表达式作为线程参数
9) 在写线程的时候,当处理子线程和主线程的关系的时候,时刻想着join和detach这两个函数,时刻要明确,用detach是否安全!
int main() //上述代码
{
auto fun = []() {cout << "12344444" << endl; };
std::thread th(fun);
th.join();
return 0;
}
9)类对象(仿函数)作为线程参数
class A
{
public:
A(int tmp):m_(tmp){ cout << "构造函数" << endl; }
A(const A& obj):m_(obj.m_)
{
cout << "拷贝构造函数"<< endl;
}
~A()
{
cout << "析构函数---" << endl;
}
void operator()() //不能带参数!!!否则调用不起来
{
cout << "***********"<< endl;
}
private:
int& m_;
};
int main() //上述代码
{
int num = 6;
A a1(num);
std::thread th(a1);//这里执行了拷贝构造函数
//th.join();
th.detach();
cout << "main thread-------" << endl;
return 0;
}
上述代码,可能有个疑问,一旦detach(), 那主线程结束了,则对象a1是否存在?
答案:这个对象实际是被【复制】到线程中去的,执行完主线程,a1确实会销毁,但复制的对象依然存在!
上述代码输出结果:
构造函数
拷贝构造函数
***********
析构函数--- //复制对象(线程的对象)
main thread-------
析构函数--- //主线程内a1对象
2)线程的参数使用
1)传递临时对象作为线程参数
//以下myprint函数中,主线程的buf地址和args地址是一个地址,当主线程detach的时候,会造成严重问题!所以绝对不能用指针
//修改为创建线程时,用string创建一个临时对象,在线程函数中,用string接这个临时对象就可以了;
//不用临时对象,则构造函数是在子线程进行的,是不安全的;用了临时对象,构造函数是在主函数执行的,所以是安全的。
//args要用引用来接,否则会再次调用一次拷贝构造函数,浪费!
//终极结论:尽可能用join,万不得已用detach;
void myprint(const int i, const string& args) //char* args,这样写时错误的!
{
cout << i << endl;
cout << args.c_str() << endl;
return;
}
int main() //上述代码
{
int var = 1;
int& varref = var;
char buf[] = "i love china";
//这里虽然是引用,但也是按值传递的【varref和参数i不是一个地址】,但老师也不建议这么写!!!
std::thread th(myprint, varref, std::string(buf)); //直接写buf作为字符串参数也错误的,必须用创建临时对象作为参数;
th.join();
cout << "main thread------" << endl;
return 0;
}
2)为什么要非得创建临时对象后,才可以作为线程参数,测试代码如下:
class A
{
public:
A(int a) :a_(a) { cout << "a constructor--" << endl; }
A(const A& obj) :a_(obj.a_) { cout << "a copy constructor--" << endl; }
~A() { cout << "a deConstructor--" << endl; }
int a_;
};
void printMsg(int i, const A& arg)
{
cout << i << endl;
cout << arg.a_ << endl;
}
int main()
{
std::thread th(printMsg, 12, A(67));
th.detach();
cout << "main------thread" << endl;
return 0;
}
2)传递类对象,智能指针作为线程参数
用对象作为参数,在创建线程的时候,当子线程结束后,子线程的数据不会影响到主线程,因为都是按值传递的;
如果真的想把子线程的数据返回给主线程,需要用std::ref()这个函数,才可以,这样子线程的计算数据会返回给主线程;
void printMsg(const A& arg) //这里为什么必须是const呢
{
arg.a_ = 56;
cout << arg.a_ << endl;
}
int main()
{
A a1(3);
std::thread th(printMsg, std::ref(a1)); //std::ref的用法
th.join();
cout << "main------thread a1.a_: "<<a1.a_ << endl;
return 0;
}
//智能指针作为线程参数,注意事项:必须用join等待,用detach一定是错误的,因为ptr地址和arg地址是同一个地址!
void printMsg(std::unique_ptr<int> arg){}
int main()
{
std::unique_ptr<int> ptr(new int(10));
std::thread my(printMsg, std::move(ptr));
my.join();
return 0;
}
3)用vector创建多个线程,用容器管理线程
void func(int i) //线程入口函数
{
cout << "thread is create,i is: " << i << endl;
}
int main()
{
std::vector<std::thread> vth;
for (int i = 0; i < 10; i++)
{
vth.push_back(std::thread(func, i)); //线程临时对象
}
for (auto iter = vth.begin(); iter != vth.end(); iter++)
{
iter->join();
}
cout << "main---------thread over" << endl;
return 0;
}
4)创建多个线程,数据共享分析,共享数据的保护案例代码
4.1)直接使用互斥量
2)数据共享分析
2.1)只读数据:是安全稳定的,不需要特别处理手段!
2.2)又读又写:需要特殊处理, 最简单处理,能读时不能写,能写时不能读;
class A
{
public:
//把收到的消息(玩家命令)插入到容器中的线程
void inQueue()//成员函数入口
{
for (int i = 0; i < 10000; i++)
{
cout << "insert element " << i << endl;
rlist.push_back(i);
}
}
//从容器取出线程
void outQueue()
{
for (int i = 0; i < 10000; i++)
{
if (!rlist.empty())
{
int cmd = rlist.front();//返回第一个元素,但不检查第一个元素是否存在
rlist.pop_front();//移除第一个元素,但不返回
}
else
{
cout << "outQueue thread handle--but empty" << i << endl;
}
}
}
private:
std::list<int> rlist;//共享容器,代码玩家发过来的命令
};
int main()
{
A obj;
std::thread thOut(&A::outQueue, &obj);
std::thread thin(&A::inQueue, &obj);
thOut.join();
thin.join();
return 0;
}
上述代码运行时会出现错误,主要是两个线程(读线程和写线程)同时操作共享数据,造成的,如果解决,需要互斥量mutex;
4.2)std::mutex与std::lock_guard的配合使用
mtx.lock(); //如果锁成功,则返回;不成功,则一直等待
std::lock_guard是类模板,如果使用了lock_guard, 就不能在使用mtx.lock和mtx.unlock; std::lock_guard<std::mutex> lock(mtx);
class A
{
public:
//把收到的消息(玩家命令)插入到容器中的线程
void inQueue()//成员函数入口
{
for (int i = 0; i < 10000; i++)
{
cout << "insert element " << i << endl;
std::lock_guard<std::mutex> lock(mtx);
//mtx.lock(); //如果锁成功,则返回;不成功,则一直等待
rlist.push_back(i);
//mtx.unlock();
}
}
//从容器取出线程
void outQueue()
{
for (int i = 0; i < 10000; i++)
{
if (!rlist.empty())
{
//mtx.lock();
std::lock_guard<std::mutex> lock(mtx);
//构造函数里执行了mtx.lock;
int cmd = rlist.front();//返回第一个元素,但不检查第一个元素是否存在
rlist.pop_front();//移除第一个元素,但不返回
//mtx.unlock();
//出作用域的时候,析构函数执行了mtx.unlock;
}
else
{
cout << "outQueue thread handle--but empty" << i << endl;
}
}
}
private:
std::list<int> rlist;//共享容器,代码玩家发过来的命令
std::mutex mtx;
};
int main()
{
A obj;
std::thread thOut(&A::outQueue, &obj);
std::thread thin(&A::inQueue, &obj);
thOut.join();
thin.join();
cout << "main thread is over--------" << endl;
return 0;
}
4.3)unique_lock代替lock_guard的使用
unique_lock取代lock_guard
6.1)缺省值时,unique_lock和lock_guard是完全一样的用法;std::unique_lock<std::mutex> unique_guard1(mtx1);
std::lock_guard<std::mutex> guard1(mtx1);
6.2)unique_lock的第二参数;std::adopt_lock,std::defe_lock,std::try_to_lock等起到标记作用!!!注意没有括号,不是成员函数
adopt_lock使用前提是提前加锁,defer_lock是不能提前加锁!调用unique_guard1.lock()手动加锁。
unique_lock中,std::try_to_lock是尝试加锁,用owns_lock()这个成员函数做判断!
unique_lock的成员函数:lock(),unlock(); try_lock();(注意:try_lock()也要和defer_lock()一起使用);
try_lock()函数用法和std::try_to_lock参数用法类似,只不过一个用成员函数实现,一个用参数实现。
release(),返回mutex对象指针,并释放所有权。也就是说unique_lock和mutex不再有任何关系,如果原有对象mutex处于加锁状态,你有责任接管并解锁;
相关的代码如下:
std::unique_lock<std::mutex> uniqueGuard(mtx1);
std::mutex* mtxptr = uniqueGuard.release();
mtxptr->unlock();
6.3) unique_lock所有权的传递:
通常情况下,unique_lock和mutex是配合实现的,类似于lock_gurard和mutex一样。
std::unique_lock<std::mutex> uniqueGuard(mtx1);
std::unique_lock<std::mutex> uniqueGuard2(std::move(uniqueGuard)); //把uniqueGuard的所有权转移到uniqueGuard2中,uniqueGuard为空;
总结:数据保护说白了,就是何时lock,何时unlock,都有哪些使用方法;