(译)POSIX Threads Programming(POSIX线程编程)

本文翻译自POSIX Threads Programming,原作者是:Blaise Barney, Lawrence Livermore National Laboratory
翻译缘由:某次面试面试官的问题,让我一直在思考如何高效的使用线程及其锁,思考很长一段时间没有答案,缺乏实际的负载和场景让我能够真切体会和验证。网上关于锁性能和线程的问题并不多,往往只是简单的介绍编程接口,并在假设的场景下对锁的使用进行介绍,在IBM开发者社区看到一些文章,但是也是比较浅显,没有太直接深入。偶然的机会找到这篇文章,觉得还不错,因此将它翻译出来,一是自己能够从中学习到一些东西和加深理解,另外一点顺道巩固英语(雅思囧)。

目录

  1. 摘要
  2. 线程概述
    2.1 线程是什么?
    2.2 Pthreas是什么?
    2.3 是什么使用线程?
    2.4 线程编程的设计
  3. Pthreads API
  4. 编译线程程序
  5. 线程管理
    5.1 创建和终止线程
    5.2 传递参数给线程
    5.3 线程结合(Joing)和分离

摘要

在共享内存多处理器架构下,线程可以用来实现并行。硬件厂商会以他们自己私有的方式实现线程,这使得可移植性成为开发人员关心的问题。对于UNIX系统而言,IEEE POSIX 1003.1c标准将c语言线程编程接口标准化了,这个标准的具体实现就是POSIX threads或者Pthreads.

这份材料首先介绍了使用线程需要了解的概念、出发点(动机)和设计方面的考虑。然后,对于三类主要的Pthreads API例程进行了介绍:线程管理,互斥量(Mutex Variables)和条件变量(Condition Variables)。通过示例代码的演示,线程编程新手可以快速学会如何使用这些接口。这份材料包含了对LLNL细节以及MPI和线程混合编程的讨论。作为练习,许多示例代码(c语言实现)也包括在内。

适合的读者/先决条件:这份材料主要是针对那些刚刚接触并行编程的人。阅读这份材料需要对C中并行编程有个基本的了解。对于那些对并行编程并不了解的人,可以参考EC3500: Introduction to Parallel Computing

Pthreads概览

线程是什么?

  • 严格的来说,一个线程被定义为由操作系统调度的一个独立的指令流。这意味着什么?
  • 对于开发人员而言,或许对线程最好的描述是:一个独立于它的主程序(main program)运行的“程序”(procedure)。
  • 在深入一步,想象一下一个主程序(a.out)包含多个程序(procedures),然后这些程序可以被操作系统同时或者独立的运行,这就是对多线程程序的描述。(译者注:其实可以理解为一个程序有多条不同的执行路径在同时执行。)
  • 这是怎么做到的呢?
  • 在理解线程之前,首先需要理解UNIX进程。一个进程有操作系统创建,这需要一定的开销(译者注:需要加载可执行文件,构建运行时环境,创建内存空间地址及其映射,打开三个标准的输入输出等等。具体可以参考《程序员的自我修养》)。进程包含了程序的资源以及执行状态,有:
    • 线程ID,线程组ID,用户ID,以及用户组ID
    • 环境
    • 工作目录
    • 程序执行
    • 寄存器
    • 文件描述符
    • 信号执行函数
    • 共享库
    • 进程间通信的工具(比如消息队列,pipes,信号量或者是内存共享)

UNIX进程
包含线程的UNIX进程

  • 线程使用并且与进程的这些资源一同存在,还能够成为操作系统调度和运行的实体。这是因为线程中仅仅包含独立运行需要的资源。
  • 线程能够成为独立的执行和调度单位,是因为线程自己维护了:
    • 栈指针
    • 寄存器
    • 调度的属性(比如策略或者优先级)
    • 包含处理和阻塞的信号集
    • 线程特定的数据
  • 总结,对于一个UNIX环境下的线程:
    • 存在于某个特定的进程之内,并且使用这个特定进程的资源
    • 拥有自己独立的控制流只要他的父进程存在并且操作系统支持
    • 仅仅包含独立调度所需的资源
    • 与其他线程共享父进程的资源
    • 父进程消亡时,线程也消亡
    • 轻量级。因为创建进程需要完成更多资源分配,而线程仅仅包含很少的资源。
  • 因为同一个进程中的线程共享资源:
    • 一个线程修改共享的资源能够被其他线程知道
    • 两个指针有相同的值并指向相同的数据
    • 能够读写相同的内存地址,因此需要开发者指定同步方式

Pthreads是什么?

  • 一直以来,硬件厂商以他们自己的方式实现了线程。不同厂商之间的实现基本是不同的,这就使得开发者很难使用线程开发出基于移植性的程序。
  • 为了能够有效利用线程带来的好处,需要一份编程标准:
    • 对于UNIX系统,这些接口由IEEE POSIX 1003.1c标准指定(1995)
    • POSIX或者Pthreads就是对这个标准的实现
    • 现在大多数的硬件厂商除了他们私有的API外,都已经提供Pthreads
  • POSIX标准在不断的演变和修改,包括Pthreads规范
  • 一些有用的链接
  • Pthreads被定义为一组C语言编程接口和调用,通过pthread.h头文件和一个线程库实现——在某些实现中,这个线程库或许是其他库的一部分,比如libc.

为什么使用线程?

->轻量级:

  • 与创建进程相比,线程创建仅仅更少开销,对线程的管理需要更少的系统资源。
  • 比如,下面的表格比较了fork()和pthread_create()两个子例程创建50, 000个进程和线程的开销。使用time命令测试所需的时间,以秒为单位,不对优化参数进行编译。
    注意:通过将系统时间和用户时间相加得到真实的运行时间是不合理的,因为对于SMP系统中多CPUs/cores是在同一时间处理用一个问题。充其量而言,这些结果接近于在本地机器过去和现在运行的结果。




Foo
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
//==============================================================================
//C Code for fork() creation test
//==============================================================================
#include <stdio.h>
#include <stdlib.h>
#define NFORKS 50000

void do_nothing() {
int i;
i= 0;
}

int main(int argc, char *argv[]) {
int pid, j, status;

for (j=0; j<NFORKS; j++) {

/*** error handling ***/
if ((pid = fork()) < 0 ) {
printf ("fork failed with error code= %d\n", pid);
exit(0);
}

/*** this is the child of the fork ***/
else if (pid ==0) {
do_nothing();
exit(0);
}

/*** this is the parent of the fork ***/
else {
waitpid(pid, status, 0);
}
}
}
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
//==============================================================================
//C Code for pthread_create() test
//==============================================================================
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define NTHREADS 50000

void *do_nothing(void *null) {
int i;
i=0;
pthread_exit(NULL);
}

int main(int argc, char *argv[]) {
int rc, i, j, detachstate;
pthread_t tid;
pthread_attr_t attr;

pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

for (j=0; j<NTHREADS; j++) {
rc = pthread_create(&tid, &attr, do_nothing, NULL);
if (rc) {
printf("ERROR; return code from pthread_create() is %d\n", rc);
exit(-1);
}

/* Wait for the thread */
rc = pthread_join(tid, NULL);
if (rc) {
printf("ERROR; return code from pthread_join() is %d\n", rc);
exit(-1);
}
}

pthread_attr_destroy(&attr);
pthread_exit(NULL);

}