不同平台的多线程
POSIX线程
Win32线程
跨平台的多线程
C++11线程
- std::thread::thread
Windows平台运行的VS2012和Linux平台运行的g++4.7,都完美支持C++11线程
C11线程
- thrd_create
第三方库
- 通过条件编译宏屏蔽平台差异
- boost
- QT
- ACE
C11线程仅仅是个“建议标准”,也就是说100%遵守C11标准的C编译器是可以不支持C11线程的。根据C11标准的规定,只要编译器预定义了 STDC_NO_THREADS(C11),就可以没有<threads.h>头文件,也就不支持thrd相关函数。
你好,并发世界
#include <iostream>
#include <thread> //
void hello() //②
{
std::cout << "Hello Concurrent World\n";
}
int main()
{
std::thread t(hello); //③
t.join(); //④
}
新标准中的并发支持
C++11标准的发布提供了一个全新的线程感知内存模型,C++标准库也被扩展了,包含了用于管理线程(参见第2章)、保护共享数据(参见第3章)、线程间同步操作(参见第4章)以及低级原子操作(参见第5章)的各个类。
启动线程
线程在 std::thread 对象创建(为线程指定任务)时启动。最简单的情况下,任务也会很简单,通常是无参数无返回(void-returning)的函数。这种函数在其所属线程上运行,直到函数执行完毕,线程也就结束了。在一些极端情况下,线程运行时,任务中的函数对象需要通过某种通讯机制进行参数的传递,或者执行一系列独立操作;可以通过通讯机制传递信号,让线程停止。线程要做什么,以及什么时候启动,其实都无关紧要。总之,使用C++线程库启动线程,可以归结为构造 std::thread 对象:
void do_some_work();
std::thread my_thread(do_some_work);
如同大多数C++标准库一样, std::thread 可以用可调用(callable)类型构造,将带有函数调用符类型的实例传入 std::thread 类中,替换默认的构造函数。
启动了线程,你需要明确是要等待线程结束,还是让其自主运行。如果 std::thread 对象销毁之前还没有做出决定,程序就会终止( std::thread 的析构函数会调用 std::terminate() )。因此,即便是有异常存在,也需要确保线程能够正确的加入(joined)或分离(detached)。需要注意的是,必须在 std::thread 对象销毁之前做出决定——加入或分离线程。
如果不等待线程,就必须保证线程结束之前,可访问的数据得有效性。这不是一个新问题——单线程代码中,对象销毁之后再去访问,也会产生未定义行为——不过,线程的生命周期增加了这个问题发生的几率。
创建线程的几种方式:
- 全局函数
std::thread tFunction(background_func);
tFunction.join();
- 函数对象
background_task task;
std::thread tFunctionObject(task);
- 成员函数
background_wraper oBackgroundWraper;
std::thread tMemberFunction(std::bind(&background_wraper::background, &oBackgroundWraper));
- 静态成员函数
std::thread tStaticMemberFunction(background_wraper ::staticBackground);
- Lambda表达式
std::thread tLambda([]()
{
cout << "lambda" << endl
<< "子线程ID为:" << this_thread::get_id() << endl << endl;
});
等待线程完成
如果需要等待线程,相关的 std::tread 实例需要使用join()。在这种情况下,因为原始线程在其生命周期中并没有做什么事,使得用一个独立的线程去执行函数变得收益甚微,但在实际编程中,原始线程要么有自己的工作要做;要么会启动多个子线程来做一些有用的工作,并等待这些线程结束。
join()是简单粗暴的等待线程完成或不等待。当你需要对等待中的线程有更灵活的控制时,比如,看一下某个线程是否结束,或者只等待一段时间(超过时间就判定为超时)。想要做到这些,你需要使用其他机制来完成,比如条件变量和期待(futures),相关的讨论将会在第4章继续。调用join()的行为,还清理了线程相关的存储部分,这样 std::thread 对象将不再与已经完成的线程有任何关联。这意味着,只能对一个线程使用一次join();一旦已经使用过join(), std::thread 对象就不能再次加入了,当对其使用joinable()时,将返回否(false)。
特殊情况下的等待
如前所述,需要对一个还未销毁的 std::thread 对象使用join()或detach()。如果想要分离一个线程,可以在线程启动后,直接使用detach()进行分离。如果打算等待对应线程,则需要细心挑选调用join()的位置。当在线程运行之后产生异常,在join()调用之前抛出,就意味着很这次调用会被跳过。
避免应用被抛出的异常所终止,就需要作出一个决定。通常,当倾向于在无异常的情况下使用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_something_in_current_thread();
} c
atch(...)
{
t.join(); // 1
throw;
} t
.join(); // 2
}
- RAII
class thread_guard
{
std::thread& t;
public:
explicit thread_guard(std::thread& t_) :
t(t_)
{}
~thread_guard()
{
if (t.joinable()) // 1
{
t.join(); // 2
}
}
thread_guard(thread_guard const&) = delete; // 3
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();
} // 4
后台运行线程
使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束;如果线程分离,那么就不可能有 std::thread 对象能引用它,分离线程的确在后台运行,所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。
使用分离线程去使用文档
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); // 1
t.detach(); // 2
}
else
{
process_user_input(cmd);
}
}
}
引用和指针传递
- 基础类型与普通函数调用无异
void func1(int &i)
{
i = 9;
}
void create_thread()
{
int i = 0;
std::thread t1(func1, i);
t1.join();
}
- 对象需加std::ref
void func2(string & str)
{
str = "new name";
}
void create_thread()
{
string str = "old name";
std::thread t2(func2, str);
t2.join();
}
- 指针传递
void func3(string * str)
{
*str = "new name with point";
}
void create_thread()
{
std::thread t3(func3, &str);
t3.join();
}
【下一篇】
[C++并发编程实战]线程间共享数据