文章目录
  1. 1. 如何创建一个线程
    1. 1.1. 传参
  2. 2. 如何销毁一个线程
  3. 3. std::thread的所有权(owenership)
  4. 4. 如何控制线程数量
  5. 5. 线程ID

C++11标准中加入了对多线程编程的支持,主要添加了下面一些方面:

  • 定义了内存模型
  • <thread>,引入std::thread来管理、控制线程。
  • <atomic>,原子操作相关类。
  • <mutex>,引入mutex,以及相关的类,用于锁等互斥操作。
  • <condition_variable>,引入condition_variable以及相关类,用于线程间同步。
  • <future>,引入future, promise以及相关的一些类,主要用于线程间同步和通信的一些机制。

这篇主要介绍对于线程管理的相关话题,对书中的内容进行总结并适当扩展。

  • 如何创建一个线程
  • 如何销毁一个线程
  • std::thread的所有权(owenership)
  • 如何控制线程数量
  • 线程ID

如何创建一个线程

1
2
template< class Function, class... Args > 
explicit thread( Function&& f, Args&&... args );

std::thread构造函数如上,它可以接受任何callable的类型,包括全局方法、成员方法、std::function、仿函数、lambda表达式等。并且后面可以带参数,用来传递给方法。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

void func1() {}
class A {
public:
void func2(){}
};

struct B {
public:
void operator()() { }
};


A obja;
B objb;
A obja;
B objb;
std::thread t1(func1); //全局方法
std::thread t2(&A::func2, obja);//成员方法
std::thread t3(objb); //functor
std::thread t4([](){}); //lambda

注意事项:

  • std::thread对象一旦创建,线程马上就起来了,没有办法延迟启动。
  • std::thread一旦起来以后没有办法通过api它杀掉,除非通过native_handle拿到平台相关的线程句柄进行操作。这个非C++提供而是操作系统提供。

传参

启动线程的时候可以传递参数,传参的形式默认为拷贝,如果有类型转换则是在线程启动时进行。所以需要保证传递的参数不依赖于方法栈上的其他内存。

如果不满足于默认的按值拷贝,可以通过std::refstd::move进行传引用或转成右值进行move操作。下面给出几个例子。

1
2
3
4
5
6
7
8
9

void pass_by_val(int i) {}
void pass_by_ref(int &i) {}
void pass_by_move(std::unique_ptr<big_obj> bigObj){}

int i;
std::thread t1(pass_by_val, i);
std::thread t2(pass_by_ref, std::ref(i));
std::thread t3(pass_by_move, std::make_unique<big_obj>());

注意事项:

  • std::ref 注意引用的生命周期和线程的生命周期的关系。

如何销毁一个线程

你没法销毁一个线程,你只能等它跑完或者让他在后台运行并且把其管理权移交给C++运行库。

下面介绍一组和线程结束相关的api:

  • bool jionable(): 如果是默认线程(即std::thread t;)则是false。只要一个线程被join过一次以后则变为false,即使一个线程已经结束了,但是没有被join过,还是会返回true
  • void join()。等待线程结束运行,一个线程只能被调用一次,如果在jioinablefalse的情况下调用,会直接抛出std::system_error
  • void detach()。将线程放到后台运行,并且将管理权转给C++运行库。

注意事项:

  • 一个线程在析构前必须调用join或者detach来显示的处理它的生命周期,否则的话会析构时直接抛出异常。
  • join本身并非线程安全,如果多个线程在没有保护的情况下同时join同一个std::threadundefined behavior

std::thread的所有权(owenership)

No two std::thread objects may represent the same thread of execution; std::thread is not CopyConstructible or CopyAssignable, although it is MoveConstructible and MoveAssignable.
http://en.cppreference.com/w/cpp/thread/thread

std::thread 只能move不能被拷贝,所以如果想要把std::thread放进容器的话必须确保是右值。直接构造出来或者用std::move进行转换都可以。

比如下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void calc_something(int i){ std::cout << i; }

std::vector<std::thread> thread_vec;
for (int i = 0 ; i < 3; i++)
{
std::thread t(calc_something, i);
thread_vec.push_back(std::move(t)); // use std::move
thread_vec.push_back(std::thread(calc_something, i)); // 直接构建为右值
thread_vec.emplace_back(calc_something, i); // 用emplace_back
}

for(auto & t : thread_vec)
{
if (t.joinable()) t.join();
}

如何控制线程数量

线程数量并非越多越好,数量太多会导致CPU的频繁上下文切换,反而会适得其反。std::thread::hardware_concurrency静态方法可以获得。

Returns the number of concurrent threads supported by the implementation. The value should be considered only a hint.
http://en.cppreference.com/w/cpp/thread/thread/hardware_concurrency

但是这个数量仅供参考,因为依赖于不同编译器和平台的实现。有可能会返回0,表示这个数量未定义,也就是说不知道在这个硬件上有多少个并发线程可以执行。

线程ID

通过 std::thread::id std::thraed::get_id()可以拿到一个线程的id。这个id主要的用途是用来打印log和作为关系容器的key。

1
2
3
4
5
6
std::map<std::thread::id, std::thread> thread_map;
for (int i = 0 ; i < 3; i++)
{
std::thread t(calc_something, i);
thread_map[t.get_id()] = std::move(t);
}
文章目录
  1. 1. 如何创建一个线程
    1. 1.1. 传参
  2. 2. 如何销毁一个线程
  3. 3. std::thread的所有权(owenership)
  4. 4. 如何控制线程数量
  5. 5. 线程ID