Linux 系统编程

第六章 线程

6.1 线程的基本概念

一、定义

  • 进程是资源管理的最小单位,线程是程序执行的最小单位
  • 每个进程拥有自己的数据段、代码段和堆栈段。线程是轻型的进程,它包含独立的栈和 CPU 寄存器状态。线程是进程的一条执行路径,每个线程共享其所附属的进程的所有资源,包括打开的文件、内存、信号标识和动态分配的内存等。
  • 线程比进程小得多,花费更少的 CPU 资源。
  • 在操作系统设计上,从进程演化出线程,最主要的目的就是更好地支持多处理器,并且减小进程上下文切换的开销。

默认情况下,一个进程中只有一个线程(主控线程/主线程)。通过主控线程,可以创建出其他若干子线程主控线程和子线程都隶属于当前的进程。当系统给一个进程分配一定的时间片,这些时间会分配给其中的线程,但是在同一时间,只能有一个线程在执行,具体的执行根据系统的调度,将对应线程从就绪状态变为运行状态。

即,系统先调度哪一个进程执行,再调度进程中哪一个线程执行。

进程和线程的关系

线程属于进程。线程运行在进程空间内。统一进程所产生的线程共享同一用户内存空间,当进程退出时该进程所产生的所有线程都会被强制退出。一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(CPU、内存、文件等),而线程将被分配到某个 CPU 上执行。

二、分类

线程按照其调度者可以分为用户级线程和内核级线程。

  • 用户级线程:主要解决上下文切换问题,其调度过程由用户决定。
  • 内核级线程:由内核调度机制实现。

现代多数操作系统都采用用户级线程和内核级线程并存的方法。用户级线程要绑定内核级线程运行。也就是说,用户可以决定用户级线程何时创建、何时终止等,但具体有没有执行,需要由系统调度其所绑定的内核级线程决定。

一个进程中的内核级线程会分配到固定的时间片,用户级线程分配的时间片以内核级线程为准。

默认情况下用户级线程和内核级线程是一对一关系,也可以多对一,但是实时性较差。

当 CPU 分配给线程的时间片用完后线程没有执行完毕,此时线程会从运行状态返回到就绪状态,将 CPU 让给其他线程。

三、Linux 线程实现

Linux 一般采用 pthread 线程库实现线程的访问与控制。此库由 POSIX 提出,具有良好的可移植性。

Linux 线程程序编译需要在 gcc 上链接 pthread 库。例如

1
gcc -o thread_test thread_test.c -lpthread

线程标识

  • 每个进程内部的不同线程都有自己的唯一标识
  • 线程标识只在其所属的进程环境中有效
  • 线程标识是pthread_t类型
1
2
3
4
5
#include <pthread.h>
int pthread_equal(pthread_t pid1, pthread_t pid2);
// 判断两个线程的 ID 是否相等
pthread_t pthread_self(void);
// 返回调用线程的线程 ID

6.2 线程编程

一、线程的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_rtn)(void *),
void *restrict arg);
/*
功能:创建一个线程
返回值:若成功返回 0,若失败返回错误编号

参数
- tidp 线程标识符指针
- attr 线程属性指针(可以是 NULL)
- start_rtn 线程运行函数的起始地址
- arg 传递给线程运行函数的参数(一般将一个结构体的指针强转为通用指针以传入多个参数)


- 新创建的线程先运行的是 start_rtn 函数
- 不能保证新线程和调用线程的执行顺序(也有可能主控线程继续运行,进程直接结束)
*/

举例应用

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
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
char name[20];
int time;
int start;
int end;
} RaceArg;

void* th_fn(void *arg)
{
RaceArg *r = (RaceArg *)arg;
int i = r->start;
for(; i <= r->end; i++) {
printf("%s(%lx) running %d\n", r->name, pthread_self(), i);
usleep(r->time);
}
return (void *)0;
}

int main()
{
int err;
pthread_t rabbit, turtle;

RaceArg r_a = {"rabbit", (int)(drand48() * 100000000), 20, 50};
RaceArg t_a = {"turtle", (int)(drand48() * 100000000), 10, 60};

if ((err = pthread_create(&rabbit, NULL, th_fn, (void *)&r_a)) != 0) {
perror("pthread_create error");
exit(1);
}
if ((err = prthread_create(&turtle, NULL, th_fn, (void *)&t_a)) != 0) {
perror("pthread_create error");
exit(1);
}

/*
主控线程可能在创建创建 rabbit 和 turtle 继续运行
从而导致还没赛跑就结束了
可以使用 pthread_join 函数优先运行两个子线程,而把主控线程阻塞
*/
pthread_join(rabbit, NULL);
pthread_join(turtle, NULL);

printf("control thread id: %lx\n", pthread_self());
printf("finish\n");

return 0;
}

截屏2022-12-05 15.57.01

二、线程的终止

1
2
3
4
5
6
7
#include <pthread.h>
int pthread_cancel(pthread_t tid);
void pthread_exit(void *retval);
int pthread_join(pthread_t th, void **thread_return);
/*
返回值:成功返回 0,否则返回错误编号
*/
  • pthread_cancel

    线程可以被同一进程的其他线程取消,tid为被终止线程的线程标识符。

  • pthread_exit

    线程退出时使用此函数,是线程的主动行为(相当于直接return)。参数retval是线程的返回值,可由其他函数或pthread_join函数检测获取。由于一个进程的多个线程共享数据段,因此通常在线程退出后,其所占用的资源并不会随着线程结束而释放。一般地,通过pthread_join执行的进程,在结束后其资源会被释放。

  • pthread_join

    参数thread_return为用户自定义的指针,用来存放返回值。

在线程执行函数中执行exit()会导致进程终止。

举例说明:pthread_join接收返回值的使用

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
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>

typedef struct {
int d1;
int d2;
} Arg;

void* th_fn(void *arg)
{
Arg *r = (Arg *)arg;
return (void *)(r->d1 + r->d2);
}

int main()
{
int err;
pthread_t th;
Arg r = {20, 50};

if ((err = pthread_create(&th, NULL, th_fn, (void *)&r)) != 0) {
perror("pthread_create error");
exit(1);
}

int *result;
pthread_join(th, (void **)&result);
printf("result is %d\n", (int)result); // 指针本身存的就是值

return 0;
}

三、线程的清理

1
2
3
4
5
6
7
8
9
10
11
12
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
/*
参数
- rtn 清理函数指针
- arg 调用清理函数传递的参数
- execute 为 1 时执行清理函数,为 0 时不执行

逻辑:每次调用 push 函数相当于把一个清理函数压入栈,pop 时根据 execute 的值决定
出栈的函数是否执行。
*/

四、线程属性

线程属性的初始化和销毁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
// 成功返回 0,否则返回错误编号
typedef struct {
int detachstate; // 线程的分离状态
int schedpolicy; // 线程的调度策略
structsched_param schedparam; // 线程的调度参数
int inheritsched; // 线程的继承性
int scope; // 线程的作用域
size_t guardsize; // 线程栈末尾的警戒缓冲区的大小
int stackaddr_set; // 线程的栈设置
void* stackaddr; // 线程的栈位置
size_t stacksize; // 线程的栈大小
} pthread_attr_t

设置和获得分离属性

1
2
3
4
5
6
7
8
9
10
#include <pthread.h>
int pthread_attr_getdetachstat(const pthread_attr_t *attr, int *detachstate);
int pthread_attr_setdetachstat(const pthread_attr_t *attr, int detachstate);
/*
成功返回 0,否则返回错误编号

detachstate 的取值
- PTHREAD_CREATE_JOINABLE(default) 正常启动线程
- PTHREAD_CREATE_DETACHED 以分离状态启动线程
*/
  • 以默认方式启动的线程,在结束后不会自动释放系统资源,除非在主控线程调用了pthread_join
  • 以分离状态启动的线程,在结束后会自动释放系统资源,但这类线程不能通过pthread_join启动
  • 分离属性一般用在网络通讯中

6.3 线程互斥和同步

(一) 基本概念与锁的使用

一、线程的同步和互斥
  • 线程同步
    • 是一个宏观概念,在微观上包含线程的相互排斥和线程先后执行的约束问题。
    • 解决同步的方式
      • 条件变量
      • 线程信号量
  • 线程互斥
    • 线程执行的相互排斥
    • 解决互斥的方式
      • 互斥锁
      • 读写锁
      • 线程信号量
二、互斥锁
  • 互斥锁又称互斥量,是一种简单的加锁方式,用于控制对共享资源的访问。

    • 一把锁,一般和一个共享资源绑定,或者设置为全局变量。
    • 在同一时刻,只能有一个线程拥有某个互斥锁,具备上锁状态的线程能够对共享资源进行访问。
    • 若其他线程希望上锁一个已经被上锁的共享资源,则该线程挂起,直到上锁的线程释放了互斥锁为止。
  • 互斥锁的类型 pthread_mutex_t

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <pthread.h>
    int pthread_mutex_init(pthread_mutex_t *restrict mutex,
    const pthread_mutexattr_t *mutexattr);
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    /*
    成功返回 0,否则返回错误编号

    mutexattr - 互斥锁的创建方式
    PTHREAD_MUTEX_INITIALER / NULL 创建标准/默认互斥锁
    PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP 创建递归互斥锁
    PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP 创建检错互斥锁

    [用法1]
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    [用法2]
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);
    */
  • 上锁和解锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <pthread.h>
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    // 上锁,如果无法上锁则阻塞
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    // 上锁,如果无法上锁返回错误信息
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    // 释放锁
    /*
    返回值:成功返回 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
    #include <pthread.h>
    int pthread_mutexattr_init(pthread_mutexattr_t *attr);
    int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
    int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,
    int *restrict type);
    int pthread_mutexattr_settype(const pthread_mutexattr_t *attr, int type);
    /*
    返回值:成功返回 0,否则返回错误编号

    type - 互斥锁类型
    PTHREAD_MUTEX_NORMAL / PTHREAD_MUTEX_DEFAULT
    标准 / 默认互斥锁 第一次上锁成功,第二次上锁阻塞
    PTHREAD_MUTEX_RECURSIVE
    递归互斥锁 第一次上锁成功,接下来上锁还成功,内部计数
    PTHREAD_MUTEX_ERRORCHECK
    检错互斥锁 第一次上锁成功,第二次上锁出错
    */

    int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr,
    int *restrict pshared);
    int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
    /*
    返回值:成功返回 0,否则返回错误编号

    pshared - 进程共享属性
    PTHREAD_PROCESS_PRIVATE(默认) 锁只能用在一个进程内
    PTHREAD_PROCESS_SHARED 锁可以用在不同进程间
    */

    举例:从命令行获取锁的类型,进行两次连续上锁测试。

    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
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    int main(int argc, char *argv[])
    {
    pthread_mutex_t mutex;
    if (argc < 2) {
    fprintf(stderr, "-usage: %s [error|normal|recursive]\n", argv[0]);
    exit(1);
    }

    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    if (!strcmp(argv[1], "error")) {
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
    } else if (!strcmp(argv[1], "normal")) {
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    } else if (!strcmp(argv[1], "recursive")) {
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    } else {
    fprintf(stderr, "unknown type\n");
    exit(1);
    }
    pthread_mutex_init(&mutex, &attr);

    if (pthread_mutex_lock(&mutex) != 0) {
    printf("first lock failure\n");
    } else {
    printf("first lock success\n");
    }
    if (pthread_mutex_lock(&mutex) != 0) {
    printf("second lock failure\n");
    } else {
    printf("second lock success\n");
    }

    pthread_mutexattr_destroy(&attr);
    pthread_mutex_destroy(&mutex);

    return 0;
    }
三、读写锁
  • 线程使用互斥锁缺乏读并发性。当读操作较多,写操作较少时,可以使用读写锁提高读并发性。

  • 读写锁数据类型 pthread_rwlock_t

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <pthread.h>
    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
    // attr 为读写锁属性,一般用 NULL
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
    /*
    返回值:成功返回 0,否则返回错误编号
    */

    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    // 上读锁
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    // 上写锁
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
    // 释放锁
    /*
    返回值:成功返回 0,否则返回错误编号
    */
  • 读写锁加锁特性

    • 先上读锁
      • 再上读锁:成功
      • 再上写锁:阻塞
    • 先上写锁
      • 再上读锁:失败
      • 再上写锁:失败

(二) 条件变量

一、条件变量概念
  • 互斥锁的缺点是其只有两种状态,即锁定和非锁定。
  • 条件变量允许通过线程阻塞和发送信号的机制,弥补互斥锁的不足。
  • 条件变量内部维护了一个等待队列,放置等待中的线程。由于等待队列本身仍被多个线程共享,因而它需要互斥锁保护。
  • 条件变量允许线程等待特定条件发生,当条件不满足时,线程先进入阻塞状态。一旦其他某个线程改变了条件,可以唤醒一个或多个等待着的线程。这个判断条件一般由用户给出。
二、条件变量的使用
  • 条件变量的类型 pthread_cond_t

  • 相关函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <pthread.h>
    int pthread_cond_init(pthread_cond_t *restrict cond,
    pthread_condattr_t *restrict attr); // 属性一般用 NULL
    int pthread_cond_destroy(pthread_cond_t *cond);


    int pthread_cond_wait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex);
    // 把自己放入等待队列,并用锁 mutex 保护队列
    int pthread_cond_timewait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex,
    const struct timespec *restrict timeout);
    // 若超时自动返回
    struct timespec
    {
    time_t tv_sec; /* seconds */
    long tv_nsec; /* nanoseconds */
    };


    int pthread_cond_signal(pthread_cond_t *cond); // 通知单个线程
    int pthread_cond_broadcast(pthread_cond_t *cond); // 通知所有线程

举例应用:创建一个计算线程和一个获取结果的线程,必须先计算,再获取结果(同步问题)

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
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

typedef struct
{
int res;
int is_wait; // 如果有多个读者,可以用计数器实现
pthread_cond_t cond;
pthread_mutex_t mutex;
} Result;

void* cal_fn(void *arg)
{
Result *r = (Result *)arg;
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
r->res = sum;

// r->is_wait 是共享资源,必须通过锁实现互斥
pthread_mutex_lock(&r->mutex);
while (!r->is_wait) { // 如果获取结果的线程尚未准备好
pthread_mutex_unlock(&r->mutex); // 先释放锁,给对方操作共享资源的机会
usleep(100); // 睡眠,给对方操作共享资源的时间
pthread_mutex_lock(&r->mutex); // 继续上锁
}
pthread_mutex_unlock(&r->mutex);

pthread_cond_broadcast(&r->cond); // 跳出循环表示对方已经准备好,进行通知

return (void *)0;
}
void* get_fn(void *arg)
{
Result *r = (Result *)arg;

pthread_mutex_lock(&r->mutex); // 操作共享资源前先上锁
r->is_wait = 1;
pthread_cond_wait(&r->cond, &r->mutex); // 等待函数应放在 unlock 之前
pthread_mutex_unlock(&r->mutex);

int res = r->res;
printf("0x%lx get result: %d\n", pthread_self(), res);

return (void *)0;
}

int main()
{
int err;
pthread_t cal, get;

Result r;
r.is_wait = 0;
pthread_cond_init(&r.cond, NULL);
pthread_mutex_init(&r.mutex, NULL);

if ((err = pthread_create(&cal, NULL, cal_fn, (void *)&r)) != 0) {
perror("pthread create error");
}
if ((err = pthread_create(&get, NULL, get_fn, (void *)&r)) != 0) {
perror("pthread create error");
}
pthread_join(cal, NULL);
pthread_join(get, NULL);

pthread_cond_destroy(&r.cond);
pthread_mutex_destroy(&r.mutex);

return 0;
}
  • pthread_cond_wait的执行流程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    pthread_cond_wait(cond, mutex)
    {
    1) unlock(&mutex) // 先释放锁
    2) lock(&mutex)
    3) 将自己插入到 cond 的等待队列中
    4) unlock(&mutex)
    5) 自己阻塞,等待其他线程唤醒自己
    6) 唤醒后 lock(&mutex) // 这里有可能被阻塞
    7) 从 cond 的等待队列中删除自己 // 所以 wait 应该放在 unlock 之前
    }

线程的状态转换

截屏2022-12-24 16.06.05

举例应用:读者和写者问题(两个线程的相互通知)

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

typedef struct
{
int val;

pthread_cond_t reader_cond;
pthread_mutex_t reader_mutex;
int reader_wait;

pthread_cond_t writer_cond;
pthread_mutex_t writer_mutex;
int writer_wait;
} Storage;

void set_data(Storage *s, int data)
{
s->val = data;
}
int get_data(Storage *s)
{
return s->val;
}

void* w_fn(void *arg)
{
Storage *s = (Storage *)arg;
// 循环写数
for (int i = 1; i <= 100; i++) {
set_data(s, i + 20);
printf("0x%lx(%d) write data: %d\n", pthread_self(), i, i + 20);
// 等待读者准备好
pthread_mutex_lock(&s->reader_mutex);
while (!s->reader_wait) {
pthread_mutex_unlock(&s->reader_mutex);
sleep(1);
pthread_mutex_lock(&s->reader_mutex);
}
// 重置读者准备状态,通知读者
s->reader_wait = 0;
pthread_mutex_unlock(&s->reader_mutex);
pthread_cond_broadcast(&s->reader_cond);
// 等待读者读完后通知自己
pthread_mutex_lock(&s->writer_mutex);
s->writer_wait = 1;
pthread_cond_wait(&s->writer_cond, &s->writer_mutex);
pthread_mutex_unlock(&s->writer_mutex);
}
return (void *)0;
}
void* r_fn(void *arg)
{
Storage *s = (Storage *)arg;
// 循环读数
for (int i = 1; i <= 100; i++) {
// 等待写者写完后通知自己
pthread_mutex_lock(&s->reader_mutex);
s->reader_wait = 1;
pthread_cond_wait(&s->reader_cond, &s->reader_mutex);
pthread_mutex_unlock(&s->reader_mutex);

int data = get_data(s);
printf("0x%lx(%d) read data: %d\n", pthread_self(), i, data);
// 等待写者准备好
pthread_mutex_lock(&s->writer_mutex);
while (!s->writer_wait) {
pthread_mutex_unlock(&s->writer_mutex);
sleep(1);
pthread_mutex_lock(&s->writer_mutex);
}
// 重置写者准备状态,通知写者
s->writer_wait = 0;
pthread_mutex_unlock(&s->writer_mutex);
pthread_cond_broadcast(&s->writer_cond);
}
return (void *)0;
}

int main()
{
int err;
pthread_t rth, wth;

Storage s;
s.reader_wait = s.writer_wait = 0;
pthread_mutex_init(&s.reader_mutex, NULL);
pthread_mutex_init(&s.writer_mutex, NULL);
pthread_cond_init(&s.reader_cond, NULL);
pthread_cond_init(&s.writer_cond, NULL);

if ((err = pthread_create(&rth, NULL, r_fn, (void*)&s)) != 0) {
perror("pthread create error");
}
if ((err = pthread_create(&wth, NULL, w_fn, (void *)&s)) != 0) {
perror("pthread create error");
}
pthread_join(rth, NULL);
pthread_join(wth, NULL);

pthread_mutex_destroy(&s.reader_mutex);
pthread_mutex_destroy(&s.writer_mutex);
pthread_cond_destroy(&s.reader_cond);
pthread_cond_destroy(&s.writer_cond);

return 0;
}

(三) 线程信号量

  • 信号量本质上是一个非负整数计数器,可以代表共享资源的数目,通常用于控制对共享资源的访问。

  • 信号量可以实现线程的同步和互斥。

  • 信号量数据类型 sem_t

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <semaphore.h>
    int sem_init(sem_t *sem, int pshared, unsigned value);
    // pshared 为是否在进程间共享的标志,0 为不共享,1 为共享
    // value 为信号量初值
    int sem_destroy(sem_t *sem);


    int sem_post(sem_t *sem);
    // 对信号量作[加 1 操作],相当于 V(1) 操作
    int sem_wait(sem_t *sem);
    // 对信号量作[减 1 操作],相当于 P(1) 操作
    // 若减 1 后信号量的值小于 0 则阻塞当前线程
    int sem_trywait(sem_t *sem);
    // sem_wait 的非阻塞版本

举例应用:通过两个信号量实现三个线程的同步问题(打印 cba)

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
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

sem_t sem1, sem2;

void* a_fn(void *arg)
{
sem_wait(&sem1);

putchar('a');
putchar('\n');

return (void *)0;
}
void* b_fn(void *arg)
{
sem_wait(&sem2);

putchar('b');

sem_post(&sem1);

return (void *)0;
}
void* c_fn(void *arg)
{
putchar('c');

sem_post(&sem2);

return (void *)0;
}

int main()
{
pthread_t a, b, c;

sem_init(&sem1, 0, 0);
sem_init(&sem2, 0, 0);

pthread_create(&a, NULL, a_fn, (void *)0);
pthread_create(&b, NULL, b_fn, (void *)0);
pthread_create(&c, NULL, c_fn, (void *)0);

pthread_join(a, NULL);
pthread_join(b, NULL);
pthread_join(c, NULL);

sem_destroy(&sem1);
sem_destroy(&sem2);

return 0;
}

(四) 死锁

  • 概念:死锁指的是多个线程在运行过程中因争夺资源造成的僵局,当程序处于死锁状态,若无外力作用则无法继续运行下去。
  • 死锁产生的条件
    • 互斥条件:线程对资源存在排他性使用
    • 请求和保持条件:线程既占有某一资源不放,又请求某一新的资源
    • 不剥夺条件:线程已经获得的资源只能由自己释放
    • 环路等待条件:设存在线程集合 $\{t_1,t_2,\ \dots\ ,t_n\}$,$t_1$ 等待 $t_2$ 占有的资源,$t_2$ 等待 $t_3$ 占有的资源,…… ,$t_n$ 等待 $t_1$ 占有的资源
  • 死锁的解决方法
    • 破坏死锁产生的条件,尤其是加锁的顺序(一般按照相同的顺序加锁)
    • 设置加锁时限(到达某一时间后放弃对某资源的请求,并主动释放自己占有的资源)(也即调用非阻塞版本的上锁函数)

6.4 线程和信号

  • 定时器是进程资源,进程中所有的线程共享相同的定时器。子线程调用alarm()产生的 SIGALRM 信号发送给主控线程。

    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
    #include <pthread.h>
    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>

    void sig_handler(int sig)
    {
    printf("pthread id in the sig_handler: 0x%lx\n", pthread_self());
    if (sig == SIGALRM) {
    printf("time out\n");
    }
    alarm(2); // 循环发信号
    }

    void* th_fn(void *arg)
    {
    if (signal(SIGALRM, sig_handler) == SIG_ERR) { // 注册 SIGALRM 信号
    perror("signal error");
    }
    alarm(2); // 定时两秒

    for (int i = 1; i <= 100; i++) {
    printf("0x%lx i: %d\n", pthread_self(), i);
    sleep(1);
    }
    return (void *)0;
    }

    int main()
    {
    int err;
    pthread_t th;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    // 以分离状态启动子线程,主控线程持续睡眠

    if ((err = pthread_create(&th, &attr, th_fn, (void *)0)) != 0) {
    perror("pthread create error");
    }

    while (1) {
    printf("control thread(0x%lx) is running...\n", pthread_self());
    sleep(6);
    }

    return 0;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 输出
    control thread(0x104694580) is running...
    0x16b9cb000 i: 1
    0x16b9cb000 i: 2
    pthread id in the sig_handler: 0x104694580 # 信号处理函数是主控线程调用的
    time out
    control thread(0x104694580) is running... # 主控线程由于 SIGALRM 信号而中断睡眠
    0x16b9cb000 i: 3
    0x16b9cb000 i: 4
    pthread id in the sig_handler: 0x104694580
    time out
    ...
  • 进程中的每个线程都有自己的信号屏蔽字和信号未决字。信号的处理方式是所有线程共享的。进程中的信号是递送到单个线程的。可以通过线程的信号屏蔽函数实现信号屏蔽。

    1
    2
    3
    #include <signal.h>
    int pthread_sigmask(int how, const sigset_t *restrict set,
    sigset_t *restrict oset);

对上例中的主控线程屏蔽 SIGALRM 信号,从而使子线程捕获。

1
2
3
4
5
6
7
// 在 main 中加入
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGALRM);
pthread_sigmask(SIG_SETMASK, &set, NULL);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 输出
control thread(0x104fc8580) is running...
0x16b037000 i: 1
0x16b037000 i: 2
pthread id in the sig_handler: 0x16b037000 // SIGALRM 信号被子线程捕获
time out
0x16b037000 i: 3
0x16b037000 i: 4
pthread id in the sig_handler: 0x16b037000
time out
0x16b037000 i: 5
0x16b037000 i: 6
control thread(0x104fc8580) is running... // 主控线程睡醒
pthread id in the sig_handler: 0x16b037000
time out
0x16b037000 i: 7
0x16b037000 i: 8
pthread id in the sig_handler: 0x16b037000
time out