线程管理


Lunching Thread

C++ 11中对线程的管理集中在std::thread这个类中,创建线程的方式包括:

  1. 使用回调函数
  2. 使用函数对象
  3. 使用Lambda表达式
void funcptr(std::string name){
    cout << name<<endl;
}
struct Functor{
    void operator()(string name){
        cout<<name<<endl;
    }
};
void runCode(){
	//使用function pointer
	std::thread t1(func1,"abc");
	t1.detach();

	//使用Functor
	Functor f;
	std::thread t2(f,"Functor thread is running");
	t2.detach();

	//使用lambda
	std::string p = "lambda thread is running";
	std::thread t3([p]{
		cout<<p<<endl;
	});
	t3.join();
}

std::thread是C++11引入的用来管理多线程的新类,是对UNIX C中pthread_t结构体的封装,构造时调用pthread_create传入pthread_t和回调函数指针

typedef pthread_t __libcpp_thread_t;
class _LIBCPP_TYPE_VIS thread{
    __libcpp_thread_t __t_; //pthread_t
	...
}
thread::thread(_Fp&& __f, _Args&&... __args){
	...
	int __ec = __libcpp_thread_create(&__t_, &__thread_proxy<_Gp>, __p.get());
	 if (__ec == 0)
        __p.release();
   	 else
        __throw_system_error(__ec, "thread constructor failed");
}
int __libcpp_thread_create(__libcpp_thread_t *__t, void *(*__func)(void *),
                           void *__arg){
  return pthread_create(__t, 0, __func, __arg);
}

std::thread的构造函数中前两个参数均为右值引用,第二个参数将传入的lambda表达式或者functor通过__thread_proxy<_Gp>转化成C函数指针(void *(*__func)(void *),这个问题可参考之前对C++11中 move语义的介绍std::thread对象在创建后,如果不做其它操作,线程立刻执行,这里称这个线程为worker_thread,称发起worker_thread的线程为launch_thread

如果std::thread对像在被销毁前未执行join()detach()操作,则在其析构函数中会调用std::terminate造成系统崩溃。因此需要确保所有创建的std::thread对象都能被正常释放,在《C++ Concurrency in Action》中,提到了一种方法:

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;
};
void runCode(){
	...
	std::thread t(f);
	thread_guard g(t); //g在t之前释放,保证join的调用
	do_something_in_current_thread();
}

由于thread_guard对象总是在std::thread对象之前析构,因此可以在t析构之前调用join函数,保证t可以安全释放。

这种方式是所谓的RAII(Resource Acquisition Is Initialization),即通过构造某个对象来获得某个资源的控制,在该对象析构时,释放被控制的资源。也就是将某资源和某对象的生命周期做绑定,在C++中这是一种很常用的设计方式,背后的原因是C++允许栈对象的创建和析构,后面在讨论mutex时还会继续用到这种技术

Join & Detach

join()launch_threadworker_thread的一个线程同步点,launch_thread会在调用join()后等待worker_thread执行完成后继续执行

std::string p = "lambda";
//using lambda expression as a callback function
std::thread td([p]{cout<<p<<" thread is running"<<endl;});
cout<<"lanched thread is running"<<endl;
td.join();
cout<<"lanched thread is running"<<endl;
td.joinable(); //return false
  1. 如果tdmain thread执行td.join()之前完成,则td.join()直接返回,否则launch_thread会暂停,等待td执行完成
  2. 如果不调用td.join()td.detach(),在td对象销毁时,在std::thread的析构函数中,如果则系统会发出std::terminate的错误
  3. td在调用join后,joinable转态变为false,此时td可被安全释放
  4. 确保join()只被调用一次

如果使用td.detach()workder_thread在创建后立刻和launch_thread分离,launch_thread不会等待workder thread执行完成。即两条线程没有同步点,各自独立执行

void runCode()
{
    cout << "lanched thread is running" << endl;
    std::string p = "lambda";
    std::thread td([p] { 
        std::this_thread::sleep_for(std::chrono::milliseconds(5000));
        cout << p << " thread is running" << endl; 
    });
    td.detach();
    cout << "lanched thread is ending" << endl;
}

上述代码中令workder thread暂停5s,则launch_thread继续执行,不会等待worker_thread执行完,如果将detach()改为join()launch_thread会阻塞等待

//detach
lanched thread is running
lanched thread is ending

//join
lanched thread is running
lambda thread is running
lanched thread is ending

向线程传递参数

std::thread构造函数传递参数的规则为:

  1. 第一个参数为函数指针,可以是functor或者lambda表达式,在第一节中已经介绍
  2. 后面参数为该函数指针需要用到的参数

观察前面的std::thread的构造函数可知,传递的参数均是拷贝到线程自己stack中,但是有某些场景,需要修改lauch_thread所在线程的局部变量,这是需要将该变量的引用传递给worker_thread。例如下面的例子中需要在worker_thread中修改data变量

void updateData(widget_data& data);
void oops(){
	widget_data data;
	std::thread t(updateData, data); //这里传过去的是data的copy
	t.join();
	process_widget_data(data);
}

参考之前文章中对bind函数的介绍,可知这里只需要一个很小的改动,使用std::ref(x),即可把data从传拷贝变成传引用:

std::thread t(updateData, std::ref(data))

如果传引用或者指针要特别注意变量的生命周期,如果该变量的内存在线程还未结束时被释放则会引undefined behavior

为了进一步加深对std::thread构造函数的理解,继续参考bind函数不难发现,std::thread的构造函数和bind的传参机制是相同的,这意味者只要第一个函数时一个函数指针,后面是该函数的参数即可,因此可以不局限于使用第一小节介绍的三种构建线程的方式,比如:

class X{
public:
    void do_some_work(){
        cout<<"do_some_work"<<endl;
    };
};
X x;
std::thread t(&X::do_some_work, &x);
t.detach();

上述代码中,X::do_some_work方法的第一个参数为this指针,因此可将x取地址后传入,可达到相同效果。

Resources