C++ 并发与多线程学习笔记(二)线程启动、结束、创建、join()、detach()

学习前置:熟悉C++的基本语法,面向对象特性,对数据结构、计算机操作系统有相应的了解。清楚计算机中进程和线程的概念,清楚并发、同步、异步、互斥等术语.
尤其是要深刻理解
宏观上并行,微观上串行”的意义。
一核超频,七核围观”的现象体现了多线程编程的重要性。

计算机CPU中的8核8线程和8核16线程的区别和意义。
等内容详见高校操作系统教材。

一个进程必然包含一个主线程,进程和主线程密不可分。
thread:是一个标准库里的类
实例化一个thread对象即可操作线程
例:

void myfun()
{
	...
}

int main()
{
	...
	std::thread myThreadObj(myfun);
	myThreadObj.join();
	...
	return 0;
}

join():加入/汇合,作用是阻塞主线程(并非让主线程进入阻塞态),让主线程等待子线程执行完毕,然后子线程和主线程汇合。

注意:如果主线程执行完毕了,但子线程没有执行完毕,这种写法是不合理的,程序也是不稳定的。
一个书写良好的程序,应该是主线程,等待子线程执行完毕后,自己才能最终退出;

detach():分离,即主线程不再等待子线程执行完毕。
为什么引入detach()?:有开始当然要有结束。
注意:多线程程序应该从稳定的角度考虑,尽量让主线程等待所有子线程结束后再结束主线程。
一旦detach()之后,与这个主线程关联的thread的对象就会失去与主线程的关联,即子线程和主线程进入并发状态。
使用detach()之后,不能再join,否则会异常。

joinable():检测能否join,返回值为bool,线程detach或者join之后,就不能再join了。

我们先来看看一个简单的例子,来理解交替并行的线程。
为了体现出区别,多线程程序往往使用较为耗时的操作来测试和查漏。我们知道输入输出流是有系统IO调度的,是一个比较耗时的操作,可以用来测试。但实际问题中,有更多需求的时候还是考虑用计时器等精确延时来操作。

//main.cpp
#include <string>
#include <iostream>

using namespace std;
void Threadfunc()
{
	cout << "我是子线程,我执行了1行" << endl;
	cout << "我是子线程,我执行了2行" << endl;
	cout << "我是子线程,我执行了3行" << endl;
	cout << "我是子线程,我执行了4行" << endl;
	cout << "我是子线程,我执行了5行" << endl;
	cout << "我是子线程,我执行了6行" << endl;
	cout << "我是子线程,我执行了7行" << endl;
	cout << "我是子线程,我执行了8行" << endl;
	cout << "我是子线程,我执行了9行" << endl;
	cout << "我是子线程,我执行了10行" << endl;
	cout << "我是子线程,我执行了11行" << endl;
	cout << "子线程结束了" << endl;
}

int main()
{
	thread my_tobj(Threadfunc);
	my_tobj.join();
	cout << "我是主线程 ,我执行了1行" << endl;
	cout << "我是主线程 ,我执行了2行" << endl;
	cout << "我是主线程 ,我执行了3行" << endl;
	cout << "我是主线程 ,我执行了4行" << endl;
	cout << "我是主线程 ,我执行了5行" << endl;
	
	system("pause");
	return 0;
}

可以看到,我在子线程创建之后就调用了join(),让主线程进入阻塞,等待子线程执行完毕。
结果如图:
在这里插入图片描述

如果我们把join注释掉,连续运行两次
在这里插入图片描述
在这里插入图片描述
场面可以说是混乱不堪,别说主线程和子线程之间,就连同一条语句中提行用的endl都没有正常的运行,主线程都已经结束了子线程仍在运行。多次结果都不一样。

学习了操作系统后可以知道,现代操作系统大多是在时间片轮转的基础上进行改进,综合多级反馈调节优先级的调度策略,这解释了为什么多线程程序的执行顺序不定,当然一般情况下不需要我们去担心系统调度的问题。对于操作系统来说,了解这个原因即可。
但对于多线程程序来说,这就体现了合理分配线程推进,和线程之间的同步互斥是非常重要的,这时候我们在书上学的PV操作信号量,消费者问题等就为我们提供了解决思路。

其他创建线程的方法

用类来创建:

//TA.h
class myThread
{
public:
	void operatpr()() //不带参 重载()变成一个可调用对象
	{
		...
	}	
};


//main.cpp
int main()
{
	myThread mt;
	thread mytobj(mt);
	//在初始化类的时候,不可以用匿名的对象
	//如以下错误:
	//thread mytobj(new myThread());
	//这样是thread接受都是一个值而非我们想要的可调用对象
	mytobj.join();
	return 0;
}

这里引申出一个问题,调用了detach,当我的主线程执行结束了,那我实例化的mt这个对象还在吗?这里其实对象已经不在了,开始的时候就被复制到了线程里面,这就有一个重要的隐患,在对象中如果有来自其他线程的引用或者指针,这个值被线程以外的方法修改时就会引起程序的不安全甚至崩溃。

思考:detach之后不能再join是不是因为这个原因所以这么规定呢?

用lambda表达式来创建线程

auto mylamthread = [] 
{
	dosomething();
	....
	
}
thread mytobj(mylamthread);
mylamthread.join();

方法和用函数创建线程大同小异。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页