C++ 多线程学习笔记(2):线程启动、结束、创建线程方法

文章目录

  • 一、线程启动、结束、创建线程方法
    • 1. 创建线程的一般方法
      • (1)thread()
      • (2)join()
      • (3)detach()
      • (4)joinable()
    • 2. 其他创建线程的手法
      • (1)用类
      • (2)用成员函数指针作为线程函数
      • (3)用lambda表达式

一、线程启动、结束、创建线程方法

1. 创建线程的一般方法

  1. 说明

    • 主线程在从main开始执行,一旦主线程从main()返回,则整个程序(进程)结束
    • 如果主线程结束了,其他子线程还没执行完,一般会被操作系统强行终止(除非子线程设置为detach
    • 通常我们创建的子线程从一个函数开始运行,一旦此函数运行完毕,代表这个线程运行结束
    • 如果想保持子线程一直运行,不要让主线程运行完毕(除非子线程设置为detach
  2. 创建子线程的一般方法

    • 包含头文件:#include
    • 编写一个子线程开始执行的函数(初始函数)
    • 使用thread()创建子线程
    • 设置主线程和子线程的关系,join()detach()

(1)thread()

1.先看一个示例,这里子线程myprint和主线程main并发运行

#include 
#include 
using namespace std;
    
//子线程的初始函数
void myprint()
{
    cout << "我的线程开始执行" << endl;
    cout << "我的线程执行完毕" << endl;
}
    
//主线程在从main开始执行,一旦主线程从main()返回,则整个程序结束 
int main()
{
	//创建子线程(这两行在main函数里)
    thread mytobj(myprint);	//创建了线程,执行起点是myprint,同时让子线程开始执行
    mytobj.join();			//主线程阻塞在这里,等子线程执行完
    
    cout << "Hello World!\n";	
	return 0;
}

/*
-----运行结果--------
我的线程开始执行
我的线程执行完毕
Hello World!\n
*/
  1. 关于thread
    • thread是一个类
    • thread mytobj(myprint);是利用构造函数创建了thread对象,传入参数是一个可调用对象
    • 这行代码做了两件事
      • 创建了线程,执行起点是myprint函数入口
      • myprint线程开始运行
    • C++11 functional对函数指针做了拓展,可调用对象包括函数指针和函数对象等

(2)join()

  • jointhread 类中的一个方法

  • 作用:阻塞主线程,让主线程等待子线程执行完毕,然后子线程和主线程汇合,再往下执行

  • 对上面的程序进行逐语句debug,在8和17行打断点,单步执行顺序:17-8-18-9-20,可以看出进程的并发效果

  • 如果把上面的mytobj.join();注释掉,运行时会看到输出混乱,而且弹出异常提示

    • 由于主线程和子线程交替执行,所以打印混乱
    • 由于子线程没有结束主线程就结束了,子线程被操作系统强制结束,所以报异常
  • 一旦把线程join了,就不能再detach(否则报异常),我们自己来控制子进程

  • 设计良好的程序

    • 主线程应等待子线程执行完毕后,进行一些收尾工作,然后才能推出
    • 并发应在子线程之间发生,主线程用来控制子线程和执行收尾工作

(3)detach()

  • 传统多线程程序,主线程要等待所有子线程执行完再退出,但C++11增加了detach(),可以不这样干了

  • detachthread 类中的一个方法

  • 作用:一旦detach之后,与这个主线程关联的对象就会失去和主线程的关联,此时这个子线程就会驻留系统在后台运行(即主线程不必再和子线程汇合了,主线程和子线程的执行独立开来)

    • 主线程可以不等子线程结束就先结束
    • 子线程不会被强制结束,而是可以继续运行,C++运行时库接管其运行,并在子线程执行完后为其清理资源(类似linux中的 “守护线程”)
  • 一旦把线程detach了,就不能再join回来了(否则报异常),我们失去了对这个进程的控制

  • 改写一下程序

#include 
#include 
using namespace std;

//子线程的初始函数
void myprint()
{
	cout << "子线程开始执行" << endl;

	cout << "子线程执行中_1" << endl;
	cout << "子线程执行中_2" << endl;
	cout << "子线程执行中_3" << endl;
	cout << "子线程执行中_4" << endl;
	
	cout << "子线程执行完毕" << endl;
}


//主线程在从main开始执行,一旦主线程从main()返回,则整个程序结束 
int main()
{
	thread mytobj(myprint);	//创建了线程,执行起点是myprint,同时让子线程开始执行
	mytobj.detach();		

	cout << "main thread is running_1" << endl;
	cout << "main thread is running_2" << endl;
	cout << "main thread is running_3" << endl;
	cout << "main thread is running_4" << endl;
	cout << "main thread is running_5" << endl;
	cout << "main thread is running_6" << endl;
	cout << "main thread is running_7" << endl;
	cout << "main thread is running_8" << endl;
    cout << "main thread is running_9" << endl;
	cout << "main thread is running_10" << endl;
	
	return 0;
}

/*-------------------运行结果---------------------
子线程开始执行main thread is running_1
子线程执行中_1

main thread is running_2
子线程执行中_2
子线程执行中_3
子线程执行中_4
子线程执行完毕
main thread is running_3
main thread is running_4
main thread is running_5
main thread is running_6
main thread is running_7
main thread is running_8
main thread is running_9
main thread is running_10

运行结果每次都不太一样,如果主线程先结束,进程结束,可能部分子线程的输出看不到(子进程还在后台执行,只是窗口关了看不到)
*/
  • 这种方式不推荐用

(4)joinable()

  • joinablethread 类中的一个方法
  • 作用:用来判断线程是否可以成功使用joindetch,返回truefalse
    • joindetach 调用之前,返回true
    • joindetach 调用之前后,返回false
  • 如果在 joinable 返回 false 时调用joindetch,会报错

2. 其他创建线程的手法

(1)用类

  1. 前面说过:thread mytobj(myprint); 是利用构造函数创建了thread对象,传入参数是一个可调用对象可调用对象也可以是类,这个类要重载(),详见仿函数

  2. 示例代码1

  class TA
  {
  public:
  	void operator()()	//运算符重载,不能带参数(这里是子线程入口)
  	{
  		cout<<"子线程operator()启动"<<endl;
  		cout<<"子线程operator()结束"<<endl;
  	}
  };
  
  
  //在主函数中
  TA ta;
  thread mytobj(ta);
  mytobj.join();
  • 根据先前关于detach的分析,假设这个类中引用了主线程的变量,而且子线程创建后调用detach() 了,如果主线程先执行完,这个被引用的变量就会被回收,而此时子线程(没执行完)仍在引用这块内存空间,会导致不可预料的结果

  • 但是有一个问题:如果主线程调用了detach 了,假设主线程先结束,那ta对象还在吗,如果ta不再了,子线程是不是也被销毁了?

    • 实际上,ta对象确实被回收了,但是子线程还在

    • 这是由于 thread mytobj(ta); 这句实际上创建了一个ta对象的副本放到子线程去了(ta对象被复制到子线程中去,调用拷贝构造函数)。虽然主线程的ta对象被销毁,但子线程的ta对象副本还在

    • 所以:只要这个TA类对象中没有引用/指针,detach 就不会出问题

  1. 示例代码2
#include 
#include 
#include 
using namespace std;

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

	//子线程入口
	void operator()(int num)
	{
		cout << "子线程thread_work执行" << this << "thread id:" << std::this_thread::get_id() << endl;
	}
};

int main()
{
	A myobj(10);
    //注意这个构造函数的调用方法
	std::thread mytobj(myobj, 15);	
	mytobj.join();

	return 0;
}
  • 注意

    • std::thread mytobj(myobj, 15); 这样构造子线程,子线程是基于 myobj 的对象副本的,放心用 detach
    • std::thread mytobj(std::ref(myobj), 15); 这样构造子线程,子线程是基于 myobj 自身的,用 detach 可能出错,不过如果要在子线程修改 myobj 对象的值,则必须这样写,此时应用 join 方式
    • 这个构造函数没有用 & 传引用的形式
  • 对比下面用成员函数指针作为线程函数的示例代码一起看

(2)用成员函数指针作为线程函数

  1. 示例程序
#include 
#include 
#include 
using namespace std;

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

	//子线程入口
	void thread_work(int num)
	{
		cout << "子线程thread_work执行" << this << "thread id:" << std::this_thread::get_id() << endl;
	}
};

int main()
{
	A myobj(10);
    //注意这个构造函数的调用方法
	std::thread mytobj(&A::thread_work, myobj, 15);	//类成员函数指针,类对象,子线程参数
	mytobj.join();

	return 0;
}

/*运行结果

构造函数执行				005BF7B8		thread id:8160
拷贝构造函数执行		  	00B6E89C		thread id:8160
子线程thread_work执行		00B6E89C		thread id:27864
析构函数执行				00B6E89C		thread id:27864
析构函数执行				005BF7B8		thread id:8160

*/
  • 从运行结果可以看出,在进入 thread_work 子线程前,在主线程中发生了一次拷贝构造,因此子线程是基于 myobj 对象的副本的,可以放心使用 detach

  • 如果std::thread mytobj(&A::thread_work, myobj, 15); 这句改成这样:

    • std::thread mytobj(&A::thread_work, std::ref(myobj), 15);
    • std::thread mytobj(&A::thread_work, &myobj, 15);(传引用)

    运行结果会变成下面这样,可见这时子线程是基于 myobj 对象自身了,此时用detach可能会出问题,不过如果要在子线程修改 myobj 对象的值,则必须这样写,此时应用 join 方式

/*运行结果
  
构造函数执行				005BF7B8		thread id:8160
子线程thread_work执行		00B6E89C		thread id:27864
析构函数执行				005BF7B8		thread id:8160
  
*/

(3)用lambda表达式

  • 示例
auto mylamthread = []{	//这里是子线程入口
 	cout<<"子线程mylambda启动"<<endl;
  	cout<<"子线程mylambda结束"<<endl;
}
  
  
//在主函数中
thread mytobj(mylamthread);
mytobj.join();

你可能感兴趣的:(#,C/C++,多线程,c++)