本章目标
- 明确线程的基本组成,以及与进程之间对比
- 描述设计多线程的好处和挑战
- 隐式多线程的多种方式:线程池、fork-join、Grand Central Dispatch
- 描述windows和linux操作系统如何使用线程
- 使用Pthreads、Java和Windows的线程API设计多线程应用
Abbr.
GCD: Grand Central Dispatch
TBB: Thread Building Blocks
TLS: Thread-Local Storage
LWP: LightWeight Process
4.1 Overview
- 线程是CPU调度的基本单元
- 组成
- 线程ID
- PC(Program Counter)
- 寄存器组
- 栈
- 同个进程下的线程共享
- 代码段
- 数据段
- 其余OS资源,比如打开的文件和信号等
- 好处
- 响应度:一个线程宕机了别的线程可以继续执行
- 资源共享:进程只能通过共享内存和信息传递共享资源,但线程默认是共享同个进程下的内存和资源的
- 性价比高:创建线程分配内存和资源、切换上下文所需的消耗比进程少得多
- 可拓展性scalability:多核架构下方便并行化
下图例子:客户端每发送一个请求,服务器就创建一个新的线程
4.2 多核编程
- 多核编程的挑战
- ·辨别可以分离、并发的任务
- 平衡:并行化所带来的收益是否值得
- 数据划分:如何将数据分配到不同的task里
- 数据依赖:多个task之间的数据若有依赖性需要做好同步(chap 6)等操作
- 测试和debug更具挑战性
Amdahl’s Law: 当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度
- 并行化种类
- 数据并行:每个task处理的数据是分隔开的
- 任务并行:数据不分割,多个task可能处理同一数据
4.3 多线程模型
- 线程类别
- 用户线程
- 内核线程:直接由OS支持和管理
- 多线程模型
-
多对一
- 由用户管理,高效
- 本质上还是只有一个kernel,并发性弱
- 一个线程block就会导致整个进程block
- 例子:Solaris Green Threads;GNU Portable Threads
-
一对一
- 用户开启的线程数量受到kernel资源的限制
- 例子:Windows NT/XP/2000、Linux、Solaris 9 and later
-
多对多
- 灵活但实现困难
-
Two-level
- 多对多,同时也允许一个用户线程绑定一个内核线程
-
4.4 线程库
线程库在用户曾难免提供创建和管理线程的API。
两种实现方式:库完全在用户层面;内核层面的库由OS直接支持。
以下代码段的例子为求和计算。
POSIX Pthreads
- 提供用户层面和内核层面库。
- A POSIX standard (IEEE 1003.1c) API for thread creation and synchronization
#include <pthread.h>
#include <stdio.h>
int sum; // 线程间共享的数据
void *runner(void *param); // 线程
int main(int argc, char *argv[]){
pthread_t tid;
pthread_attr_t attr;
if(argc != 2){
fprintf(stderr, "usage: a.out <integer value>\n");
return -1;
}
if(atoi(atgv[1]) > 0){
fprintf(stderr, "%d must be >= 0\n", atoi(argv[1]));
return -1;
}
pthread_attr_init(&attr); // 获取默认属性
pthread_create(&tid, &attr, runner, argv[1]); // 创建线程,产生对应id
pthread_join(tid, NULL); // 主程序main等待线程结束后继续
printf("sum = %d\n", sum);
}
void *runner(void *param){
int i, upper = atoi(param);
sum = 0;
for(i = 1; i <= upper; i ++)
sum += i;
pthread_exit(0);
}
Win32
内核层面库。
#include <windows.h>
#include <stdio.h>
DWORD Sum; // 线程间共享的数据
DWORD WINAPI Summation(LPVOID Param){
DWORD Upper = *(DWORD*)Param;
for(DWORD i = 0; i <= Upper; i ++)
Sum += i;
return 0;
}
int main(int argc, char *argv[]){
DWORD ThreadId;
HANDLE ThreadHandle;
int Param;
// 略一些参数检查
// 创建线程
ThreadHandle = CreateThread(
NULL, // 默认的安全属性
0, // 默认的栈的size
Summation, // 线程所要执行的函数
&Param, // 函数所需的参数
0, // 默认的创建flag
&ThreadId // 返回线程id
);
if(ThreadHandle != NULL){
// 等待线程执行结束
WaitForSingleObject(ThreadHandle, INFINITE);
// 关闭线程句柄
CloseHandle(ThreadHandle);
printf("sum = %d\n", Sum);
}
}
Java
直接通过Java程序创建和管理线程。可以
1. 创建一个新的class,继承Thread class并重写run 方法
2. 定义一个实现runnable interfave的class
4.5 隐式多线程
为了更高效更可靠地创建和管理线程,现在逐步将这些任务从开发者身上转移到编译器和run-time库,这种策略就叫做隐式多线程(Implicit Threading)。
线程池
创建一定数量的线程放到pool里待命,有request就在pool里找空闲的线程执行。线程执行完后会再次被放进pool里。如果没有空闲的线程,task会被放到一个队列里等待被执行。
Fork Join
在chap 3中系统调用fork表示创建一个新的进程,在多线程程序下,fork和exec的语义有所不同。
一个线程fork,有两种情形,如果fork之后立即执行exec,则只复制该线程,否则,会复制线程所在进程下的所有线程。
OpenMP
Open Multi-Processing
参考:(46条消息) 并行编程OpenMP基础及简单示例_-牧野-的博客-CSDN博客_openmp
OpenMP是一种用于共享内存并行系统的多线程程序设计方案,支持的编程语言包括C、C++和Fortran。OpenMP提供了对并行算法的高层抽象描述,特别适合在多核CPU机器上的并行程序设计。编译器根据程序中添加的pragma指令,自动将程序并行处理,使用OpenMP降低了并行编程的难度和复杂度。当编译器不支持OpenMP时,程序会退化成普通(串行)程序。程序中已有的OpenMP指令不会影响程序的正常编译运行。
例如,运行以下代码,系统有几个core就会产生几个线程,输出几次。
#pragma omp parallel
{
printf("I am a parallel region.");
}
Grand Central Dispatch
由Apple开发,通过将任务放置到派遣队列dispatch queue里来进行运行时调度。略。
Intel TBB
由C++编写的设计并行程序的模板库,不需要特殊的编译器或语言。不必关注线程,专注任务本身;抽象层仅需很少的接口代码;多平台适用
4.6 线程问题
- fork 和 exec的语义问题:见前文
- 信号处理
- 信号用来通知一个进程某个特定的事件发起了
- 方式
- 发送给指定线程/所有线程
- 可以指定一个线程来接受进程监听的所有信号
- 线程取消
- 在线程结束前终止
- 方式
- 异步取消操作立即取消目标线程 pthread cancel(tid);
- deferred 取消周期性地检查目标线程是否需要取消 pthread testcancel();
- 线程局部存储thread-local storage (or TLS)
- 线程特有的数据
- 调度激活
- 定义:用户线程库和内核之间的通信
- 多对多和TwoLevel模型都需要通信,以维护分配给应用程序的适当数量的内核线程
- 调度程序激活提供了回调upcall——从内核到线程库的通信机制; 这种通信允许应用程序维护正确的内核线程数
- Lightweight process (LWP): 用户线程和内核线程之间的中间数据结构,每个LWP对应一个内核线程
- CPU-bound程序使用一个线程(一个LWP)就足够;I/O-bound程序一般需要多个LWP来执行,例如打印机。
4.7 OS例子
Windows XP Threads
用户层面:
TEB(thread environment block): 线程环境块
内核层面:
Ethread:执行线程块
Kthread:内核线程块
如上图,各个块之间通过指针关联。
Linux Threads
Linux不区分进程和线程,都看作task看待。
系统调用clone用来创建新的task,创建时通过flag指定子任务和父任务共享多少资源。
Takeaways
- 四种多线程模型
- 一些常用线程库
- 线程和进程之间的资源关系
- 一个进程下的所有线程共享的数据有哪些?
- 哪些是线程specific的数据?