一、猴子拿苹果问题

逛脉脉时,看到一网友遇到的面试题:有9个苹果,2只猴子。一个猴子每次拿2个苹果,一个猴子每次拿3个苹果。如果剩余的苹果数量不够猴子拿的数量,则停止拿苹果。请用多线程的方式模拟上面的描述。

看到问题的第一眼,觉得很有趣,脑海中第一个想到的就是通过信号量来实现,因为信号量是最适合做线程和进程状态同步了,而问题的考点就是线程同步。

恰好这两天刚好有在看信号量,所以很容易就想到通过信号量来实现这个问题。

其实原题说的是通过java多线程实现,但是我不会java,只能用c来写了。

为什么说考查的问题点是线程同步?因为题目要求是通过多线程来实现,如果不对线程状态制约,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);

四个函数中,下面三个和有名信号量中的一样,唯一有区别的是第一个初始化信号量函数。匿名信号量无需传入信号量的名字,直接传入信号量对象就可以初始化了。后面的两个参数也都是一样,一个代表共享属性,一个代表默认值。

三、实现猴子拿苹果问题

两个猴子,肯定是需要两个线程,线程中辨别是哪个猴子肯定要传入猴子的参数,因此先实现一个猴子信息的对象,传入线程参数:

// 每个猴子的信息
struct monkey_info {
    int id; // 猴子的ID
    int cnt; // 每次拿苹果的数量
    sem_t *my; // 自己的信号量
    sem_t *other; // 另外一个猴子的信号量
};

为什么需要两个信号量呢,逻辑是:我去拿猴子,拿完通知你拿,你去拿猴子,我等在这里,等你拿完通知我拿。一个是等待自己的信号量,一个是通知另外猴子的信号量,所以需要两个。

线程函数实现:

// 苹果总量
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;
}

主函数实现:

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;
}

执行结果:

过程描述:

  1. 第一个猴子拿2个苹果,剩余7个苹果。
  2. 第二个猴子拿3个苹果,剩余4个苹果。
  3. 第一个猴子拿2个苹果,剩余2个苹果。
  4. 第二个猴子想要拿3个苹果,但是苹果不够了,退出。
  5. 第一个猴子拿两个苹果,苹果被拿完。
  6. 第一个猴子想要拿2个苹果,但是苹果不够了,退出。
最后修改:2020 年 02 月 26 日
如果觉得我的文章对你有用,请随意赞赏