教程链接: https://study.163.com/course/introduction/1006067356.htm.
1-基本概念
-
进程:
一个可执行程序运行起来了,就叫创建了一个进程(进程就是运行 起来的可执行程序)。 -
线程:
- 用来执行代码的。
- 每个进程都有一个主线程,这个主线程是唯一的;也就是一个进程只能有一个主线程。
- 当产生一个进程后,这个主线程就随着这个进程默默的启动起来了。
- 主线程与进程是唇齿相依的(main()函数执行主线程)。
- 每创建一个新线程,就可以在同一时刻,多干一件不同的事。
-
多线程:
线程并不是越多越好,每个线程都需要一个独立的堆栈空间,线程之间的切换要保存很多中间状态,切换会耗费本该属于程序运行的时间。
-
并发(两个或对个任务同时进行)的实现方法:
实现并发的手段:- 通过多个进程实现并发:
进程之间通信:1.同一电脑上:管道、文件、消息队列、共享内存。
2.不同电脑上:socket通信技术。 - 在单独的线程中,创建多个线程实现并发(多线程并发):
一个进程中的所有线程共享地址空间(共享内存)(全局变量、指针、引用都可以在线程之间传递)。
- 通过多个进程实现并发:
2-线程启动、结束、创建方法;join()/detach()
-
程序运行起来,生成一个进程,该进程所属主线程开始自动运行。
-
主线程从main()开始执行,那么我们自己创建的线程,也需要从一个函数开始运行(初始函数),一旦这个函数运行完毕,就代表着我们这个线程运行结束。
-
整个进程是否执行完毕的标志是主线程是否执行完,如果主线程执行完毕了,就代表整个进程执行完毕了。
-
此时,一般情况下,如果其他子线程还没有执行完毕,那么这些子线程也会被操作系统强行终止。
-
所以,一般情况下,如果想保持子线程的运行状态的话,要让主线程一直保持运行状态,不要让主线程运行完毕。
-
如何写多线程:
-
包含一个头文件
<thread>
-
初始函数要写:
void myprint(){……}
-
main()中开始写代码
thread mytobj(myprint); mytobj.join(); //阻塞主线程并等待myprint子线程执行完毕
-
说明:该程序有两个线程在跑,所以可以同时干两件事,即使一条线被堵住了,不影响另一条线运行。
-
-
解释:
1.1-
thread
:是标准库里的类,myprint
:可调用对象thread mytobj(myprint); //1-创建了线程,线程执行起点(入口)myprint();2-myprint线程开始执行。
1.2-
join();
//阻塞主线程,让主线程等待子线程执行完毕,然后子线程和主线程汇合mytobj.join();//主线程阻塞到这里等待myprint()执行完,当子线程执行完毕,join()就执行完毕,主线程就继续往下走
Note:子线程未执行完,主线程不能提前执行完。主线程等待子线程执行完毕后,自己才能最终退出。
1.3-
detach():
传统多线程主线程要等待子线程执行完毕,然后自己再最后退出。detach:分离。//主线程执行你的,子线程执行我的,主线程不必等子线程运行结束,这并不影响子线程的执行。
1.4-
joinable()
:判断是否可以成功使用join()
、detach()
;返回true
(可以join()
或detach()
)或false
。 -
为什么引入detach()?
我们创建了很多子线程,让主线程逐个等待子线程结束,这种编程方法不太好,所以引入了detach()。
- 一旦
detach()
之后(mytobj.detach()
),与这个主线程关联的thread对象就会失去与主线程的关联,此时子线程就会驻留在后台运行(主线程跟该子线程失去联系),子线程就相当于被C++运行时库接管了,当这个子线程执行完毕后,由运行时库负责清理相关的资源。(守护线程) - detach()使线程失去控制。一旦调用了detach(),就不能再用join()了,否则系统会报异常。
- 一旦
-
其他创建线程的方法:
-
用类对象(可调用对象),以及一个问题范例:
class TA { public: void operator()()//不能带参数 {......} }; mian() { TA ta; thread myobj(ta);//ta:可调用对象 }
- Q:一旦调用了detach(),那主线程结束了,这里的ta对象还在吗?
这个对象实际上是被 复制到线程中去的;所以执行完主线程后ta会被销毁,但是所复制 的TA对象依旧存在。所以,只要这个TA类对象里没有引用,没有指针,就不会产生问题。
-
用lambda表达式
auto mylamthread=[]{......}; ...... main() { ...... thread myobj(mylamthread); ...... }
3-detach()陷阱分析
-
线程传参、成员函数做线程参数
1.传递临时变量作为线程参数(临时对象作为线程参数)
-
要避免的陷阱:
-
1.常数引用,字符指针(
void myprint(const int &i, char *pmybuf)
):问题:可能出现引用时,主线程已结束,引用参数地址空间已释放的情况。
-
2.在形参中进行类型转换:
void myprint(const int i, const string &pmybuf); main() { ...... thread mytobj(myprint, mvar, mybuf); ...... } 问题:可能出现主线程已结束,而子线程还未开始转换的情况。
- 正确使用:
thread mytobj(myprint, mvar,string(mybuf));
在这里直接将mybuf
转换成string对象,这是一个可以保证在线程中用肯定有效的对象;创建线程的同时构造临时对象的方法传递参数是可行的。
-
-
总结:
-
若传递
int
这种简单类型参数,建议都是值传递,不要用引用 -
如果传递类对象,避免隐式类型转换;全部都在创建线程这一行就构建临时对象来
thread(myprint,var,A(mbuf))
;然后在函数参数里用引用来接,否则系统还会构造一次对象。 -
终极结论:建议不适用
detach()
,只使用join()
,这样就不存在局部变量失效导致线程对内存的非法引用问题。线程id概念:id是个数字,每个线程(不管是主线程还是子线程)都对应一个数字,且都不相同,即不同的线程对应的线程id不同,可用
std::this_thread::get_id()
来获取。
-
2. 传递类对象、智能指针作为线程参数
-
类对象
void myprint(const A &pmybuf); main() { A myobj(10);//A为类名 thread mytobj(myprint,myobj);//将类对象(myobj)作为线程参数,子线程中修改m_i=10的值不会影响main()中的m_i。 }
std::ref()
函数:
thread mytobj(myprint,std::ref(myobj));
//真引用,pmbuf
地址与myobj
地址一样(用detach()
时可能就会有问题,主线程可能先执行完了) -
智能指针:
void myprint(unique_ptr<int> pmbuf);//以智能指针作为参数 main() { unique_ptr<int> myp(new int(100));//智能指针 std::thread mytobj(myprint,std::move(myp));//传递智能指针 }