目录
当协调多个线程对共享资源进行访问的时候,需要确保每个线程看到一致的数据视图。例如,所有线程都可以读取或者修改某个变量,那么当线程A在操作这个变量的时候,就需要对其他线程进行同步,来确保其它线程访问变量的时候不会访问到无效的值。
产生这个问题的主要原因是因为寄存器读和寄存器写中间存在周期交叉。大家可能感觉这句话很别扭,没关系,下面用一个例子去解释这句话:
存在一个共享变量 num=5,线程A和B同时去对其进行+1的操作。
- 如果时间不交叉,正常应该是num进行了两次+1操作,变成了 num=7;
- 如果时间恰好交叉了,就会出现线程 A 将 num=6 这个值存到了寄存器但是没有写回,B 线程读出来的 num 还是5,B也进行 +1 操作得到 num=6 写到寄存器。然后A、B线程将 num 写回后,导致 num 最后是 6。
所以说,归根结底还是因为线程A和线程B同时去操作了同一个变量,那如果这个操作变得有序,那问题也就解决了。线程就是通过使用锁来解决这个问题的,在同一时间只允许一个线程访问该变量。如果线程B希望读取变量,它首先要去获取锁;同样地,当线程A更新变量时,也需要去获取这把同样的锁。线程B在线程A释放锁之前不能去读取变量。
1.互斥量
互斥量(mutex)从本质上来说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量加锁之后,其他再次对互斥量加锁的线程就会被阻塞,直到互斥锁被释放。
初始化:
互斥变量用 pthread_mutex_t 数据来表示,必须对其进行初始化(静态初始化),可以把它置为常量 PTHREAD_MUTEX_INITIALIZER 。也可以调用 pthread_mutex_init 函数进行动态初始化,释放内存需前需要调用 pthread_mutex_destory 进行销毁。
#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destory(pthread_mutex_t *mutex);
/*---------------例子----------------*/
// 静态初始化(编译时初始化)
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
// 动态初始化(运行时初始化)
pthread_mutex_t mutex2;
pthread_mutex_init(&mutex2, NULL); // 第二个参数为互斥量属性
/* 使用后需要销毁 */
pthread_mutex_destroy(&mutex2);
加锁:
- pthread_mutex_lock:对互斥量加锁(如果互斥量已经上锁,调用线程会阻塞直到互斥量被解锁)
- pthread_mutex_unlock:互斥量解锁
- pthread_mutex_trylock:尝试对互斥量加锁,但是不会阻塞线程,加锁失败就返回EBUSY。
#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);
使用示例:
创建 5 个线程进行 counter++ 的操作,每个线程操作前后进行加锁和解锁,避免出现不同步问题。
#include <stdio.h>
#include <pthread.h>
#define THREAD_NUM 5
int counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* increment(void* arg) {
for (int i = 0; i < 10000; ++i) {
pthread_mutex_lock(&mutex); // 加锁
counter++; // 临界区操作
pthread_mutex_unlock(&mutex);// 解锁
}
return NULL;
}
int main() {
pthread_t threads[THREAD_NUM];
// 创建线程
for (int i = 0; i < THREAD_NUM; ++i) {
pthread_create(&threads[i], NULL, increment, NULL);
}
// 等待线程结束
for (int i = 0; i < THREAD_NUM; ++i) {
pthread_join(threads[i], NULL);
}
printf("Final counter value: %d (expected: %d)\n", counter, THREAD_NUM * 10000);
pthread_mutex_destroy(&mutex);
return 0;
}
运行结果: Final counter value: 50000 (expected: 50000)
2.避免死锁
线程对同一个互斥量加锁两次,它自身就会陷入死锁状态。或者线程A占有互斥量1,并且试图锁住互斥量2,线程B占有互斥量2,并且试图锁住互斥量1,这样也会造成互相阻塞,产生死锁。
死锁的四个必要条件
| 条件 | 说明 | 示例场景 |
|---|---|---|
| 互斥条件 | 资源一次只能被一个线程占用 | 打印机、锁等独占资源 |
| 占有并等待 | 线程持有至少一个资源,同时等待获取其他被占用的资源 | 线程A锁住X后尝试获取Y,线程B反之 |
| 非抢占条件 | 已分配给线程的资源不能被其他线程强行夺取 | 必须由持有者主动释放 |
| 循环等待 | 存在一个线程的环形等待链 | A等B,B等C,C等A |
解决死锁
| 方法 | 实现方式 |
|---|---|
| 破坏互斥条件 | 使用共享资源(如读写锁) |
| 破坏占有并等待 | 一次性申请所有资源(pthread_mutex_lock(m1); pthread_mutex_lock(m2);) |
| 破坏非抢占条件 | 设置锁超时(如pthread_mutex_trylock) |
| 破坏循环等待 | 定义资源获取的全局顺序(如所有线程必须先获取锁A才能获取锁B) |
死锁示例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 定义两个互斥锁
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
// 线程1函数:先获取mutex1,再尝试获取mutex2
void* thread1_func(void* arg) {
pthread_mutex_lock(&mutex1);
printf("Thread 1: 持有 mutex1,尝试获取 mutex2...\n");
sleep(1); // 人为制造延迟,确保线程2能拿到mutex2
pthread_mutex_lock(&mutex2); // 这里会死锁
printf("Thread 1: 成功获取 mutex2\n");
// 临界区代码(永远不会执行到这里)
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
// 线程2函数:先获取mutex2,再尝试获取mutex1
void* thread2_func(void* arg) {
pthread_mutex_lock(&mutex2);
printf("Thread 2: 持有 mutex2,尝试获取 mutex1...\n");
sleep(1); // 人为制造延迟
pthread_mutex_lock(&mutex1); // 这里会死锁
printf("Thread 2: 成功获取 mutex1\n");
// 临界区代码(永远不会执行到这里)
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
return NULL;
}
int main() {
pthread_t thread1, thread2;
printf("创建线程...\n");
pthread_create(&thread1, NULL, thread1_func, NULL);
pthread_create(&thread2, NULL, thread2_func, NULL);
// 等待线程结束(实际上会永远阻塞)
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("程序结束(这行永远不会打印)\n");
return 0;
}
3.读写锁
读写锁相比于互斥锁有更高的并行性。读写锁有三种状态:读模式下加锁,写模式加锁,不加锁。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式所锁住的;当它以写模式锁住时,它是以独占模式锁住的。当读写锁是写加锁的时候,其它所有尝试对着把锁加锁的线程都会被阻塞。当读写锁是读加锁的时候,其他所有以读模式加锁的线程都可以获取访问权,要是此时想用写加锁的方式对其加锁的话,这样会阻塞掉其它所有线程(写优先级>读优先级)。
初始化:
读写锁经常会用在一些对数据结构的读的操作次数远大于写操作的情况。并且与互斥量一样,读写锁使用之前必须初始化,释放底层内存前必须销毁。
#include<pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrick rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁:
#include<pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //读模式下锁定读写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //写模式下锁定读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); //解锁(通用)
实现读写锁时可能对共享模式下可获取的锁的数量进行限制,所以要检查 pthread_rwlock_rdlock的返回值。即使 pthread_rwlock_wrlock 和 pthread_rwlock_unlock 有错误的返回值,如果锁设计合理的话,也不需要检查其返回值。错误返回值的定义只是针对不正确地使用读写锁的情况,例如未经初始化的锁,或者试图获取已拥有的锁从而可能产生死锁这样的错误返回等。
#include<pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define READER_NUM 5 // 读者线程数量
#define WRITER_NUM 2 // 写者线程数量
#define OPERATION_TIMES 3 // 每个线程操作次数
int shared_data = 0; // 共享数据
pthread_rwlock_t rwlock; // 读写锁
// 读者线程函数
void* reader(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < OPERATION_TIMES; ++i) {
// 获取读锁(共享访问)
pthread_rwlock_rdlock(&rwlock);
printf("Reader %d: 读取数据 = %d\n", id, shared_data);
usleep(100000); // 模拟读取耗时
// 释放读锁
pthread_rwlock_unlock(&rwlock);
usleep(200000); // 模拟其他操作
}
free(arg);
return NULL;
}
// 写者线程函数
void* writer(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < OPERATION_TIMES; ++i) {
// 获取写锁(独占访问)
pthread_rwlock_wrlock(&rwlock);
shared_data++; // 修改数据
printf("Writer %d: 写入数据 = %d\n", id, shared_data);
usleep(200000); // 模拟写入耗时
// 释放写锁
pthread_rwlock_unlock(&rwlock);
usleep(300000); // 模拟其他操作
}
free(arg);
return NULL;
}
int main() {
pthread_t readers[READER_NUM], writers[WRITER_NUM];
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 创建读者线程
for (int i = 0; i < READER_NUM; ++i) {
int* id = malloc(sizeof(int));
*id = i + 1;
pthread_create(&readers[i], NULL, reader, id);
}
// 创建写者线程
for (int i = 0; i < WRITER_NUM; ++i) {
int* id = malloc(sizeof(int));
*id = i + 1;
pthread_create(&writers[i], NULL, writer, id);
}
// 等待所有线程完成
for (int i = 0; i < READER_NUM; ++i) {
pthread_join(readers[i], NULL);
}
for (int i = 0; i < WRITER_NUM; ++i) {
pthread_join(writers[i], NULL);
}
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
printf("最终共享数据值: %d\n", shared_data);
return 0;
}
运行结果:
Reader 1: 读取数据 = 0
Reader 2: 读取数据 = 0
Writer 1: 写入数据 = 1
Reader 3: 读取数据 = 1
Reader 4: 读取数据 = 1
Writer 2: 写入数据 = 2
Reader 5: 读取数据 = 2
...
最终共享数据值: 6
4.条件变量
条件变量允许线程以无竞争的方式等待特定的条件发生,与互斥量一起使用。
#include<pthread.h>
int pthread_cond_init(pthread_cond_t *restrick cond,
pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_wait 是用于线程同步的条件变量等待函数,必须与互斥锁配合使用。其核心行为分为三步:
- 无条件释放关联的互斥锁(允许其他线程修改共享数据);
- 阻塞当前线程,直到被 pthread_cond_signal 唤醒;
- 被唤醒后自动重新获取锁,再继续执行。
pthread_cond_wait 的调用必须位于条件检查之后(如 while 循环)。
传递给 pthread_cond_wait 的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait 返回时,互斥量再次被锁住。
pthread_cond_timedwait 函数的工作方式与 pthread_cond_wait 函数相似,只是多了一个 timeout 。timeout 值指定了等待的时间,它是通过timespec结构指定。
#include<pthread.h>
int pthread_cond_wait(pthread_cond_t *restrick cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrick cond,
pthread_mutex_t *restrick mutex,
const struct timespec *restrict timeout);
使用示例:
条件变量最经典的使用场景就是生产者-消费者模型。
下面这个示例实现了生产者-消费者模型:生产者线程向共享缓冲区放入数据,消费者线程从中取出数据,通过条件变量和互斥锁协调两者操作——当缓冲区满时生产者阻塞等待 cond_full 信号,缓冲区空时消费者阻塞等待 cond_empty 信号,每次操作后通过 pthread_cond_signal 唤醒对方线程,确保生产与消费的安全交替执行,避免缓冲区溢出或空消费。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define MAX_ITEMS 5 // 缓冲区最大容量
int buffer[MAX_ITEMS]; // 共享缓冲区
int count = 0; // 当前缓冲区物品数量
int in = 0, out = 0; // 生产/消费位置索引
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 互斥锁
pthread_cond_t cond_full = PTHREAD_COND_INITIALIZER; // 缓冲区满条件
pthread_cond_t cond_empty = PTHREAD_COND_INITIALIZER;// 缓冲区空条件
// 生产者线程函数
void* producer(void* arg) {
for (int i = 1; i <= 10; ++i) { // 生产10个物品
pthread_mutex_lock(&mutex);
// 如果缓冲区满,等待消费者通知
while (count == MAX_ITEMS) {
printf("生产者: 缓冲区满,等待...\n");
pthread_cond_wait(&cond_full, &mutex);
}
// 生产物品
buffer[in] = i;
printf("生产者: 放入物品 %d (位置 %d)\n", i, in);
in = (in + 1) % MAX_ITEMS;
count++;
// 通知消费者缓冲区非空
pthread_cond_signal(&cond_empty);
pthread_mutex_unlock(&mutex);
usleep(100000); // 模拟生产耗时
}
return NULL;
}
// 消费者线程函数
void* consumer(void* arg) {
for (int i = 1; i <= 10; ++i) { // 消费10个物品
pthread_mutex_lock(&mutex);
// 如果缓冲区空,等待生产者通知
while (count == 0) {
printf("消费者: 缓冲区空,等待...\n");
pthread_cond_wait(&cond_empty, &mutex);
}
// 消费物品
int item = buffer[out];
printf("消费者: 取出物品 %d (位置 %d)\n", item, out);
out = (out + 1) % MAX_ITEMS;
count--;
// 通知生产者缓冲区非满
pthread_cond_signal(&cond_full);
pthread_mutex_unlock(&mutex);
usleep(150000); // 模拟消费耗时
}
return NULL;
}
int main() {
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_cond_destroy(&cond_full);
pthread_cond_destroy(&cond_empty);
pthread_mutex_destroy(&mutex);
return 0;
}
843

被折叠的 条评论
为什么被折叠?



