1.线程管理的基础
1.1启动线程
使用C++线程库启动线程,可以归结为构造 std::thread 对象。
关于一个thread对象是否是joinable:
如果一个线程正在执行,那么它是joinable的
下列任一情况,都是非joinable:
A:默认构造器构造的。
B:通过移动构造获得的。
C:调用了join或者detach方法的。
版权声明:以上图片为CSDN博主「小罗tongxue」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44843859/article/details/112170599
提供的函数对象会复制到新线程的存储空间中,函数对象的执行和调用都在线程的内存空间中进行。
注意:当把函数对象传入到线程构造函数中,如果传递了一个临时变量,而不是一个命名变量,C++编译器会将其解析为函数声明,而不是类型对象的定义。
std::thread my_thread(background_task()); //编译器将其看为一个函数
class background_task
{
public:
void operator()() const //伪函数
{
do_something();
do_something_else();
}
};
可以使用:
1.命名对象的方式:
background_task f;
std::thread my_thread(f);
2.多组括号
std::thread my_thread( (background_task()) )
3.新统一的初始化语法
std::thread my_thread{background_task()}
4.使用lambda表达式:允许使用一个可以捕获局部变量的局部函数
std::thread my_thread([]{
do_something();
do_something_else();
});
启动了线程,必须明确是等待线程结束(加入式),还是让其自主运行(分离式)。如果std::thread对象销毁前还没做出决定,程序就会终止(std::thread析构函数会调用std::terminate())。因此,即便有异常存在,也需要确保线程能够正确加入或分离。
如果不等待线程,就必须保证线程结束之前,可访问数据的有效性。如果线程还没结束,主函数已经退出,这时线程函数还持有函数局部变量的指针或引用,就会引发错误。
处理这种情况的常规方法:
a.使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中(共享数据第三章介绍,后边在补概念);
b.此外,可以通过join()函数确保线程在函数完成前结束。
1.2等待线程完成
需要等待线程完成,相关std::thread实例需要使用join()。
如果需要对等待时间中的线程有更灵活的控制,比如只等待一段时间,需要使用其他机制来完成,比如条件变量和期待。
只能对一个线程使用一次join()的行为,因为该行为清理了线程相关的存储部分,std::thread对象与已经完成的线程没有任何关联了。
1.3特殊情况下等待
为了避免在join()还未调用前,程序就因异常而终止这样的情况,我们需要在异常处理过程中调用join(),从而避免生命周期的问题。
方法一:使用try/catch块
struct func;
void f()
{
int some_local_state = 0;
func my_func(some_local_state); //结构体里有伪函数,传递的参数用于构造这个结构体
std::thread t(my_func); //线程开启
try
{
do_some_thing_in_current_thread(); //正常运行
}
catch (...)
{
t.join(); //发生异常,先加入,在抛出异常
throw;
}
t.join(); //正常退出线程,会在这里加入
}
方法二:使用RAII(资源获取即初始化方式)等待线程完成
//提供一个类,在析构函数中使用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是第一个销毁的,在析构函数中,判断t是否已经加入
1.4后台运行线程
方法:使用detach()
解释:使用detach()会让程序在后台运行,主线程和线程不能直接进行交互,并且std::thread()对象不能再引用线程,但C++运行库保证,线程退出后,相关资源能够正确的回收。
意义:分离线程也被称为守护线程,因为没有任何显式用户接口,线程的生命周期可能会很长,例如后台监视文件系统、对缓存进行清理等应用。
条件:要从std::thread()对象中分离线程,必须这个线程是joinable()才可以。(先判断,再分离)
示例:文字处理应用同时编辑多个文档
//让每个文档处理窗口都有自己的线程;每个线程运行相同的代码,并隔离不同窗口的数据
///因为是对独立的文档进行操作,没必要等其他线程完成
//让文档处理窗口运行在分离的线程上
void edit_document(std::string const& filename)
{
open_document_and_display_gui(filename);
while (!done_editing())
{
user_command cmd = get_user_input();
if (cmd.type == open_new_document)
{
std::string const new_name = get_filename_from_user();
std::thread t(edit_document, new_name); //传递参数
t.detach();
}
else
{
process_user_input(cmd);
}
}
}
2.向线程中传递参数
注意:std::thread对象默认对所有参数都进行复制拷贝,即便是参数引用的形式。
2.1正常情况
void edit_document(std::string const& filename);
std::thread t(edit_document, new_name); //传递参数
向std::thread对象构造函数直接传递参数。
2.2涉及到参数的隐式转换
如果不涉及指针,在线程的上下文中会自动完成类型的隐式转换,但如果传入的是一个指针,由于对默认参数的拷贝只会拷贝数据,并不会拷贝该数据是什么类型,就会导致std::thread的构造函数不知道该将其转换为什么类型,这就是悬垂指针,为了避免该情况,应该在构造函数之前就将指针主动给转换为正确类型。
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); //不对
std::thread t(f,3,std::string(buffer)) //正确
}
2.3传递引用参数
使用std::ref将参数转换为引用形式,
std::thread t(update_data_for_weight, w, std::ref(data));
2.4传递动态对象
std::thread对象提供的参数可以移动,不能拷贝(移动类似剪切),类似于智能指针,只能指向一个对象,当这个指针被销毁,指向的对象也会被删除。使用移动转移原对象后,就会留下一个空指针。
(仅当需要的参数 原对象是个动态且是一个命名变量才需要这样)移动操作可以将对象转换为可接受的类型。当原对象是一个临时变量,自动进行移动操作,当原对象是一个命名变量,转移的时候需要使用std::move()进行显式移动。
示例: std::move是如何转移一个动态对象到一个线程中:
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_data, std::move(p));
big_object对象所有权收益按转移到新建立线程的内部存储,之后传递给process_big_object函数。
std::thread不像std::unique_ptr那样能占有一个对象的所有权,但能占有其他资源,执行线程的所有权可以在多个std::thread对象中转移,(std::thread实例可移动且不可复制性),不可复制性保证了在同一时间,一个std::thread实例只能关联一个执行线程。
3.转移线程所有权
原因:创建一个线程,并在函数中转移所有权,都必须等线程结束,想在线程不结束的时候直接对线程所有权进行转移。
示例一:
void some_function();
void some_other_function();
std::thread t1(some_function);
std::thread t2 = std::move(t1); //t1的所有权转移给t2,与t1无关
t1 = std::thread(some_other_function); //一个临时std::thread开启了新线程,转移给t1,
//为什么不使用std::move,因为是临时变量,移动
//操作隐式调用
std::thread t3; //默认方式构造t3
t3 = std::move(t2); //t2线程所有权转移给t3
t1 = std::move(t3); //t1有线程,转移失败,终止程序,不抛出异常
//保证与std::thread的析构函数的行为一致
在线程对象被析构前,显式的等待线程完成,或者分离它,赋值同样需要满足这些条件。
terminate()函数调用exit()结束当前程序时,会调用析构函数;
terminate()函数调用abort()结束当前程序时,不会调用析构函数,防止在析构函数中抛出异常。
C++默认调用abort()函数,并且不要在析构函数中抛出异常,因为析构函数是用来释放资源的,如果抛出异常,资源无法释放,会导致全局的结束函数terminate()函数被反复调用。
Terminate()是整个程序释放系统资源最后的机会。
std::thread支持移动,表明线程所有权可以在函数外进行转移:
示例二:函数返回一个thread对象
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;
}
示例三:所有权可以在函数内部传递,std::thread实例可以作为参数进行传递
void f(std::thread t);
void g()
{
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std::move(t)); //t所对应的线程传递给函数f中
}
示例四:
//为了保证线程程序退出前完成,定义scope_thread类
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();
}
新线程直接传递到scoped_thread中,而非创建一个独立变量。thread_guard类在析构中检查线程是否可以加入,这里把检查放在了构造函数中,并且当线程不可加入时,抛出异常。
4.运行时线程数量的确定
std::thread::hardware_concurrency()函数能返回能在一个程序中并发的线程数量。例如,多核系统,返回值可以是CPU核芯的数量。返回值仅是一个提示,当系统信息无法获取,函数也会返回0。
原生并行版std::accumulate()函数的实现:
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 paraller_accumulate(Iterator first, Iterator last, T init)
{
unsigned long const length = std::distance(first, last); //判断长度
if (!length)
return init; //长度为0,返回默认值
unsigned long const min_per_thread = 25; //每分钟线程处理的数据量
unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread; //计算需要使用的线程数量(数据量来计算)
unsigned long const hard_threads = std::thread::hardware_concurrency();
unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads); //在max_threads和hard_threads(2)选取较小那个
unsigned long const block_size = length / num_threads; //真正数量
std::vector<T> results(num_therads); //存放中间结果
std::vector<std::therad> 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); //前进n
threads[i] = std::thread(accumulate_block<Iterator, T>(), block_start, block_end, std::ref(result[i]));
block_start = block_end;
}
accumulate_block<Iterator, T>() (block_start, last, results[nums_therads - 1]); //处理最终块的结果
//最终块的数据可能不满,单独处理
//当累加最终块的结果后,让所有线程join()
std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));
return std::accumulate(results.begin(), results.end(), init); //结果累加
}
注意:因为不能直接从一个线程中返回一个值,所以需要传递results容器引用到线程中去;还可以使用地址来获取线程的结果(CH4介绍)。
例子中,T类型的加法运算不满足结合律(float和double数据相加,可能会有截断操作)可能会导致parallel_accumulate得到结果与std::accumulate结果不同。
迭代器必须是前向迭代器,std::accumulate只要是迭代器就可以工作。
创造出results容器,必须保证T有默认构造函数。
对于线程运行时,必须所有的信息都要传递到线程中去,包括存储计算结果的位置。但并非总是如此,如何是一个识别线程的任务,可以传递一个标识数。
5.标识线程
线程标识类型为 std::thread::id,可以通过两种方式检索。
方法一
调用std::thread对象的成员函数get_id()来直接获取,如果对象与没有与线程相关,函数将返回std::thread::type默认构造值。
方法二
在当前线程中调用std::this_thread::get_id()。
作用:
std::thread::id类型对象提供了非常丰富的对比操作,可以将其作为容器的键值,做排序,或做其他方式的比较。std::thread::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_commom_work();
}
可以将当前线程的std::thread::id存储到一个数据结构中,之后在这个结构体中对当前线程ID与存储的线程ID做对比,来决定操作。
此外,线程和本地存储不适配,可以将线程ID在容器中作为键值,作为将线程存储到本地的代替方案。
std::thread::id可以作为线程的通用标识符,当标识符只与语义相关(类似数组索引),就可以这样使用,也可以将id输出来记录std::thread::id对象的值。
std::cout<<std::this_thread::get_id();