經典IPC問題實現與思考
本文介紹多種同步方法來解決程序間通訊問題,並給出模型的程式碼實現。
程式碼介紹
本文主要側重於程式碼實現,若對互斥(mutex)以及死鎖問題還不夠熟悉,可以參考我這篇文章。
-
include <pthread.h>
- pthread_mutex_t
- 用於建立互斥(mutex)變數
- pthread_mutex_lock
- 用於對互斥加鎖,防止其他程序(執行緒)進入臨界區。
- pthread_create
- 用於建立臨界區
- pthread_t
- 標識執行緒```
-
include <semaphore.h>
- sem_t
- 用於建立訊號量變數
- sem_init
- 訊號量初始化函式
- sem_wait
- 相當於Dijkstra的down操作
- sem_post
- 相當於Dijkstra的up操作
哲學家進餐問題
引言
對於問題的描述不過多介紹,可參考wiki。
最naive的想法就是哲學家先取左叉子,再取右叉,最後再依次放回就行。但很遺憾這種想法會造成死鎖。想象一下:若在同一時刻所有的哲學家都同時拿起左叉,那他們再也拿不到右叉,導致程式在一直執行卻又無法進行下去。
因此,我們很容易想到利用互斥(mutex)進行改進。我們將哲學家進餐的一系列動作都lock起來,這樣的確能避免死鎖,但這樣也會導致同一時間只有一個程序在進餐(資源浪費),有沒有更好的方法呢?
我們知道,若有n個哲學家,則最多n/2個哲學家同時進餐。我們可以首先利用mutex將取叉的動作lock住,同時檢查周圍的人是否正在進餐。這樣,我們就需要一個數組來標識每個哲學家的狀態。最後,我們把動作分解為take_forks,eat,think,put_forks
四個狀態,其中需要特別注意take_forks和put_forks
。
程式碼
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#define N 5
#define LEFT (i - 1 + N) % N
#define RIGHT (i + 1) % N
#define THINKING 0
#define HUNGRY 1
#define EATING 2
int state[N];
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
sem_t s[N];
void think(int i)
{
if (state[i] == THINKING)
{
printf("philosopher %d is thinking.......\n", i);
sleep(3);
}
}
void eat(int i)
{
if (state[i] == EATING)
{
printf("philosopher %d is eating.......\n", i);
sleep(3);
}
}
void init()
{
int i;
for (i = 0; i < N; i++)
{
if (sem_init(&s[i], 1, 1) != 0)
{
printf("sem_init is wrong\n");
}
}
}
void test(int i)
{
if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING)
{
state[i] = EATING;
sem_post(&s[i]);
return;
}
}
void take_forks(int i)
{
pthread_mutex_lock(&mutex);
state[i] = HUNGRY;
test(i);
pthread_mutex_unlock(&mutex);
sem_wait(&s[i]);
}
void put_forks(int i)
{
pthread_mutex_lock(&mutex);
state[i] = THINKING;
test(LEFT);
test(RIGHT);
pthread_mutex_unlock(&mutex);
}
void philosopher(int i)
{
while (1)
{
think(i);
take_forks(i);
eat(i);
put_forks(i);
}
}
int main()
{
int i = 0;
pthread_t id;
int ret;
init();
for (i = 0; i < N; i++)
{
ret = pthread_create(&id, NULL, (void *)philosopher, i);
if (ret != 0)
{
printf("Create pthread error!/n");
return 1;
}
}
pthread_join(id, NULL);
}
需要注意,我們的test
函式只是測試周圍的哲學家的狀態,若周圍哲學家已經在進餐,則不能拿起叉子,只能等待(63行),直到傳送訊號解除阻塞後才可以進食。
讀者-寫者問題
引言
哲學家進餐問題對於多個競爭程序互斥的訪問有限資源(如I/O裝置)這一類問題的建模十分有用,在讀者-寫者問題中,為資料庫訪問建立了一個模型。
我們知道,對一個成熟的資料庫來說,很重要的一點就是處理多併發請求的問題。在這裡我們規定:允許多個程序同時讀一個數據庫,但不允許讀寫同時進行,也不允許同時寫。
程式碼實現
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#define N 5
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
sem_t db; /* control the access of database */
int rc; /* the process want to access the database */
void init()
{
rc = 0;
if (sem_init(&db, 1, 1) != 0)
{
printf("sem_init is wrong\n");
}
}
void read_data(int i)
{
printf("person %d is reading.......\n", i);
sleep(3);
}
void finish_read(int i)
{
printf("person %d finish reading!\n", i);
}
void write_data()
{
printf("person is writing.......\n");
sleep(5);
}
void finish_write()
{
printf("person finish writing!\n");
}
void reader()
{
while (1)
{
pthread_mutex_lock(&mutex);
rc += 1;
if (rc == 1)
sem_wait(&db);
pthread_mutex_unlock(&mutex);
read_data(rc);
pthread_mutex_lock(&mutex);
rc -= 1;
finish_read(rc);
if (rc == 0)
sem_post(&db);
pthread_mutex_unlock(&mutex);
sleep(3); /* wait for another reading ... */
}
}
void writer()
{
while (1)
{
sem_wait(&db);
write_data();
finish_write();
sem_post(&db); /* restore the access */
sleep(5); /* wait for another writing ... */
}
}
int main()
{
int i = 0;
pthread_t id1, id2;
int ret1, ret2;
init();
for (i = 0; i < N; i++)
{
ret1 = pthread_create(&id1, NULL, (void *)reader, NULL);
ret2 = pthread_create(&id2, NULL, (void *)writer, NULL);
if (ret1 != 0 || ret2 != 0)
{
printf("Create pthread error!/n");
return 1;
}
}
pthread_join(id1, NULL);
pthread_join(id2, NULL);
return 1;
}
建模方式與以上類似,但要注意,這個模型有一定缺陷。比如,如果讀者的週期小於寫者的週期,那麼系統將一直允許讀者進入,而寫者一直被掛起直到沒有一個讀者為止。這個時候也許就會讓寫者等待很久。
因此,讀者可以自行思考更為合理的建模方式,這種建模方式是根據實際應用需求的,如:
- 讀者優先模式
- 寫者優先模式
- 嚴格按照時間戳讀寫