在Linux下使用C语言编写多线程程序时,条件变量和互斥锁是常用的同步机制,用于确保线程之间的正确协作。以下是它们的基本用法和理解:
一、互斥锁和条件变量的基本用法和理解
1.1 互斥锁(Mutex):
- 互斥锁用于确保在任何给定时刻只有一个线程可以访问被保护的共享资源,以防止竞态条件(Race Condition)。
- 互斥锁有两个主要操作:锁定(Lock)和解锁(Unlock)。
- 锁定互斥锁后,其他线程会被阻塞,直到拥有锁的线程释放它。
- 在C语言中,你可以使用pthread_mutex_init初始化互斥锁,pthread_mutex_lock锁定它,pthread_mutex_unlock解锁它,最后使用pthread_mutex_destroy销毁它。
1.2 条件变量(Condition Variable):
- 条件变量用于线程之间的通信和协作,允许线程等待某个特定条件的发生。
- 条件变量通常与互斥锁一起使用,以确保线程在检查条件和等待条件满足之间的操作是原子的。
- 条件变量有两个主要操作:等待(Wait)和通知(Signal)。
- 等待操作使线程等待某个条件的满足,并且会在条件满足或者被其他线程发出的通知后继续执行。
- 通知操作用于通知等待的线程条件已满足。
1.3 总结
理解互斥锁和条件变量的关键点在于:
互斥锁用于保护共享资源,确保一次只有一个线程能够修改或访问它。
条件变量用于线程之间的协作,使一个线程能够等待另一个线程发出的信号,以便在满足某些条件时继续执行。
二、经典示例
多线程经典的示例是生产者-消费者问题,其中生产者线程生产数据,消费者线程消费数据,它们必须同步以避免竞态条件。
其中,互斥锁用于保护共享缓冲区,条件变量用于通知消费者何时可以消费数据。
生产者-消费者示例代码如下:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0; // 记录缓冲区中的数据数量
pthread_mutex_t mutex;
pthread_cond_t not_full, not_empty;
void *producer(void *arg) {
int item = 1;
while (1) {
pthread_mutex_lock(&mutex); // 锁定互斥锁
while (count == BUFFER_SIZE) { // 如果缓冲区已满,等待
pthread_cond_wait(¬_full, &mutex);
}
buffer[count] = item; // 将数据放入缓冲区
count++;
printf("Produced: %d\n", item);
pthread_cond_signal(¬_empty); // 通知消费者缓冲区不为空
pthread_mutex_unlock(&mutex); // 解锁互斥锁
item++;
sleep(1); // 模拟生产时间
}
}
void *consumer(void *arg) {
while (1) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(¬_empty, &mutex);
}
int item = buffer[count - 1];
count--;
printf("Consumed: %d\n", item);
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&mutex);
sleep(2); // 模拟消费时间
}
}
int main() {
pthread_t producer_thread, consumer_thread;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(¬_full, NULL);
pthread_cond_init(¬_empty, NULL);
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&consumer_thread, NULL, consumer, NULL);
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
return 0;
}
这个示例演示了一个简单的生产者-消费者问题,其中有一个生产者线程和一个消费者线程,它们共享一个有限大小的缓冲区(buffer)。互斥锁 mutex 用于保护缓冲区的访问,条件变量not_full
和not_empty
用于通知生产者何时可以继续生产和消费者何时可以继续消费。
生产者线程生成数据并将其放入缓冲区,消费者线程从缓冲区中取出数据并处理。它们使用条件变量来等待合适的时机来执行这些操作,以避免竞态条件。
此示例中的sleep
函数用于模拟生产和消费的时间,以便更容易观察线程的交互。
三、代码详解
3.1 头文件
首先,包括必要的头文件和定义了一些全局变量
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0; // 记录缓冲区中的数据数量
pthread_mutex_t mutex;
pthread_cond_t not_full, not_empty;
-
buffer
是一个大小为5
的整数数组,用于表示共享的缓冲区 -
count
用于跟踪缓冲区中的数据数量 -
pthread_mutex_t
和pthread_cond_t
分别是互斥锁和条件变量的类型
3.2 生产者代码
接着,producer
函数是生产者线程的入口函数。它使用互斥锁来确保在访问共享资源(buffer
和 count
)时没有竞争。
void *producer(void *arg) {
int item = 1;
while (1) {
pthread_mutex_lock(&mutex); // 锁定互斥锁
while (count == BUFFER_SIZE) { // 如果缓冲区已满,等待
pthread_cond_wait(¬_full, &mutex);
}
buffer[count] = item; // 将数据放入缓冲区
count++;
printf("Produced: %d\n", item);
pthread_cond_signal(¬_empty); // 通知消费者缓冲区不为空
pthread_mutex_unlock(&mutex); // 解锁互斥锁
item++;
sleep(1); // 模拟生产时间
}
}
具体来说:
-
pthread_mutex_lock(&mutex)
锁定互斥锁,防止其他线程同时进入临界区。 -
while (count == BUFFER_SIZE)
检查缓冲区是否已满,如果满了就调用pthread_cond_wait
进入等待状态,等待消费者线程通知它缓冲区不再满。 - 一旦条件允许,生产者将数据放入缓冲区,增加
count
计数,并打印出生产的数据。 -
pthread_cond_signal(¬_empty)
通知消费者线程缓冲区不再为空,可以继续消费。 - 最后,解锁互斥锁
pthread_mutex_unlock(&mutex)
,以允许其他线程进入临界区。
3.3 消费者代码
然后,consumer
函数是消费者线程的入口函数,它执行类似的操作:
void *consumer(void *arg) {
while (1) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(¬_empty, &mutex);
}
int item = buffer[count - 1];
count--;
printf("Consumed: %d\n", item);
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&mutex);
sleep(2); // 模拟消费时间
}
}
- 先锁定互斥锁,确保在访问共享资源之前没有竞争。
- 然后检查缓冲区是否为空,如果是,就等待条件变量
not_empty
的通知。 - 一旦条件满足,消费者从缓冲区中取出数据,减少
count
计数,并打印出消费的数据。 - 最后,通过
pthread_cond_signal(¬_full)
通知生产者线程缓冲区不再满,可以继续生产。
3.4 main函数
最后,main
函数是程序的入口点,它初始化互斥锁和条件变量,然后创建了生产者和消费者线程。通过 pthread_join
等待线程的结束,并在程序结束前销毁互斥锁和条件变量。
int main() {
pthread_t producer_thread, consumer_thread;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(¬_full, NULL);
pthread_cond_init(¬_empty, NULL);
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&consumer_thread, NULL, consumer, NULL);
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
return 0;
}
3.5 总结
这个示例演示了如何使用互斥锁和条件变量来实现线程之间的协作,确保生产者和消费者线程可以正确地共享和操作缓冲区,同时避免竞态条件。这是一个基本的多线程同步示例,用于理解互斥锁和条件变量的基本概念。在实际应用中,可能需要更复杂的同步和错误处理机制。
四、Q&A
4.1 为什么一个线程上锁了,另一个线程就无法访问,另一个线程怎么知道上锁了?
在程序中人为规定代码,线程只有成功获取锁,才可以访问共享资源!
例如以下模板就是获取锁后才进行操作:
// 在线程中尝试获取锁
if (pthread_mutex_lock(&mutex) == 0) {
// 成功获取锁,可以访问共享资源
// ...
// 释放锁
pthread_mutex_unlock(&mutex);
} else {
// 锁已经被其他线程占用,无法访问共享资源
// 在此处可以选择等待或执行其他操作
}
或者
void *consumer(void *arg) {
while (1) {
pthread_mutex_lock(&mutex); //等待获取锁,才可以访问共享资源
...
pthread_mutex_unlock(&mutex); //对共享资源操作完,释放锁
... //执行自己的逻辑
}
}
在多线程的线程代码中,执行前首先判断是否能获取锁,获取锁后才能继续执行程序,否则等待。
注意:多线程的应用中通常会有多个锁,每个锁用于不同的目的或保护不同的共享资源,线程需要获取自己需要的锁,锁的数量和种类取决于应用的复杂性和需要。
举一个例子,考虑一个简单的文件系统模拟,其中有多个文件对象,每个文件对象都需要保护以防止并发访问。在这种情况下,可以使用多个锁,每个锁用于保护一个文件对象。这样,不同的线程可以并行访问不同的文件对象,而不会产生争用条件。
当一个线程获取了互斥锁(Mutex)并锁定了共享资源时,其他线程尝试获取相同互斥锁会被阻塞。这是因为互斥锁的主要作用是确保在任何给定时刻只有一个线程可以访问被保护的共享资源,以防止多个线程同时修改它,从而避免竞态条件(Race Condition)。
另一个线程如何知道互斥锁已经被上锁了?这是因为线程在尝试获取锁时,如果锁已经被其他线程占用,那么获取锁的操作会被阻塞,直到互斥锁被释放。在这种情况下,线程会等待在获取锁的地方,无法继续执行。这种等待是线程被动感知锁的状态的方式。
要知道是否成功获取互斥锁,通常线程可以检查获取锁的函数的返回值。在C语言中,使用pthread_mutex_lock来尝试获取锁,它会返回0表示成功获取锁,如果锁已被其他线程占用,则会阻塞等待,并在成功获取锁后返回。
4.2 pthread_cond_wait(¬_empty, &mutex);这里为什么还要用mutex变量这个函数干什么?
pthread_cond_wait
函数用于在等待条件变量的同时,释放互斥锁。这个函数通常与互斥锁一起使用,以实现复杂的线程同步。
当一个线程调用 pthread_cond_wait
时,它会进入等待状态,并且会释放与传递给函数的互斥锁相关联的互斥锁。这是因为等待条件变量的线程期望在某个条件满足时被唤醒,而在等待期间允许其他线程访问互斥锁保护的共享资源。
具体到代码行 pthread_cond_wait(¬_empty, &mutex);
:
-
¬_empty
是条件变量,它用于等待某个条件的发生。在这个示例中,它用于等待缓冲区不为空。 -
&mutex
是互斥锁,它用于保护共享资源(这里是buffer
和count
)。在等待条件变量期间,互斥锁会被释放,以允许其他线程访问共享资源。
当其他线程修改了共享资源并且认为某个条件已满足时,它们可以调用 pthread_cond_signal
或 pthread_cond_broadcast
来通知等待的线程条件已经满足,从而唤醒等待的线程。一旦被唤醒,等待的线程会尝试重新获取互斥锁,以继续执行,并检查条件是否真的满足。
所以,pthread_cond_wait
的作用是等待条件变量的信号,同时释放互斥锁以允许其他线程访问共享资源,从而实现线程之间的协作和同步。
4.3 sleep的时间
这里的sleep时间模仿的是实际代码逻辑处理时间,如进行协议解析等。
sleep时间的不同将导致程序处于不同的状态,在生产者-消费者问题示例中,sleep 的时间设置将影响缓冲区的状态和线程之间的交互方式。
具体来说,不同的 sleep 时间设置可以影响以下方面:
1. 缓冲区状态:
如果生产者和消费者的 sleep 时间相对较短,缓冲区可能很快填满或清空,取决于生产速度和消费速度。
如果 sleep 时间较长,可能会导致缓冲区保持在某种状态,例如,生产者和消费者都处于等待状态,或者缓冲区在某个特定数量的数据上保持平衡。
2. 线程交互:
线程的 sleep 时间设置会影响线程之间的交互方式。较短的 sleep 时间可能导致线程频繁地争夺互斥锁,从而增加竞争,但也可能导致较高的上下文切换。
较长的 sleep 时间可能减少线程争夺锁的频率,但也可能导致线程在等待期间浪费时间。