设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

使用C++11编写Linux多线程程序

2014-12-16 11:23| 发布者: joejoe0332| 查看: 4165| 评论: 0|原作者: 朱广旭|来自: IBM DeveloperWorks

摘要: 多线程可以让我们的应用程序拥有更加出色的性能,同时,如果没有用好,多线程又是比较容易出错的且难以查找错误所在,甚至可以让人们觉得自己陷进了泥潭,希望本文能够帮助您更好地使用 C++11 来进行 Linux 下的多线 ...

前言

  在这个多核时代,如何充分利用每个 CPU 内核是一个绕不开的话题,从需要为成千上万的用户同时提供服务的服务端应用程序,到需要同时打开十几个页面,每个页面都有几十上百个链接的 web 浏览器应用程序,从保持着几 t 甚或几 p 的数据的数据库系统,到手机上的一个有良好用户响应能力的 app,为了充分利用每个 CPU 内核,都会想到是否可以使用多线程技术。这里所说的“充分利用”包含了两个层面的意思,一个是使用到所有的内核,再一个是内核不空闲,不让某个内核长时间处于空闲状态。在 C++98 的时代,C++标准并没有包含多线程的支持,人们只能直接调用操作系统提供的 SDK API 来编写多线程程序,不同的操作系统提供的 SDK API 以及线程控制能力不尽相同,到了 C++11,终于在标准之中加入了正式的多线程的支持,从而我们可以使用标准形式的类来创建与执行线程,也使得我们可以使用标准形式的锁、原子操作、线程本地存储 (TLS) 等来进行复杂的各种模式的多线程编程,而且,C++11 还提供了一些高级概念,比如 promise/future,packaged_task,async 等以简化某些模式的多线程编程。

  多线程可以让我们的应用程序拥有更加出色的性能,同时,如果没有用好,多线程又是比较容易出错的且难以查找错误所在,甚至可以让人们觉得自己陷进了泥潭,希望本文能够帮助您更好地使用 C++11 来进行 Linux 下的多线程编程。


认识多线程

  首先我们应该正确地认识线程。维基百科对线程的定义是:线程是一个编排好的指令序列,这个指令序列(线程)可以和其它的指令序列(线程)并行执行,操作系统调度器将线程作为最小的 CPU 调度单元。在进行架构设计时,我们应该多从操作系统线程调度的角度去考虑应用程序的线程安排,而不仅仅是代码。

  当只有一个 CPU 内核可供调度时,多个线程的运行示意如下:

图 1、单个 CPU 内核上的多个线程运行示意图

  我们可以看到,这时的多线程本质上是单个 CPU 的时间分片,一个时间片运行一个线程的代码,它可以支持并发处理,但是不能说是真正的并行计算。

  当有多个 CPU 或者多个内核可供调度时,可以做到真正的并行计算,多个线程的运行示意如下:


图 2、双核 CPU 上的多个线程运行示意图


  从上述两图,我们可以直接得到使用多线程的一些常见场景:

  • 进程中的某个线程执行了一个阻塞操作时,其它线程可以依然运行,比如,等待用户输入或者等待网络数据包的时候处理启动后台线程处理业务,或者在一个游戏引擎中,一个线程等待用户的交互动作输入,另外一个线程在后台合成下一帧要画的图像或者播放背景音乐等。
  • 将某个任务分解为小的可以并行进行的子任务,让这些子任务在不同的 CPU 或者内核上同时进行计算,然后汇总结果,比如归并排序,或者分段查找,这样子来提高任务的执行速度。

需要注意一点,因为单个 CPU 内核下多个线程并不是真正的并行,有些问题,比如 CPU 缓存不一致问题,不一定能表现出来,一旦这些代码被放到了多核或者多 CPU 的环境运行,就很可能会出现“在开发测试环境一切没有问题,到了实施现场就莫名其妙”的情况,所以,在进行多线程开发时,开发与测试环境应该是多核或者多 CPU 的,以避免出现这类情况。


C++11 的线程类 std::thread

  C++11 的标准类 std::thread 对线程进行了封装,它的声明放在头文件 thread 中,其中声明了线程类 thread, 线程标识符 id,以及名字空间 this_thread,按照 C++11 规范,这个头文件至少应该兼容如下内容:


  清单 1.例子 thread 头文件主要内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
namespace std{
 struct thread{
 // native_handle_type 是连接 thread 类和操作系统 SDK API 之间的桥梁。
 typedef implementation-dependent native_handle_type;
 native_handle_type native_handle();
 //
 struct id{
 id() noexcept;
 // 可以由==, < 两个运算衍生出其它大小关系运算。
 bool operator==(thread::id x, thread::id y) noexcept;
 bool operator<(thread::id x, thread::id y) noexcept;
 template<class charT, class traits>
 basic_ostream<charT, traits>&
 operator<<(basic_ostream<charT, traits>&out, thread::id id);
 // 哈希函数
 template <class T> struct hash;
 template <> struct hash<thread::id>;
 };
 id get_id() const noexcept;
 // 构造与析构
 thread() noexcept;
 template<class F, class… Args> explicit thread(F&f, Args&&… args);
 ~thread();
 thread(const thread&) = delete;
 thread(thread&&) noexcept;
 thread& operator=( const thread&) = delete;
 thread& operator=(thread&&) noexcept;
 //
 void swap(thread&) noexcept;
 bool joinable() const noexcept;
 void join();
 void detach();
 // 获取物理线程数目
 static unsigned hardware_concurrency() noexcept;
 }
 namespace this_thead{
 thread::id get_id();
 void yield();
 template<class Clock, class Duration>
 void sleep_until(const chrono::time_point<Clock, Duration>& abs_time);
 template<class Rep, class Period>
 void sleep_for(const chromo::duration<Rep, Period>& rel_time);
 }
}

  和有些语言中定义的线程不同,C++11 所定义的线程是和操作系的线程是一一对应的,也就是说我们生成的线程都是直接接受操作系统的调度的,通过操作系统的相关命令(比如 ps -M 命令)是可以看到的,一个进程所能创建的线程数目以及一个操作系统所能创建的总的线程数目等都由运行时操作系统限定。


  native_handle_type 是连接 thread 类和操作系统 SDK API 之间的桥梁,在 g++(libstdc++) for Linux 里面,native_handle_type 其实就是 pthread 里面的 pthread_t 类型,当 thread 类的功能不能满足我们的要求的时候(比如改变某个线程的优先级),可以通过 thread 类实例的 native_handle() 返回值作为参数来调用相关的 pthread 函数达到目的。thread::id 定义了在运行时操作系统内唯一能够标识该线程的标识符,同时其值还能指示所标识的线程的状态,其默认值 (thread::id()) 表示不存在可控的正在执行的线程(即空线程,比如,调用 thead() 生成的没有指定入口函数的线程类实例),当一个线程类实例的 get_id() 等于默认值的时候,即 get_id() == thread::id(),表示这个线程类实例处于下述状态之一:

  • 尚未指定运行的任务
  • 线程运行完毕
  • 线程已经被转移 (move) 到另外一个线程类实例
  • 线程已经被分离 (detached)


  空线程 id 字符串表示形式依具体实现而定,有些编译器为 0×0,有些为一句语义解释。


  有时候我们需要在线程执行代码里面对当前调用者线程进行操作,针对这种情况,C++11 里面专门定义了一个名字空间 this_thread,其中包括 get_id() 函数可用来获取当前调用者线程的 id,yield() 函数可以用来将调用者线程跳出运行状态,重新交给操作系统进行调度,sleep_until 和 sleep_for 函数则可以让调用者线程休眠若干时间。get_id() 函数实际上是通过调用 pthread_self() 函数获得调用者线程的标识符,而 yield() 函数则是通过调用操作系统 API sched_yield() 进行调度切换。


如何创建和结束一个线程

  和 pthread_create 不同,使用 thread 类创建线程可以使用一个函数作为入口,也可以是其它的 Callable 对象,而且,可以给入口传入任意个数任意类型的参数:


  清单 2.例子 thread_run_func_var_args.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
int funcReturnInt(const char* fmt, ...){
 va_list ap;
 va_start(ap, fmt);
 vprintf( fmt, ap );
 va_end(ap);
 return 0xabcd;
}
void threadRunFunction(void){
 thread* t = new thread(funcReturnInt, "%d%s\n", 100, "\%");
 t->join();
 delete t;
}
我们也可以传入一个 Lambda 表达式作为入口,比如:

  清单 3.例子 thread_run_lambda.cc
1
2
3
4
5
6
7
8
9
10
11
void threadRunLambda(void){
 int a = 100,
 b = 200;
 thread* t = new thread( [](int ia, int ib){
 cout << (ia + ib) << endl;
 },
 a,
 b );
 t->join();
 delete t;
}


  一个类的成员函数也可以作为线程入口:


  清单 4.例子 thread_run_member_func.cc
1
2
3
4
5
6
7
8
9
10
11
struct God{
 void create(const char* anything){
 cout << "create " << anything << endl;
 }
};
void threadRunMemberFunction(void){
 God god;
 thread* t = new thread( &God::create, god, "the world" );
 t->join();
 delete t;
}


  虽然 thread 类的初始化可以提供这么丰富和方便的形式,其实现的底层依然是创建一个 pthread 线程并运行之,有些实现甚至是直接调用 pthread_create 来创建。


  创建一个线程之后,我们还需要考虑一个问题:该如何处理这个线程的结束?一种方式是等待这个线程结束,在一个合适的地方调用 thread 实例的 join() 方法,调用者线程将会一直等待着目标线程的结束,当目标线程结束之后调用者线程继续运行;另一个方式是将这个线程分离,由其自己结束,通过调用 thread 实例的 detach() 方法将目标线程置于分离模式。一个线程的 join() 方法与 detach() 方法只能调用一次,不能在调用了 join() 之后又调用 detach(),也不能在调用 detach() 之后又调用 join(),在调用了 join() 或者 detach() 之后,该线程的 id 即被置为默认值(空线程),表示不能继续再对该线程作修改变化。如果没有调用 join() 或者 detach(),那么,在析构的时候,该线程实例将会调用 std::terminate(),这会导致整个进程退出,所以,如果没有特别需要,一般都建议在生成子线程后调用其 join() 方法等待其退出,这样子最起码知道这些子线程在什么时候已经确保结束。


  在 C++11 里面没有提供 kill 掉某个线程的能力,只能被动地等待某个线程的自然结束,如果我们要主动停止某个线程的话,可以通过调用 Linux 操作系统提供的 pthread_kill 函数给目标线程发送信号来实现,示例如下:



酷毙

雷人
1

鲜花

鸡蛋

漂亮

刚表态过的朋友 (1 人)

  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部