天天看点

C++并发与多线程(1)——创建线程

C++并发与多线程(1)——创建线程

    • 零、注意事项
    • 一、创建线程的一般方法
    • 二、判断线程是否可以join或detach
    • 三、创建线程的其他方法
      • 1.类创建线程
      • 2.lambda(匿名函数)表达式创建线程

零、注意事项

  1. 程序运行起来,生成一个进程,该进程所属的主线程开始自动运行;当主线程从main()函数返回,则整个进程执行完毕。
  2. 主线程从main()开始执行,那么我们自己创建的线程,也需要从一个函数开始运行,一旦这个函数运行完毕,线程也结束运行。
  3. 整个进程是否执行完毕的标志是:主线程是否执行完,如果主线程执行完毕了,就代表整个进程执行完毕了,此时如果其他子线程还没有执行完,也会被强行终止(此条有例外,以后会解释)。

一、创建线程的一般方法

首先要引入头文件

#include

,C++11中管理线程的函数和类在该头文件中声明,其中包括

std::thread

类。

创建线程的格式是

std::thread mythread(function);

,意思是创建了一个名为

mythread

的线程,并且线程

mythread

开始执行。其中可调用对象名(即括号中的

function

)作为第一个参数,第二个参数为该函数的第一个参数,如果该函数接收多个参数就依次写在后面。此时线程开始执行。

需要注意,线程类参数是一个可调用对象,C++中的可调用对象可以是函数、函数指针、lambda表达式、bind创建的对象或者重载了函数调用运算符的类对象。

当线程启动后,一定要在

thread

对象销毁前,对线程运用

join()

或者

detach()

方法。这是两种线程阻塞的方法,两者的区别在于是否等待子线程执行结束:

  • join():主线程等待子线程运行完毕,子线程运行完后,主线程才可继续运行。
  • detach():主线程与子线程分离,即主线程不必等待子线程运行完毕,子线程也不必理会主线程是否运行完毕。即使主线程运行完毕,子线程也可在后台继续运行。

以下程序展示了子线程的创建及执行过程:

#include <iostream>
#include <thread>
using namespace std;

void test()
{
	int i = 0;
	for(; i < 15; i++)
		cout << "test " << i << endl;
}

int main()
{
	int i = 0;
	//创建了线程,线程执行起点(入口)是test,并开始执行线程
	thread mytobj(test);
	//阻塞主线程并等待test执行完,当test执行完毕,join()就执行完毕,主线程继续往下执行
	//join意为汇合,子线程和主线程汇合
	mytobj.join();
	//一般在多线程程序中,主线程要等待子线程执行完毕,然后主线程才能向下执行
	for (; i < 10; i++)
		cout << "main " << i << endl;
	return 0;
}
           

程序输出的结果是可预料的:先是15个test(0-14),然后是10个main(0-9)。从程序中我们已经看到,

join()

方法其实更像有等待的意思,而不是加入的意思。如果我们忘记使用

join()

方法,编译虽然不会报错,但是执行过程中会报错,从而导致程序中止运行(abort)。

除此之外还有另外一种线程阻塞的方法:

detach()

方法,它有分离的意思,主线程不再和与子线程汇合,不再等待子线程。此时子线程和主线程失去关联,驻留在后台,由C++运行时库接管。

对于上面的程序,注意以下三个问题:

  1. 如果我们把上面程序中的

    mytobj.join();

    改为

    mytobj.detach();

    ,那么输出结果将会是不可预料的,因为你无法预知CPU在哪个时刻切换到哪个线程执行,因此很可能会出现输出内容打印不全的问题。因此,我们初学时一般不用detach,它违背了传统多线程程序的规律。
  2. 如果主线程比子线程先结束,那子线程剩余未输出的内容将不会显示在控制台窗口里,因为控制台窗口受主线程控制,主线程结束后控制台的输出也就结束了。所以,每次输出结果都会不一样。
  3. 在创建线程时,如果

    thread

    类传入的参数含有引用或指针,则子线程中的数据依赖于主线程中的内存,主线程结束后会释放掉自身的内存空间,则子线程会出现错误。例如以下程序:
#include <iostream>
#include <thread>
using namespace std;

void print(int &x)
{
	cout << "print" << x << endl;
}

int main()
{
	int a = 10;
	thread mythread(print, a);
	mythread.join();
	cout << "main" << endl;
	return 0;
}
           

在VS2019环境中可以编译通过,但是运行时会发生错误。初步原因应该是main线程结束后释放了内存空间,使得print线程无法引用main线程的内存。解决方法:(1)去掉&后恢复正常;(2)将语句

thread mythread(print, a);

修改为

thread mythread(print,ref(a));

。关于ref函数的问题我以后再去研究研究。

二、判断线程是否可以join或detach

上面我们说了,

detach()

方法使用后,子线程与主线程独立运行,互相不受影响。join()和detach()两者中只能调用其中一个且只能调用一次,如果子线程分离(detach)后,再想调用join,是不行的;相反地,如果子线程join了以后再想detach也是不行的,这两种情况程序运行的会报错。幸好C++11为我们提供了一种判断方法。

thread

类提供了

joinable()

方法,用以判断某个线程是否可以join或detach。若可以使用其中一个,则返回

true

;若都不可以使用,则返回

false

下面为一个例子:

#include <iostream>
#include <thread>
using namespace std;

void test()
{
	int i = 0;
	for(; i < 15; i++)
		cout << "test " << i << endl;
}

int main()
{
	int i = 0;
	thread mytobj(test);
	mytobj.detach();
	//joinable()判断是否可以成功使用join()或者detach()
	//如果返回true,证明可以调用join()或者detach()
	//如果返回false,证明调用过join()或者detach(),join()和detach()都不能再调用了
	if (mytobj.joinable() == true)
		cout << "join success" << endl;
	else
		cout << "join fail" << endl;
	for (; i < 10; i++)
		cout << "main " << i << endl;
	return 0;
}
           

程序的输出结果会有

join fail

三、创建线程的其他方法

1.类创建线程

使用该方法之前,首先需要重载括号运算符,否则在创建线程时圆括号不能被编译器解释。下面是一个例子:

#include <iostream>
#include <thread>
using namespace std;

class TA {
public:
	int &i;
	TA(int &mi) :i(mi) {
		cout << "构造函数!" << this << endl;
	}

	TA(const TA &ta) :i(ta.i) {
		cout << "拷贝构造函数!" << this <<endl;
	}

	~TA()
	{
		cout << "析构函数!" << endl;
	}

	void operator ()() const 
	{
		for (int i = 0; i < 15; i++)
			cout << "重载运算符" << i << endl;
	}
};

int main()
{
	int i = 6;
	TA a(i);
	
    //创建线程时会调用拷贝构造函数
	thread mytobj(a);
	mytobj.join();

	for (int i = 0; i < 10; i++)
		cout << "main " << i << endl; 

	return 0;
}
           

容易发现,主线程中的对象和子线程中的对象的地址是不一样的。

2.lambda(匿名函数)表达式创建线程

下面是一个例子:

auto lambdaThread = [] {
		cout << "我的线程开始执行了" << endl;
		//。。。
		//。。。
		cout << "我的线程开始执行了" << endl;
	};

	thread myThread(lambdaThread);
	myThread.join();