一、猴子拿苹果问题
逛脉脉时,看到一网友遇到的面试题:有9个苹果,2只猴子。一个猴子每次拿2个苹果,一个猴子每次拿3个苹果。如果剩余的苹果数量不够猴子拿的数量,则停止拿苹果。请用多线程的方式模拟上面的描述。
看到问题的第一眼,觉得很有趣,脑海中第一个想到的就是通过信号量来实现,因为信号量是最适合做线程和进程状态同步了,而问题的考点就是线程同步。
恰好这两天刚好有在看信号量,所以很容易就想到通过信号量来实现这个问题。
其实原题说的是通过java多线程实现,但是我不会java,只能用c来写了。
为什么说考查的问题点是线程同步?因为题目要求是通过多线程来实现,如果不对线程状态制约,9个苹果,很容易在一瞬间就被一个猴子拿完,或许第二个线程还没开始苹果就已经拿完了。这明显不是面试官想看到的。所以不难想到考点就是如何协调两个猴子(线程)有序的拿桃子,有序的意思就是:你拿一次,我拿一次,要有次序的进行,直到把桃子拿完。如何控制线程之间你执行一次,我执行一次,肯定就要利用同步了。
二、匿名信号
匿名信号比较适用于线程间同步,因为它没有名字,所以多个进程间无法通过一个载体来共享它。只有在线程中,通过变量或者参数传递的方式来共享,所以适用于线程。
有名信号的用法参考:进程间通信之信号量。
匿名信号的原理也和有名信号一样,维持一个计数器,然后通过等待计数器的状态来继续往下执行。相关的函数:
1 2 3 4 5 6 7 8 9 |
#include <semaphore.h> // 初始化信号量 int sem_init(sem_t *sem, int pshared, unsigned int value); // 发送信号量 int sem_post(sem_t *sem); // 等待信号量 int sem_wait(sem_t *sem); // 关闭信号量 int sem_close(sem_t *sem); |
四个函数中,下面三个和有名信号量中的一样,唯一有区别的是第一个初始化信号量函数。匿名信号量无需传入信号量的名字,直接传入信号量对象就可以初始化了。后面的两个参数也都是一样,一个代表共享属性,一个代表默认值。
三、实现猴子拿苹果问题
两个猴子,肯定是需要两个线程,线程中辨别是哪个猴子肯定要传入猴子的参数,因此先实现一个猴子信息的对象,传入线程参数:
1 2 3 4 5 6 7 |
// 每个猴子的信息 struct monkey_info { int id; // 猴子的ID int cnt; // 每次拿苹果的数量 sem_t *my; // 自己的信号量 sem_t *other; // 另外一个猴子的信号量 }; |
为什么需要两个信号量呢,逻辑是:我去拿猴子,拿完通知你拿,你去拿猴子,我等在这里,等你拿完通知我拿。一个是等待自己的信号量,一个是通知另外猴子的信号量,所以需要两个。
线程函数实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 苹果总量 static int s_apple_cnt = 9; void *get_apple(void *ptr) { struct monkey_info *monkey = ptr; if (monkey == NULL) return NULL; while (1) { // 等待信号量 sem_wait(monkey->my); if (s_apple_cnt < monkey->cnt) { // 当前的苹果数量不足以给到当前猴子 printf("monkey %d: apple has't enough! total: %d, need: %d\n", monkey->id, s_apple_cnt, monkey->cnt); // 退出前,通知另外猴子继续拿 sem_post(monkey->other); break; } else { // 拿苹果,减库存 s_apple_cnt -= monkey->cnt; printf("monkey %d: get %d apple! remaining apples: %d\n", monkey->id, monkey->cnt, s_apple_cnt); } // 通知另外猴子 sem_post(monkey->other); } return NULL; } |
主函数实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
int main() { sem_t sem1, sem2; pthread_t tid1, tid2; // 初始化两个猴子的状态 struct monkey_info monkey1 = {1, 2, &sem1, &sem2}; struct monkey_info monkey2 = {2, 3, &sem2, &sem1}; // 初始化信号 sem_init(&sem1, 0, 1); sem_init(&sem2, 0, 0); // 创建线程 pthread_create(&tid1, NULL, get_apple, (void *)&monkey1); pthread_create(&tid2, NULL, get_apple, (void *)&monkey2); // 等待线程退出 pthread_join(tid1, NULL); pthread_join(tid2, NULL); // 退出信号量 sem_close(&sem1); sem_close(&sem2); return 0; } |
执行结果:
过程描述:
- 第一个猴子拿2个苹果,剩余7个苹果。
- 第二个猴子拿3个苹果,剩余4个苹果。
- 第一个猴子拿2个苹果,剩余2个苹果。
- 第二个猴子想要拿3个苹果,但是苹果不够了,退出。
- 第一个猴子拿两个苹果,苹果被拿完。
- 第一个猴子想要拿2个苹果,但是苹果不够了,退出。
评论