线程互斥量和信号量的使用(未完成)
线程互斥量和信号量的使用
- 信号量和互斥量的介绍
- 1. 线程进入阻塞状态
- 2. 线程收到信号
- 3. 线程竞争互斥锁
- 1.初始化:
- 2.生产者线程:
- 3.消费者线程:
- 4.销毁:
- 疑问1:发送信号量和解锁顺序可以调整吗
- 答:不推荐,在释放锁和唤醒线程之间,可能会有其他线程修改共享资源的状态,导致被唤醒的线程检查条件时条件不满足,从而再次进入等待状态。
信号量和互斥量的介绍
总结:信号量的参于带来线程阻塞到就绪状态,当等待的线程收到信号后,先变为就绪状态,争抢到互斥量后(能够上锁后),才能够进入运行态
在使用条件变量和互斥锁进行线程同步时,带有条件变量等待的线程会经历一系列状态变化,从阻塞开始,到收到信号,再到获取互斥量。下面详细介绍这些状态变化。
1. 线程进入阻塞状态
当线程调用 pthread_cond_wait 函数时,它会执行以下操作:
释放互斥锁:pthread_cond_wait 函数会自动释放当前线程持有的互斥锁,这样其他线程就可以获取该互斥锁并访问共享资源。
进入阻塞状态:线程会进入条件变量的等待队列,处于阻塞状态,等待其他线程发送信号。
2. 线程收到信号
当其他线程调用 pthread_cond_signal(唤醒一个等待的线程)或 pthread_cond_broadcast(唤醒所有等待的线程)函数时,等待队列中的线程会收到信号。收到信号后,线程会从阻塞状态变为就绪状态,但此时它还没有获取互斥锁,不能立即继续执行。
3. 线程竞争互斥锁
收到信号的线程进入就绪状态后,会参与互斥锁的竞争。具体情况如下:
互斥锁未被持有:如果此时互斥锁没有被其他线程持有,收到信号的线程可以立即获取互斥锁,从就绪状态变为运行状态,继续执行 pthread_cond_wait 函数之后的代码。
互斥锁被其他线程持有:如果互斥锁被其他线程持有,收到信号的线程会再次进入阻塞状态,等待互斥锁被释放。当互斥锁被释放后,该线程会与其他等待该互斥锁的线程一起竞争,竞争成功后才能获取互斥锁并继续执行。
示例代码及状态变化说明
1.初始化:
在 main 函数中,对互斥锁和条件变量进行初始化。
2.生产者线程:
在 producer 函数中,线程先加锁,检查缓冲区是否已满,若已满则调用 pthread_cond_wait 函数阻塞。生产数据后,调用 pthread_cond_signal 函数唤醒等待的消费者线程,最后解锁。
3.消费者线程:
在 consumer 函数中,线程先加锁,检查缓冲区是否为空,若为空则调用 pthread_cond_wait 函数阻塞。消费数据后,调用 pthread_cond_signal 函数唤醒等待的生产者线程,最后解锁。
4.销毁:
在 main 函数结束前,销毁互斥锁和条件变量。
下面是之前生产者 - 消费者模型的示例代码,结合代码说明线程的状态变化:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
// 缓冲区
int buffer[BUFFER_SIZE];
int count = 0;
int in = 0;
int out = 0;
// 互斥锁和条件变量
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;
// 生产者线程函数
void *producer(void *arg) {
for (int i = 0; i < 10; i++) {
// 加锁
pthread_mutex_lock(&mutex);
// 等待缓冲区不满
while (count == BUFFER_SIZE) {
pthread_cond_wait(¬_full, &mutex);
}
// 生产数据
buffer[in] = i;
printf("Produced: %d\n", buffer[in]);
in = (in + 1) % BUFFER_SIZE;
count++;
// 唤醒等待的消费者线程
pthread_cond_signal(¬_empty);
// 解锁
pthread_mutex_unlock(&mutex);
// 模拟生产时间
sleep(1);
}
return NULL;
}
// 消费者线程函数
void *consumer(void *arg) {
for (int i = 0; i < 10; i++) {
// 加锁
pthread_mutex_lock(&mutex);
// 等待缓冲区不为空
while (count == 0) {
pthread_cond_wait(¬_empty, &mutex);
}
// 消费数据
int data = buffer[out];
printf("Consumed: %d\n", data);
out = (out + 1) % BUFFER_SIZE;
count--;
// 唤醒等待的生产者线程
pthread_cond_signal(¬_full);
// 解锁
pthread_mutex_unlock(&mutex);
// 模拟消费时间
sleep(1);
}
return NULL;
}
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;
}
疑问1:发送信号量和解锁顺序可以调整吗
//唤醒等待的消费者线程
pthread_cond_signal(¬_empty);
//解锁
pthread_mutex_unlock(&mutex);
以上顺序可以调换吗
答:不推荐,在释放锁和唤醒线程之间,可能会有其他线程修改共享资源的状态,导致被唤醒的线程检查条件时条件不满足,从而再次进入等待状态。
在大多数情况下,pthread_cond_signal 和 pthread_mutex_unlock 的调用顺序不建议调换,即通常应该先调用 pthread_cond_signal 唤醒等待的线程,再调用 pthread_mutex_unlock 解锁,下面详细解释原因。
原顺序(先 pthread_cond_signal 后 pthread_mutex_unlock)
原代码的顺序如下:
// 唤醒等待的消费者线程
pthread_cond_signal(¬_empty);
// 解锁
pthread_mutex_unlock(&mutex);
这种顺序是合理且推荐的,原因如下:
减少线程上下文切换开销:当调用 pthread_cond_signal 时,被唤醒的线程会进入就绪状态,但由于互斥锁仍被当前线程持有,被唤醒的线程会阻塞在互斥锁上。之后当前线程释放锁,被唤醒的线程能立即获取锁并继续执行,减少了不必要的线程上下文切换。
避免竞态条件:确保被唤醒的线程能在锁被释放后立即获得锁,避免其他线程在释放锁和唤醒线程之间获取锁,从而减少竞态条件的发生。
调换顺序(先 pthread_mutex_unlock 后 pthread_cond_signal)
若将顺序调换,代码如下:
// 解锁
pthread_mutex_unlock(&mutex);
// 唤醒等待的消费者线程
pthread_cond_signal(¬_empty);
这种顺序可能会导致以下问题:
增加线程上下文切换开销:释放锁后,其他等待该锁的线程可能会立即获取锁。当调用 pthread_cond_signal 唤醒等待条件变量的线程时,被唤醒的线程可能需要再次等待锁的释放,增加了线程上下文切换的次数。
潜在的竞态条件:在释放锁和唤醒线程之间,可能会有其他线程修改共享资源的状态,导致被唤醒的线程检查条件时条件不满足,从而再次进入等待状态。