这里常用的有三种方法,如下所示。
/* 使用函数指针表示线程入口点 */
void worker_func();
std::thread my_thread1(worker_func); // 函数指针
/*//////////////////////////////////////////////////////////////*/
/* 使用仿函数表示线程入口点 */
class mytask {
public:
void operator()() const {
do_something();
}
};
my_task task;
std::thread my_thread2(task); // 仿函数
/*//////////////////////////////////////////////////////////////*/
/* 使用 lambda 表达式表示线程入口点 */
std::thread my_thread3([](){ // lambda 表达式
do_something();
});
线程对象如果用以上方式被构造,那么就会启动线程,如果放任不管,当线程对象被析构之时(比如脱离它的作用域之后),它的析构函数就会默认调用 std::terminate()
终止这个线程。
线程安全:当需要分离线程的时候,需要保证线程需要使用到的资源在分离后依然有效。
/* 错误的示例 */
struct func {
int & _i;
func(int & i) : _i(i) {}
void operator()() {
for (auto j = 0; j < 1000000; ++j)
do_some_thing(i);
}
};
void foo() {
int local = 0;
func my_func(local);
std::thread my_thread(my_func);
my_thread.detach(); // 线程分离,但是局部变量 local 将会被析构,线程引用了它的地址,这时线程如果还在执行,极有可能产生错误
}
可以通过调用 thread::join()
函数连接到线程,来等待这个线程执行完毕。
thread::join()
只能等待这个线程执行完毕,如果需要更精细的控制,现代 C++ 还提供了条件变量和期待 (futures)。
调用 join()
是不可逆转的,它将清理线程相关的存储,std::thread
对象将不再与任何已结束的线程有关联,即对一个线程只能调用一次 join()
,一旦已调用过 join()
就无法再次连接,同时地,joinable()
函数将返回 false。
分离 (detach) 的情形:构造线程之后,可立刻调用 detach()
进行分离。
连接 (join) 的情形:需要细心挑选调用 join()
的位置,如果在调用前有异常抛出,那么 join()
函数很容易跳过。并且,由于 std::thread
对象的析构,线程将终止执行。这显然都不是我们想要的结果。以下是两种妥善处理这种情形的代码。
void f() {
int local = 0;
func my_func(local);
std::thread t(my_func);
try {
may_cause_exception();
} catch (...) {
t.join(); // 捕获异常之后先 join,线程结束之后再向上抛出异常
throw;
}
// 没有发生异常,照常调用
t.join();
}
这种方法可以利用析构函数,在其中放置 join()
代码
class thread_guard : boost::noncopyable {
std::thread & _t;
public:
explicit thread_guard(std::thread & t) : _t(t) {}
~thread_guard() {
if (_t.joinable()) {
_t.join();
}
}
}
void f() {
int local = 0;
func my_func(local);
std::thread t(my_func);
thread_guard g(t);
may_cause_exception(); // 出现异常之后,线程守护对象 g 将析构,将调用 join() 连接到线程 t
}
如果不需要等待线程结束,那么使用 detach() 函数分离线程通常是一个很好的选择,这将使得线程本体不在于 std::thread 对象关联,析构过程将不会使得线程终止。
线程分离之后,就无法用直接的方法连接到线程,而分离的线程确确实实在后台运行,所有权和控制权将传递给 C++ 运行时库,它将保证最终线程的相关资源会被正确地回收。
分离线程通常也叫做守护线程 (daemon thread) 。以下是一些逻辑上的示例,实现了一个多文档编辑器。
void edit_doc(const std::string & filename) {
open_doc_and_display_gui(filename);
while (!done_editing()) {
user_cmd cmd = get_user_input();
if (cmd.type == OPEN_NEW_INPUT) {
const std::string new_name = get_filename_from_user();
std::thread t(edit_doc, new_name); // 启动新线程
t.detach(); // 分离线程
} else {
process_user_input(cmd);
}
}
}
一个引例:
void f(int i, const std::string & s);
std::thread t(f, 3, "hello");
延伸(传递自动变量一定要小心):
void f(int i, const std::string & s);
void oops(int some_param) {
char buffer[1024];
sprintf(buffer, "%i", some_param);
std::thread t(f, 3, buffer); // 将 buffer 传送进线程函数
t.detach(); // 分离线程,接着本函数返回。buffer 会被销毁,尤其是可能在它从 char * 隐式转换到 std::string 的过程还没完成之前,buffer 就已经被析构了,所以这些代码不是线程安全的
}
改良(先转换再分离):
void f(int i, const std::string & s);
void oops(int some_param) {
char buffer[1024];
sprintf(buffer, "%i", some_param);
std::thread t(f, 3, std::string(buffer)); // 将 buffer 转换为 std::string 后传送进线程函数,确保分离之后是安全的
t.detach(); // 分离线程,接着本函数返回
}
示例:
void update_data_for_widget(widget_id w, widget_data & data);
void oops_again(widget_id w) {
widget_data data;
std::thread t(update_data_for_widget, w, data); // 试图传送 data 的引用
display_status();
t.join();
process_widget_data(data);
}
根据 std::thread
构造函数的机制,它在默认情况下只会拷贝对象(作为右值,为了照顾只能移动构造的对象),而不是真的传递引用。
上述代码将导致一个编译错误,因为没办法将一个右值传递到非 const 的左值引用。
所以需要使用 std::ref
将参数包装成引用的形式,故而改为如下代码。
std::thread t(update_data_for_widget, w, std::ref(data));
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_object, std::move(p)); // 传递只可移动的 unique_ptr 对象
🖊️ 本文由 Alone Café 创作,如果您觉得本文让您有所收获,请随意赞赏 🥺
⚖️ 本文以 CC BY-NC-SA 4.0,即《署名-非商业性使用-相同方式共享 4.0 国际许可协议》进行许可
👨⚖️ 本站所发表的文章除注明转载或出处外,均为本站作者原创或翻译,转载前请务必署名并遵守上述协议
🔗 原文链接:https://alone.cafe/2021/10/cpp并发编程笔记-1
📅 最后更新:2021年10月29日 Friday 23:14
Update your browser to view this website correctly. Update my browser now