传送门:
进程同步与互斥——哲学家就餐问题源码实现(dining philosopher’s problem)
进程同步与互斥——读者/写者问题源码实现(reader-writer lock)
进程同步与互斥——吸烟者问题源码实现(cigarette smoker’s problem)
进程同步与互斥——理发师问题源码实现(sleeping barber problem)
摘自:操作系统导论
第一次尝试
第一次尝试解决该问题时,我们用两个信号量empty和full分别表示缓存区空或者满。put()和get()函数,下面是我们尝试解决生产者/消费者问题的代码。
#define MAX 10
int buffer[MAX];
int fill = 0;
int use = 0;
void put(int value)
{
buffer[fill] = value; //line f1
fill = (fill + 1) % MAX;
}
int get()
{
int tmp = buffer[use];
use = (use + 1) % MAX;
return tmp;
}
sem_t empty;
sem_t full;
void *producer(void *arg)
{
for(int i = 0; i < loops; i++)
{
sem_wait(&empty);
put(i);
sem_post(&full);
}
}
void *consumer(void *arg)
{
int i, tmp = 0;
while(tmp != -1)
{
sem_wait(&full)
tmp = get();
sem_post(&empty);
printf("%d\n", tmp);
}
}
int main()
{
sem_init(&empty, 0, MAX); // MAX buffers are empty to begin with...
sem_init(&full, 0, 0); // ...and 0 are full
}
你可以用更多的线程来尝试这个例子(即多个生产者和多个消费者)。它应该仍然正常运行。
我们现在假设MAX大于1(比如MAX=10)。对于这个例子,假定有多个生产者,多个消费者。现在就有问题了:竞态条件。你能够发现是哪里产生的吗?(花点时间找一下)如果没有发现,不妨仔细观察put()和get()的代码。
好,我们来理解该问题。假设两个生产者(Pa和Pb)几乎同时调用put()。当Pa先运行,在f1行先加入第一条数据(fill=0),假设Pa在将fill计数器更新为1之前被中断,Pb开始运行,也在f1行给缓冲区的0位置加入一条数据,这意味着那里的老数据被覆盖!这可不行,我们不能让生产者的数据丢失。
解决方案:增加互斥
你可以看到,这里忘了互斥。向缓冲区加入元素和增加缓冲区的索引是临界区,需要小心保护起来。所以,我们使用二值信号量来增加锁。
sem_t empty;
sem_t full;
sem_t mutex;
void *producer(void *arg)
{
for(int i = 0; i < loops; i++)
{
sem_wait(&mutex);
sem_wait(&empty);
put(i);
sem_post(&full);
sem_post(&mutex);
}
}
void *consumer(void *arg)
{
for(int i = 0; i < loops; i++)
{
sem_wait(&mutex);
sem_wait(&full)
tmp = get();
sem_post(&empty);
sem_post(&mutex);
printf("%d\n", tmp);
}
}
int main()
{
sem_init(&empty, 0, MAX); // MAX buffers are empty to begin with...
sem_init(&full, 0, 0); // ...and 0 are full
sem_init(&mutex, 0, 1); // mutex = 1, because it is a lock
}
现在我们给整个put()/get()部分都增加了锁。这似乎是正确的思路,但仍然有问题。为什么?死锁。为什么会发生死锁?考虑一下,尝试找出一个死锁的场景。必须以怎样的步骤执行,会导致程序死锁?
避免死锁
好,既然你想出来了,下面是答案。假设有两个线程,一个生产者和一个消费者。消费者首先运行,获得锁,然后对full信号量执行sem_wait() 。因为还没有数据,所以消费者阻塞,让出CPU。但是,重要的是,此时消费者仍然持有锁。
然后生产者运行。假如生产者能够运行,它就能生产数据并唤醒消费者线程。遗憾的是,它首先对二值互斥信号量调用sem_wait()。锁已经被持有,因此生产者也被卡住。
这里出现了一个循环等待。消费者持有互斥量,等待在full信号量上。生产者可以发送full信号,却在等待互斥量。因此,生产者和消费者互相等待对方——典型的死锁。
最后,可行的方案
要解决这个问题,只需减少锁的作用域。可以看到,我们把获取和释放互斥量的操作调整为紧挨着临界区,把full、empty的唤醒和等待操作调整到锁外面。结果得到了简单而有效的有界缓冲区,多线程程序的常用模式。现在理解,将来使用。未来的岁月中,你会感谢我们的。至少在期末考试遇到这个问题时,你会感谢我们。
sem_t empty;
sem_t full;
sem_t mutex;
void *producer(void *arg)
{
for(int i = 0; i < loops; i++)
{
sem_wait(&empty);
sem_wait(&mutex);
put(i);
sem_post(&mutex);
sem_post(&full);
}
}
void *consumer(void *arg)
{
for(int i = 0; i < loops; i++)
{
sem_wait(&full)
sem_wait(&mutex);
tmp = get();
sem_post(&mutex);
sem_post(&empty);
printf("%d\n", tmp);
}
}
int main()
{
sem_init(&empty, 0, MAX); // MAX buffers are empty to begin with...
sem_init(&full, 0, 0); // ...and 0 are full
sem_init(&mutex, 0, 1); // mutex = 1, because it is a lock
}
源码实现:
#include <semaphore.h>
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <time.h>
using namespace std;
#define MAX 10
int buffer[MAX];
int _fill = 0;
int use = 0;
void put(int value)
{
buffer[_fill] = value;
_fill = (_fill + 1) % MAX;
}
int get()
{
int tmp = buffer[use];
use = (use + 1) % MAX;
return tmp;
}
sem_t _empty;
sem_t full;
sem_t mutexlock;
#define loops 10
void *producer(void* arg)
{
for(int i = 0; i < loops; i++)
{
sem_wait(&_empty);
sem_wait(&mutexlock);
put(i);
sem_post(&mutexlock);
sem_post(&full);
cout << "producer put " << i << endl;
sleep(1);
}
pthread_exit(0);
}
void *consumer(void* arg)
{
for(int i = 0; i < loops; i++)
{
int tmp;
sem_wait(&full);
sem_wait(&mutexlock);
tmp = get();
sem_post(&mutexlock);
sem_post(&_empty);
cout << "consumer get " << tmp << endl;
sleep(2);
}
pthread_exit(0);
}
int main()
{
sem_init(&_empty, 0, MAX);
sem_init(&full, 0, 0);
sem_init(&mutexlock, 0, 1);
pthread_t pth1;
pthread_t pth2;
pthread_create(&pth1, nullptr, producer, nullptr);
pthread_create(&pth2, nullptr, consumer, nullptr);
pthread_join(pth1, nullptr);
pthread_join(pth2, nullptr);
return 0;
}