线程管理基础

本文详细介绍了C++中线程的启动、线程启动后的操作、如何传递参数、线程所有权转移以及线程的识别,特别强调了线程安全和资源管理的重要性,提供了多种示例来解释线程管理的关键概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程的启动

线程在以初始函数(新线程的入口)构造std::thread对象时启动,换句话说,合理的构造std::thread即启动线程

(注意:以无参构造函数构造的std::thread对象并不启动线程)

 

构造std::thread对象的三种方式

使用普通函数构造

void do_some_work();
std::thread my_thread(do_some_work);

使用可调用对象构造

class background_task
{
 public:
	void operator()() const
	{
		do_something();
		do_something_else();
	}
};
background_task f;
std::thread my_thread(f);


使用类成员函数与类对象构造

class Test
{ 
public:
	void testFunc();
};
Test myTest;
std::thread t(&Test::testFunc, &myTest);

注意:

1、使用以函数对象构造std::thread对象的方式时,需要注意避免直接传递临时对象(直接调用类的构造函数时产生的对象),因为当传递临时对象时,C++编译器会将其解析为函数声明,而不是类对象的定义,因此不会启动线程

std::thread my_thread(background_task());	//被视为声明一个参数为函数指针且返回值为std::thread对象的函数

解决方案

1)、使用命名对象而不是直接传递临时对象

background_task f;
std::thread my_thread(f);


2)、使用多组括号

std::thread my_thread( (background_task()) );	//额外的一对括号

3)、使用C++11新特性,同一初始化语法

std::thread my_thread{ background_task() };	//大括号

4)、使用C++11新特性lambda表达式

std::thread my_thread( []{ 	//捕获列表为空
do_something();			//参数列表与返回值类型为空
do_something_else();		//函数体为两个函数的调用
} );

Lambda表达式表示一个可调用的代码单元(像函数对象一样的可调用对象),可以将其理解为未命名的内联函数

 

线程启动之后

线程在启动之后我们可以对其选择两种操作:

1、使用join()明确等待线程结束(加入式)(有点像pthread_join())

2、使用detach()让其自主运行(分离式)(有点像pthread_detach())

 

注意

1、在std::thread对象析构之前,无论是分离还是加入,必须为其选择一种操作,否则程序就会终止(std::thread对象的析构函数会调用std::terminate()函数)

2、如果线程已经结束(指线程入口函数全部执行完毕),再去分离它,线程(线程入口函数)可能会在std::thread对象销毁后继续运行下去(测试发现并不是这样,或许没碰到我期望的可能吧)

3、join()方式会使当前线程阻塞直到线程执行完毕(当前线程为了回收线程的资源),而detach()方式则直接分离了当前线程与线程,当前线程不会阻塞直到线程结束,使用detach()方式整体来说更为高效,但是使用这种方式必须要谨慎

必须保证线程在结束之前,其可以访问到的数据有效!!!!!

比如线程持有当前线程函数中的局部变量/对象,但是当前线程比线程先结束,这时,线程所持有的指针指向或引用的变量/对象已经无效,这非常危险!

注:当前线程是指构造std::thread对象的线程

struct func
{
	int& i;
	func(int& i_) : i(i_) {}
	void operator() ()
	{
		for(unsigned j=0;j<1000000;++j)
		{
			do_something(i);	//危险
		}
	}
};
void oops()
{
	int some_local_state=0;
	func my_func(some_local_state);
	std::thread my_thread(my_func);
	my_thread.detach();		//当前线程可能先于线程结束
}

等待线程完成

Join()是简单粗暴的等待线程完成(阻塞),一旦join()成功,std::thread对象将不再与已经完成的线程有任何关系。

应注意只能对std::thread对象使用一次join(),每次使用joinable()判断后再join()

if( t.joinable() )
{
	t.join();
}

 

合适的Join()的位置

如果确定需要等待线程结束,则应该仔细考虑join()的位置,join()不应该被某种原因影响而跳过执行

struct func;          //函数对象
void f()
{
	int some_local_state=0;
	func my_func(some_local_state);
	std::thread t(my_func);
	try
	{
		do_something_in_current_thread();
	} 
        catch(...)
	{
		t.join(); 
		throw;
	}
t.join(); 
}


若确定要等待线程,则务必确保线程在当前线程之前结束

RAII_1

有一个简洁的机制可以让我们不必总是寻找在何处join(): 利用RAII机制RAII(Resource Acquisition Is Initialization)”资源取得实际便是初始化时机”,”以对象管理资源”,使线程刚被启动时就被对象所管理,并在析构函数中使用join()

class thread_guard
{
	std::thread& t;
public:
	explicit thread_guard(std::thread& t_) : t(t_) {}
	~thread_guard()
	{
		if( t.joinable() ) 
		{
			t.join(); 
		}
	} 
    thread_guard(thread_guard const&)=delete; 	//声明删除函数,不能以任何方式使用删除函数
	thread_guard& operator=(thread_guard const&)=delete;	//手握资源的对象不应该被随意拷贝或赋值
};

struct func;

void f()
{
	int some_local_state=0;
	func my_func(some_local_state);
	std::thread t(my_func);
	thread_guard g(t); 				
	do_something_in_current_thread();
}

由于析构的顺序,g对象会被先析构,线程在g析构时即join()

 

后台运行线程

使用detach()会让线程在后台运行,即会分离线程,分离后,想要的std::thread对象与实际执行的线程无关了,而且被分离的线程不能被加入join()

线程被分离后,当当前线程退出时,线程的相关资源的回收、线程的归属与控制由C++运行库处理

由于不能对没有执行线程的std::thread对象(默认构造函数构造的对象)与已分离的对象使用detach(),因此使用detach()之前也需要使用joinable()判断,如果返回true,则可以detach()

 

向线程传递参数

首先,知晓一个概念对理解传参非常有帮助 :

std::thread 的构造函数会拷贝提供的变量/对象

其实这个大家都知道的,构造函数也是函数,函数传参除了传递引用都会存在拷贝行为(包括传地址,传地址是拷贝的地址值)

 

传参的三种方式

向由普通函数启动的线程传参

void f(int i, std::string const& s);
std::thread t(f, 3, "hello"); //3、以及字符串字面值的地址被拷贝

如果仔细看了上面的总结,当看到向线程传递地址或引用的动作时你应该有所警觉!因为这一动作可能会违反 必须保证线程在结束之前,其可以访问到的数据有效”这一规则,值得庆幸的是字符串常量”hello”本身位于常量区,所以无论线程被加入还是分离都不会违反这一规则。

下面的例子则不然

void f(int i,std::string const& s);
void oops(int some_param)
{
	char buffer[1024]; 
	sprintf(buffer, "%i",some_param);
	std::thread t(f,3,buffer); 
	t.detach();
}

上面的代码中,定义时buffer为栈数组,在std::thread对象构造时,构造函数得到的实际是指向栈数组首元素首地址的指针,是个指向局部变量的指针!并且线程是分离方式!这非常危险!

解决方案

在传递到std::thread构造函数之前将其转化为std::string对象

void f(int i,std::string const& s);
void not_oops(int some_param)
{
	char buffer[1024];
	sprintf(buffer,"%i",some_param);
	std::thread t(f,3,std::string(buffer)); 
	t.detach();
}

还记得吗? std::thread 的构造函数会拷贝提供的变量/对象,此处用生成的临时std::string对象给线程传参,虽然临时对象也是非常危险的,它只能存在于它产生的所在行,but,临时对象刚一生成就被函数传参这一动作所拷贝。即使在执行到下一句临时对象被销毁,其拷贝(函数形参)依旧存在。即使线程被分离,线程获得的参数也是一份拷贝(准确的说是拷贝的拷贝第一次拷贝发生在std::thread的构造函数构造时,第二次拷贝发生在将拷贝传递给线程入口函数时),并不会存在什么危险。

 

 

一个新问题?

有时候,在线程中改变当前线程中的变量值好像也是必要的,如果想要这样,我们必须保证

的东西已经反复强调过了,具体的方法是要么使用生命周期长于线程生命周期的变量,要么使用join()阻塞直到线程结束。

好了,做好了保证的东西之后怎么在线程中改变当前线程中的值呢?我之前有说过,线程的入口函数其实是得到的拷贝的拷贝(最开始仅仅是我的猜想,经测试,确实如此)

 

测试代码如下

#include <thread>
#include <cstdlib>
#include <iostream>

void hello_(int times, int j)
{
	j = 1000;
	for(int i = 0; i < times; ++i)
	{
		std::cout << "for" << std::endl;
	}
	std::cout << "hello_:: j = " << j << std::endl;
}

void hello_2(int times, int &j)
{
	j = 1000;
	for (int i = 0; i < times; ++i)
	{
		std::cout << "for" << std::endl;
	}
	std::cout << "hello_2:: j = " << j << std::endl;
}

int main(int argc, char **argv)
{
	int t1_num = 10;
	int t2_num = 10;

	std::thread test(hello_, 1, std::ref(t1_num));
	std::thread test2(hello_2, 1, t2_num);

	if( test.joinable() )
		test.join();

	if( test2.joinable() )
		test2.join();

	std::cout << "t1_num = " << t1_num <<std::endl;
	std::cout << "t2_num = " << t2_num <<std::endl;
	
	system("pause");
	return 0;
}

运行结果如下:

 


注:linux下测试需要指定以C++11方式编译并指定连接线程库

 

由于发生了两次拷贝,为了能在线程内部改变当前线程中的值,需要通过下面的方式之一:

1、std::thread对象构造时向构造函数传递变量的地址,并将线程入口函数中形参声明为指针

2、std::thread对象构造时向构造函数传递变量的地址,并将线程入口函数中形参声明为指针的引用

3、构造std::thread对象时,使用std::ref()(C++11新特性)将参数转换成引用形式,并将线程入口函数中形参的声明为引用

 

如果我没有遗漏的话,应该没有其他方式可以在线程中改变当前线程中的值了,另外值得注意一点的是,确保传参类型一致,在传参时编译器有类型检查,不一致会报错

 

向由函数对象启动的线程传参

还是这个 std::thread 的构造函数会拷贝提供的变量/对象  于是乎可以通过函数对象的成员变量向线程传参

struct func
{
	int i;
	func(int i_) : i(i_) {}
	void operator() ()
	{
		for (unsigned j=0 ; j<i; ++j)
		{
			std::cout << j << std::endl;
		}
	}
};
void oops()
{
	int some_local_state = 10;
	func my_func(some_local_state);
	std::thread my_thread(my_func);
	my_thread.detach(); 
}

 

向由类成员函数启动的线程传参

当以此种方式向线程传参时,std::thread的构造函数的第一个参数就是类的成员函数,第二个参数就是类对象,第三个参数开始往后的参数为成员函数需求的参数

class X
{ 
public:
	void do_lengthy_work(int);
};

X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num);

注意到了吗?,构造函数的第二个参数是指向对象的指针!由此,你应该想到些什么!

 

转移线程所有权

C++标准库中很多资源占用类型,如std::ifstream,std::unique_ptrstd::thread都是可移动不可拷贝的,也就是说线程的所有权可以在多个std::thread对象中移动

转移意味着原对象将不再占用资源,即原std::thread对象将不再与线程相关联,原对象也将不能对线程进行加入或分离操作

 

注意

线程的所有权可以移动,但是一个std::thread对象只能拥有一个线程,再次向一个std::thread对象转移线程所有权的操作会终止程序的运行()

 

两种所有权转移的方式

显示转移

显示转移适用于有名对象之间转移线程所有权,使用时需要使用std::move()(C++11特性)

void some_function();
std::thread t1(some_function);
std::thread t2=std::move(t1);

注意:如果你非要使用std::move()将一个临时对象所关联的线程所有权转移给有名对象也是可以的,但反之将会引发程序的终止(因为这相当于直接丢弃一个线程)!

 

隐式转移

隐式转移是自动发生的,很多所有潜在的拷贝操作都将引发隐式转移,如拷贝构造、拷贝赋值、函数传参、函数返回。

t1=std::thread(some_other_function);

std::thread th (std::thread(&test::func, &t) );		//func为成员函数,t为类对象

std::thread g()						//返回std::thread对象
{
	void some_other_function(int);
	std::thread t(some_other_function,42);
	return t;
}

void f(std::thread t);
void g()
{
	void some_function();
	f(std::thread(some_function));	
	std::thread t(some_function);
	f(std::move(t));				//显示转移
}

 

注意

就像我之前提到的,一个std::thread对象只能拥有一个线程,试图向一个std::thread对象再次转移线程所有权的操作将会使程序终止,不过不用担心两个已关联线程的对象间存在拷贝赋值操作,因为你这样做的话,编译器会提示

error C2280: 'std::thread &std::thread::operator =(const std::thread &)' : attempting to reference a deleted function

 

RAII_2

class scoped_thread
{
	std::thread t;
public:
	explicit scoped_thread(std::thread t_): 
	t(std::move(t_))
	{
		if( !t.joinable() ) 
		throw std::logic_error(“No thread”);
	} 
    ~scoped_thread()
	{
		t.join(); 
	} 
    scoped_thread(scoped_thread const&)=delete;
	scoped_thread& operator=(scoped_thread const&)=delete;
};
struct func;          //函数对象
void f()
{
	int some_local_state;
	scoped_thread t(std::thread(func(some_local_state))); 
	do_something_in_current_thread();
}

如果一个容器是移动敏感的容器( std::vector<> )std::thread对象的移动操作同样适合

void do_work(unsigned id);
void f()
{
	std::vector<std::thread> threads;
	for(unsigned i=0; i < 20; ++i)
	{
		threads.push_back(std::thread(do_work,i)); // 产生线程
	} 
    std::for_each(threads.begin(),threads.end(),
				std::mem_fn(&std::thread::join)); // 对每个线程调用join()
}

一个相当有用的库函数

函数std::thread::hardware_concurrency()的返回值是在一个线程能真正并发的数量(硬件线程)或者返回0(当系统信息无法获取时)

借助这个库函数,也许我们能写出并发效率最高的代码

 

 

线程的识别

一个线程的标识是std::thread::id,可以通过两种方式获得一个线程的标识:

获取非当前对象所关联线程id

通过调用std::thread对象的成员函数get_id(),如果std::thread对象没有与任何线程关联,get_id()将返回std::thread::id的默认构造值(值为0),这个值表示没有线程

 

 

获取当前线程id

通过调用std::this_thread::get_id()函数

 

std::thread对象不同,std::thread::id对象可以自由拷贝,还可以对比两个std::thread::id对象。

如果两个std::thread对象的std::thread::id相等,那么它们就是同一线程或都没有线程,如果不等,就代表了两个不同的线程,或者一个有线程,另一个没有。

 

线程id的作用

通过区别线程id来使线程执行不同的操作

std::thread::id master_thread;
void some_core_part_of_algorithm()
{
	if( std::this_thread::get_id()==master_thread )
	{
		do_master_thread_work();
	} 
do_common_work();
}

 

注:文中的例代码大部分来自  C++并发编程实战


参考: C++并发编程实战

 C++ Primer

 Effective C++




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值