线程管理基础
C++11 所有的线程都封装在<thread>
头文件中,使用命名空间std
说明。
最简单的例子:
#include <iostream>
#include <thread>
void hello() {
std::cout << "hello world !\n";
}
int main() {
std::thread t(hello); // 定义一个线程,注意是直接构造执行的
t.join(); // 在这里进行等待
return 0;
}
每个线程都必须有一个初始函数,新线程在函数中执行。join()
的作用是等待线程执行完毕,然后向下执行。该例子中,如果没有join
,那么可能出现主线程执行完毕,子线程还未完成,出现异常。
C++线程库启动线程可以归结为构造一个std::thread
对象,使用可调用类型进行构造。传入带有函数调用符类型的参数实例,可以用来替代默认的构造函数。
join
和detach
的区别:
join
是用于等待线程执行完毕后,再向下执行detach
是声明是线程再后台执行,不必等待线程执行完毕,主线程可以继续向下执行。而且此时不能有其他的std::thread
对象能引用它。- 两者都不能对没有执行的线程进行操作,保险的做法是先进行
joinable
的判断,然后调用函数。
线程使用的注意事项:尽量不要在线程中访问局部变量的引用或者指针,保险的做法是复制数据到线程中。
#include <iostream>
#include <thread>
void do_something(int i) {/*do some thing here*/}
struct func {
int& i;
func(int& i_): i(i_) {} // 注意都是引用类型!!!!
void operator()() {
for(unsigned j = 0; j < 1000000; ++j) {
do_something(i);
}
}
};
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();
}
int main(){
f();
}
使用异常的方式,使得f
函数过于繁琐,可以利用析构函数的方式简化:
#include <iostream>
#include <thread>
void do_something(int i) {
/*do some thing here*/
for(int j = 0; j < 10; ++j) {
i = j;
}
}
struct func {
int& i;
func(int& i_): i(i_) {}
void operator()() {
for(unsigned j = 0; j < 1000000; ++j) {
do_something(i);
}
}
};
class thread_guard {
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;
std::thread& t;
};
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
}
int main() {
f();
}
在f
函数中,当函数执行完成时,需要销毁局部变量。因为局部变量时存放在栈内存中的,所以首先要销毁g
,即调用g
的构造函数,此时主线程需要进行joinable
的判断,来确定子线程是否执行完毕,未结束则等待到结束。
向线程传递参数
基本方式很简单,声明函数后,依次添加对应的参数即可,类似于std::bind
。
void foo(int i, std::string const& s) {
std::cout << "i= " << i << ", s= " << s << std::endl;
}
int main() {
std::thread t(foo, 3, "hello world !");
if(t.joinable()) {
t.join();
}
return 0;
}
/*
输出结果:
i= 3, s= hello world !
*/
一般来说,如果无特殊需求,而且复制的开销比较小,那么应该给每个线程一个局部变量的复制。但是存在两个特殊情况:传递指针和传递引用。
一般来说,传递指针时要注意不要出现指针悬空的情况。传递引用的时候要使用std::ref
,这和使用std::bind
的时候类似。
void foo(int i, std::string& s) {
std::cout << "i= " << i << std::endl;
s = " " + s;
std::cout << "s= " << s << std::endl;
}
int main() {
std::string str("hello world");
std::thread t(foo, 3, std::ref(str)); // 一定要注意std::ref的使用!!!
if(t.joinable()) {
t.join();
}
std::cout << "main thread s= " << str << std::endl;
return 0;
}
如果要使用类成员函数,则需要单独声明,使用方式如下:
class X {
public:
void foo(int i) {
std::cout << "i= " << i << std::endl;
}
};
int main() {
X my_x;
std::thread t(&X::foo, &my_x, 8);
if(t.joinable()) {
t.join();
}
return 0;
}
仅仅是添加上域作用符和函数地址声明符,参数从第三个开始。
也可以
也可以使用移动std::move
的方式进行参数传递。
转移线程所有权
线程没有拷贝操作!!! ,所有权的转移都是通过std::move
实现的。这种类型的资源是不能直接赋值的,没有意义,只能直接创建。
线程的所有权可以在函数外进行转移。
std::thread f() {
void some_function();
return std::thread(some_function);
}
std::thread g() {
void some_other_function(int);
std::thread t(some_other_function, 42);
return t;
}
编译器有ROV,不用担心出现多余的拷贝,这里返回值就是相当于移动操作。
所有权可以在函数内部传递,同样的也可以作为函数的参数,不过要使用std::move
进行操作。
void f(std::thread t);
void g() {
void some_function();
f(std: thread(some_function)); // 可以直接在参数处进行构造
std::thread t(some_function); // 直接进行移动操作
f(std::move(t));
}
可以使用类构造的方式,直接传入线程,而不是各个参数。这样,可以更加简化操作。
#include <iostream>
#include <thread>
void do_something(int i) {
/*do some thing here*/
for(int j = 0; j < 10; ++j) {
i = j;
}
}
struct func {
int& i;
func(int& i_): i(i_) {}
void operator()() {
for(unsigned j = 0; j < 1000000; ++j) {
do_something(i);
}
}
};
class scoped_thread {
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;
private:
std::thread t;
};
void f() {
int some_local_state;
scoped_thread t(std::thread(func(some_local_state)));
// do something in current thread
}
int main() {
f();
}
可以使用线程容器存放线程,前提是容器支持移动操作。这里以std::vector
为例子:
#include <thread>
#include <vector>
#include <algorithm>
void do_work(){}
void f() {
std::vector<std::thread> threads;
for(unsigned i = 0; i < 20; ++i) {
threads.push_back(std::thread(do_work));
}
std::for_each(threads.begin(), threads.end(),
std::mem_fn(&std::thread::join));
补充说明:std::mem_fn
相当于一个函数适配器,返回适配后的可调用的函数对象,这里相当于对每个线程都执行一次join
操作。这种方式相当于创建了一个线程组。
运行时线程的数量
std::thread::hardware_concurrency()
函数返回CPU核心的数量或者是一个程序中最多能同时并发的函数数量。
给出一个多线程版本的std::accumulate
函数。
#include <thread>
#include <algorithm>
// 每个线程内部进行累加操作
template<typename Iterator, typename T>
struct accumulate_block {
void operator()(Iterator first, Iterator last, T& result) {
result = std::accumulate(first, last, result);
}
};
// 线程并行的函数
template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init) {
auto const length = std::distance(first, last); // 任务的数量
if(!length) // 没有任务,则返回初始值
return init;
// 每个线程最小的任务数量
unsigned long const min_per_thread = 25;
// 系统最多启动线程的个数,这种操作是防止出现0个线程的情况,同时让每个线程平均分配任务
unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads = std::thread::hardware_concurrency();
// 最多线程不能超过任务数量,也不能超过系统支持线程的数量
unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
// 每个线程实际分配的任务个数
unsigned long const block_size = length / num_threads;
// 存放累加结果
std::vector<T>results(num_threads);
// 存放线程,注意-1是因为不存放主线程
std::vector<std::thread>threads(num_threads - 1);
Iterator block_start = first;
for(unsigned long i = 0; i < (num_threads - 1); ++i) {
Iterator block_end = block_start;
// 每次累加任务个数
std::advance(block_end, block_size);
// 线程内部执行自己所属区块的累加,结果存储到对应的results中,注意引用!!
threads[i] = std::thread(accumulate_block<Iterator, T>(),
block_start, block_end, std::ref(results[i]));
block_end = block_start;
}
// 处理可能分配不均匀的块
accumulate_block<Iterator, T>()(block_start, last, results[num_threads - 1]);
// 等待所有的线程结束
std::for_each(threads.begin(), threads.end(),
std::mem_fn(&std::thread::join));
// 累加所有的结果,并返回
return std::accumulate(results.begin(), results.end(), init);
}
补充:std::accumulate()
是累加函数,std::advance()
是迭代器移动函数,必须是前向或者双向迭代器才可以。
注意:不能从线程中直接返回一个值,在这里采取引用的方式进行解决。
识别线程
C++中,线程的标识符类型是std::thread::id
。使用std::thread
的get_id()
成员直接获取。一般来说,这种方式是用来对线程进行检测的,以判断是否需要进行有关的操作。