Linux 线程的概念和使用

本文主要介绍Linux线程的概念以及线程的模型,并介绍基于POSIX线程的接口。


什么是线程?

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
  • 一切进程至少都有一个执行线程。

进程与线程

  • 进程是资源分配与竞争的基本单位
  • 线程是程序执行的最小单位
  • 线程共享进程数据,但也有自己的一部分数据
    • 线程ID
    • 一组寄存器:IP,PSW,堆栈指针
    • errno
    • 信号状态
    • 优先级

fork和创建新线程的区别

  • 当一个进程执行一个fork调用的时候,会创建出进程的一个新拷贝,新进程将拥有它自己的变量和它自己的PID。这个新进程的运行时间是独立的,它在执行时几乎完全独立于创建它的进程。
  • 在进程里面创建一个新线程的时候,新的执行进程会拥有自己的堆栈(因此也就有自己的局部变量),但要与他的创建者共享全局变量、文件描述符、信号处理器和当前工作目录状态。

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作少得多
  • 线程占用的资源要比进程少得多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高系统,将I/O操作重叠。线程可以同时等待不同的I/O操作

线程的缺点

  • 性能损失
    • 一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低
    • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制
    • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高
    • 编写与调试一个多线程比单线程程序困难得多

线程调度竞争范围

  • 操作系统提供了各个模型,用来调度应用程序创建的线程。这些模型时间的主要不同是:在竞争系统资源(特别是CPU时间)时,线程调度竞争范围(thread-scheduling contention scope)不一样
  • 进程竞争范围(Process contention scope):各个线程在同一个进程竞争“被调度的CPU时间”(但不直接和其他进程中的线程竞争)。
  • 系统竞争范围(System contention scope):线程直接和”系统范围“内的其他线程竞争。

线程模型

N:1用户线程模型

  • “线程实现”建立在“进程控制”机制之上,由用户空间的程序库来管理。OS内核完全不知道线程信息。这些线程成为用户空间线程。
  • 这些线程都工作在“进程竞争范围”
  • 在N:1线程模型中,内核不干涉线程的任何生命活动,也不干涉同一进程中的线程环境切换
  • 在N:1线程模型中,一个进程中的多个线程只能调度到一个CPU,这种约束限制了可用的并行总量。
  • 第二个缺点是如果某个线程执行了一个“阻塞式”操作(如read),那么,进程中的所有线程都会阻塞,直至那个操作结束。为此,一个线程的实现是为这些阻塞式函数提供包装器,用非阻塞版本替换这些系统调用,以清除这种限制。

1:1核心线程模型

  • 在1:1核心线程模型中,应用程序创建的每一个线程都由一个核心线程直接管理。
  • OS内核将每一个核心线程都调度到系统CPU上,因此,所有线程都工作在“系统竞争范围”。
  • 这种线程的创建于调度由内核完成,因为这种线程的系统开销比较大(但一般来说,比进程开销小)

N:M混合线程模型

  • N:M混合线程模式提供了两级控制,将用户线程映射为系统的可调度体以实现并行,这个可调度体称为轻量级进程(LWP:lightweight process),LWP再一一映射到核心线程。

POSIX线程库

pthread_create函数

  • 功能:创建一个新的线程
  • 原型:

    1
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
  • 参数:

    • thread:返回线程ID
    • attr:设置线程的属性,attr为NULL表示使用默认属性
    • start_routine:是个函数地址,线程启动后要执行的函数
    • arg:传给线程启动函数的参数
  • 返回值:成功返回0,失败返回错误码

pthread_join函数

  • 功能:等待线程结束
  • 原型:

    1
    int pthread_join(pthread_t thread, void **retval);
  • 参数:

    • thread:线程ID
    • retval:它指向一个指针,后者指向线程的返回值
  • 参数:成功返回0,失败返回错误码

pthread_exit函数

  • 功能:线程终止
  • 原型:

    1
    void pthread_exit(void *value_ptr);
  • 参数:

    • value_ptr:value_ptr不要指向一个局部变量。
  • 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

pthread_self函数

  • 功能:返回线程ID
  • 原型:

    1
    pthread_t pthread_self(void);
  • 返回值:成功返回0


pthread_cancel

  • 功能:取消一个执行的线程
  • 原型:

    1
    int pthread_cancel(pthread_t thread);
  • 参数:

    • thread:线程ID
  • 返回值:成功返回0;失败返回错误码

pthread_detach函数

  • 功能:将一个线程分离
  • 原型:

    1
    int pthread_detach(pthread_t thread);
  • 参数:

    • thread:线程ID
  • 返回值:成功返回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
43
44
45
46
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
void *thread_routine(void *arg) {
int i;
for (i=0; i<20; ++i) {
printf("B");
fflush(stdout);
usleep(20);
if (i == 3) {
pthread_exit("ABC");
}
}
return 0;
}
int main() {
pthread_t tid;
int ret;
if ((ret = pthread_create(&tid, NULL, thread_routine, NULL)) != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
int i;
for (i=0; i<20; ++i) {
printf("A");
fflush(stdout);
usleep(20);
}
void *value;
if ((ret = pthread_join(tid, &value)) != 0) {
fprintf(stderr, "pthread_join: %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
printf(" msg = %s\n", (char *)value);
return 0;
}

线程属性

初始化与销毁属性

1
2
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

获取与设置分离属性

1
2
int pthread_attr_getdetachstate(const pthread_attr_t \*attr, int \*detachstate);
int pthread_attr_setdetachstate(const pthread_attr_t *attr, int detachstate);

获取与设置栈大小

1
2
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t \*attr, size_t \*stacksize);

获取与设置栈溢出保护区大小

1
2
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize);

获取与设置线程竞争范围

1
2
int pthread_attr_setscope(pthread_attr_t *attr, int scope);
int pthread_attr_getscope(pthread_attr_t *attr, int *scope);

获取与设置调度策略

1
2
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
int pthread_attr_getschedpolicy(pthread_attr_t *attr, int *policy);

获取与设置继承的调度策略

1
2
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
int pthread_attr_getinheritsched(pthread_attr_t *attr, int *inheritsched);

获取与设置调度参数

1
2
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
int pthread_attr_getschedparam(pthread_attr_t *attr, struct sched_param *param);

获取与设置并发级别

1
2
int pthread_setconcurrency(int new_level);
int pthread_getconcurrency(void);
  • 仅在N:,线程模型中有效,设置并发级别,给内核一个提示:表示提供给定级别数量的核心线程来映射用户线程是有效的。

线程特定数据

  • 在单线程程序中,我们经常要用到全局变量以实现多个函数间共享数据
  • 在多线程环境下,由于数据空间是共享的,因此全局变量也为所有线程所共有
  • 但有时应用程序设计中有必要提供线程的私有的全局变量,仅在某个线程中有效,但却可以跨多个函数访问
  • POSIX线程库通过维护一定的数据结构来解决这个问题,这些数据成为(Thread-sepcific Data,或TSD)
    1
    2
    3
    4
    5
    6
    7
    8
    int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
    int pthread_key_delete(pthread_key_t key);
    void *pthread_getspecific(pthread_key_t key);
    int pthread_setspecific(pthread_key_t key, const void *value);
    int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
    pthread_once_t once_control = PTHREAD_ONCE_INIT;

实例:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
typedef struct tsd {
pthread_t tid;
char *str;
} tsd_t;
pthread_key_t key_tsd;
pthread_once_t once_control = PTHREAD_ONCE_INIT;
void destroy_routine(void *value) {
printf("destroy\n");
free(value);
}
void init_routine() {
pthread_key_create(&key_tsd, destroy_routine);
}
void *thread_routine(void *arg) {
pthread_once(&once_control, init_routine);
tsd_t *value = (tsd_t *)malloc(sizeof(tsd_t));
tsd_t *value = (tsd_t *)malloc(sizeof(tsd_t));
value->tid = pthread_self();
value->str = (char *)arg;
pthread_setspecific(key_tsd, value);
printf("%s setspecific %p\n", (char *)arg, value);
value = pthread_getspecific(key_tsd);
printf("tid=0x%x str=%s\n", value->tid, value->str);
sleep(2);
value = pthread_getspecific(key_tsd);
printf("tid=0x%x str=%s\n", value->tid, value->str);
return NULL;
}
int main() {
// pthread_key_create(&key_tsd, destroy_routine);
pthread_t tid1;
pthread_t tid2;
int ret;
if ((ret = pthread_create(&tid1, NULL, thread_routine, "thread1")) != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
if ((ret = pthread_create(&tid2, NULL, thread_routine, "thread2")) != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
pthread_join(tid1, NULL);
pthread_join(tid1, NULL);
pthread_key_delete(key_tsd);
return 0;
}