1. 线程与进程

1.1 线程

线程和进程在操作系统中的表现形式与应用场景有所不同。进程是操作系统中资源分配的基本单位,而线程是进程中的实际执行单元。

特性 进程 线程
资源独立性 各进程有独立的地址空间 线程共享进程的地址空间和资源
执行单元 每个进程一个程序计数器 每个线程有自己的程序计数器
调度单位 操作系统调度的基本单位 进程内的调度由线程完成
创建和销毁复杂度 创建开销较大,资源隔离明显 较轻量,资源共享
上下文切换 开销高(完整切换) 相对较低(部分切换)
独立性与鲁棒性 较高,一个崩溃不影响其他进程 较低,一个线程崩溃可能使整个进程失败

线程由进程创建、管理,同时完成资源的共享。同一进程内的线程虽共享数据,但并不能避免各自独立的栈和控制信息。因此在多线程程序中,为了避免线程之间的冲突,必须实施适当的同步控制。

1.2 线程的同步问题

在多线程用户程序中常见的两种同步问题包括:

  1. 竞态条件

    • 由于线程可能在执行中被打断,因此多个线程可能同一时间访问共享数据,导致数据的不确定性和数据竞争。
  2. 线程依赖关系

    • 某些线程需要等待其他线程完成某些操作以达到特定的条件,这时需要通过同步机制来实现线程间的协调。

2. 线程的同步机制

2.1 互斥锁(Mutex)

互斥锁用于保证线程对共享数据的独占访问,避免竞态条件的发生。互斥锁通过锁定资源,保证临界区在任何时刻只能被一个线程访问。对于Pthreads库,常用的互斥锁函数包括:

  • 初始化pthread_mutex_init()用于初始化互斥锁,可以指定锁的属性。
  • 加锁pthread_mutex_lock()加锁,如果锁已经被占用,则线程进入睡眠。
  • 尝试加锁pthread_mutex_trylock()进行加锁尝试而不阻塞。
  • 定时加锁pthread_mutex_timedlock()提供超时机制进行加锁。
  • 解锁pthread_mutex_unlock()释放锁。
  • 销毁pthread_mutex_destroy()销毁锁,解除对资源的锁定。

使用C语言中的pthread库实现互斥锁的示例

以下示例演示了如何在多线程环境中使用pthread互斥锁保护共享数据。

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

// 声明一个全局的互斥锁
pthread_mutex_t mtx;

// 声明一个全局的共享资源
int shared_counter = 0;

// 线程要执行的函数
void* incrementCounter(void* arg) {
for (int i = 0; i < 5; ++i) {
// 加锁保护共享资源
pthread_mutex_lock(&mtx);

// 临界区开始
// 对共享资源进行安全的修改
printf("Thread %ld: Counter = %d\n", (long)pthread_self(), shared_counter);
shared_counter++;

// 临界区结束
// 解锁
pthread_mutex_unlock(&mtx);

// 模拟其他操作
usleep(100000); // 睡眠100毫秒
}
return NULL;
}

int main() {
// 初始化互斥锁
pthread_mutex_init(&mtx, NULL);

const int numberOfThreads = 5; // 定义要创建的线程数
pthread_t threads[numberOfThreads]; // 定义线程标识符的数组

// 创建多个线程
for (int i = 0; i < numberOfThreads; ++i) {
pthread_create(&threads[i], NULL, incrementCounter, NULL);
}

// 等待所有线程完成
for (int i = 0; i < numberOfThreads; ++i) {
pthread_join(threads[i], NULL);
}

// 销毁互斥锁
pthread_mutex_destroy(&mtx);

// 打印最终的共享计数器值
printf("Final Counter: %d\n", shared_counter);

return 0;
}

2.2 条件变量(Condition Variable)

条件变量使线程能够睡眠等待特定条件,并在其他线程修改该条件时被唤醒:

  • 初始化条件变量pthread_cond_init()用于创建条件变量。
  • 等待条件pthread_cond_wait()会将线程放入等待队列,并释放持有的互斥锁。当重新获得锁并且条件满足时,线程被唤醒。
  • 发送信号pthread_cond_signal()pthread_cond_broadcast()唤醒一个或多个等待线程。
  • 销毁条件变量pthread_cond_destroy()释放条件变量的相关资源。
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
#include <stdio.h>  
#include <pthread.h>
#include <unistd.h>

// 声明互斥锁与条件变量
pthread_mutex_t mutex;
pthread_cond_t cond_var;

// 共享资源
int data_ready = 0;

// 生产者线程函数,负责设置共享资源状态并通知消费者线程
void* producer(void* arg) {
// 模拟数据准备延迟
sleep(1);

// 获取互斥锁
pthread_mutex_lock(&mutex);
data_ready = 1; // 设置共享资源的状态
printf("Producer: Data is ready.\n");

// 通知等待在条件变量上的线程
pthread_cond_signal(&cond_var);
// 释放互斥锁
pthread_mutex_unlock(&mutex);

return NULL;
}

// 消费者线程函数,等待数据准备完成
void* consumer(void* arg) {
// 获取互斥锁
pthread_mutex_lock(&mutex);

while (data_ready == 0) {
// 等待条件变量,释放互斥锁并挂起线程,直到条件变量被唤醒
printf("Consumer: Waiting for data.\n");
pthread_cond_wait(&cond_var, &mutex);
}

// 当条件满足时继续执行
printf("Consumer: Data has been acquired.\n");

// 释放互斥锁
pthread_mutex_unlock(&mutex);

return NULL;
}

int main() {
// 初始化互斥锁和条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond_var, NULL);

pthread_t prod_thread, cons_thread;

// 创建生产者和消费者线程
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);

// 等待线程完成
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);

// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond_var);

return 0;
}

2.3 信号量(Semaphore)

信号量用于控制对多个资源的访问,提供更灵活的同步机制:

  • 主要功能操作
    • sem_wait():当资源可用时获取资源,信号量减一,否则进入休眠。
    • sem_post():释放一个资源,更新信号量。若有等待线程,则唤醒。

信号量可以被应用于各种场合:

  • 二进制信号量:用于实现互斥锁功能。
  • 计数信号量:用于多个资源的同步控制。
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
#include <stdio.h>  
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

#define BUFFER_SIZE 5 // 定义缓冲区大小

int buffer[BUFFER_SIZE]; // 共享缓冲区
int count = 0; // 当前缓冲区中的项数

// 声明信号量
sem_t empty; // 表示缓冲区中的空闲单元
sem_t full; // 表示缓冲区中的占用单元
pthread_mutex_t mutex; // 互斥锁,用于保护对缓冲区的访问

// 生产者线程函数
void* producer(void* arg) {
int item;
for (int i = 0; i < 10; ++i) {
item = i; // 生成新项
sem_wait(&empty); // 减少空闲单元信号量,等待空单元
pthread_mutex_lock(&mutex); // 加锁保护缓冲区

// 临界区开始
buffer[count] = item;
count++;
printf("Producer produced item: %d\n", item);
// 临界区结束

pthread_mutex_unlock(&mutex); // 解锁
sem_post(&full); // 增加占用单元信号量
sleep(1); // 模拟产生下一个项的延迟
}
return NULL;
}

// 消费者线程函数
void* consumer(void* arg) {
int item;
for (int i = 0; i < 10; ++i) {
sem_wait(&full); // 等待占用单元
pthread_mutex_lock(&mutex); // 加锁保护缓冲区

// 临界区开始
item = buffer[count - 1];
count--;
printf("Consumer consumed item: %d\n", item);
// 临界区结束

pthread_mutex_unlock(&mutex); // 解锁
sem_post(&empty); // 增加空闲单元信号量
sleep(1); // 模拟处理下一个项的延迟
}
return NULL;
}

int main() {
// 初始化信号量和互斥锁
sem_init(&empty, 0, BUFFER_SIZE);
sem_init(&full, 0, 0);
pthread_mutex_init(&mutex, NULL);

pthread_t prod_thread, cons_thread;

// 创建生产者和消费者线程
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);

// 等待线程完成
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);

// 销毁信号量和互斥锁
sem_destroy(&empty);
sem_destroy(&full);
pthread_mutex_destroy(&mutex);

return 0;
}

3. 内核同步原语

在多核系统中,内核也需要合适的同步机制来确保系统调度中的数据一致性。这些主要的内核同步原语包括:

名称 描述
Spinlock自旋锁 利用自旋忙等待锁。适用于锁等待较短场合
信号量Semaphore 基于信号量进行阻塞同步控制
顺序锁Seqlock 使用读写版本号实现的锁机制
RCU(Read-Copy-Update) 使用读复制更新机制实现高效的读线程处理

3.1 信号量(Semaphore)

在内核中,信号量用于在访问共享资源时实现阻塞同步控制。其特点在于降等待线程转至睡眠以节省CPU资源。

3.2 顺序锁(Seqlock)

顺序锁通过使用版本号机制,实现高效的数据读写锁。顺序锁特别适用于写操作频繁,但对数据一致性要求较低的场合,其提供了读写间的锁竞争转换。

3.3 RCU(Read-Copy Update)

RCU提供一种高效特殊的同步机制,适用于读多写少场合。读操作无锁框架实现迅速访问,而写操作利用副本更新和延迟指针跳转方式进行无锁响应,以减少写操作对读操作的干扰。


这些同步机制不仅提高了多线程程序的可靠性和效率,也为内核的并发管理提供了重要的技术支撑。在编写时注意三元素: 互斥锁+条件变量+状态变量,集齐三元素,确保多线程编程不出错.