经典同步问题

🔥 高优先级
真题练习
同步问题设计 是操作系统解答题必考题,本节的几个经典问题虽然不会直接考察,但是其中的设计思想还是需要深入掌握。

生产者消费者问题

Producer
Producer
Consumer
Consumer
Text is not SVG - cannot display

生产者消费者问题是并发编程中的经典问题,涉及到两种线程 —— 生产者消费者,它们共享一个固定大小的缓冲区或存储区。

  • 生产者的任务是生成数据并将其 放入缓冲区
  • 消费者的任务是 从缓冲区中取出 并消费这些数据。

关键的挑战在于确保生产者不会在缓冲区满时添加数据,同时确保消费者不会在缓冲区空时尝试消费数据。

semaphore mutex = 1;          // 临界区互斥信号量
semaphore empty = n;          // 空闲缓冲区数量
semaphore full  = 0;          // 忙缓冲区数量

producer() {
    while (1) {
        P(empty)              // 等待一个空位置
        P(mutex)              // 进入临界区前先获取mutex
        ....                  // 将数据项添加到缓冲区
        V(mutex)              // 离开临界区,释放mutex
        V(full)               // 增加一个数据项的计数
    }
}

consumer() {
    while (1) {
        P(full)               // 等待一个数据项
        P(mutex)              // 进入临界区前先获取mutex
        ...                  // 从缓冲区取出数据项并消费
        V(mutex)              // 离开临界区,释放mutex
        V(empty)              // 增加一个空位置的计数
    }
}

生产者消费者过程可以参考以下流程图理解:

ProducerConsumer生产者消费者问题cluster_semaphores信号量定义cluster_buffer共享缓冲区 (大小: n)cluster_producer生产者 (Producer)cluster_consumer消费者 (Consumer)mutexmutex = 1(互斥信号量)emptyempty = n(空闲缓冲区)fullfull = 0(忙缓冲区)buffer缓冲区[][][]...[](固定大小)cons_consume从缓冲区取出并消费数据buffer->cons_consume取出数据prod_start开始prod_wait_emptyP(empty)等待空位置prod_start->prod_wait_emptyprod_wait_empty->emptyP操作prod_wait_mutexP(mutex)获取互斥锁prod_wait_empty->prod_wait_mutexprod_wait_mutex->mutexP操作prod_produce生产数据放入缓冲区prod_wait_mutex->prod_produceprod_produce->buffer放入数据prod_release_mutexV(mutex)释放互斥锁prod_produce->prod_release_mutexprod_release_mutex->mutexV操作prod_signal_fullV(full)增加数据计数prod_release_mutex->prod_signal_fullprod_signal_full->fullV操作prod_signal_full->prod_wait_empty循环cons_start开始cons_wait_fullP(full)等待数据项cons_start->cons_wait_fullcons_wait_full->fullP操作cons_wait_mutexP(mutex)获取互斥锁cons_wait_full->cons_wait_mutexcons_wait_mutex->mutexP操作cons_wait_mutex->cons_consumecons_release_mutexV(mutex)释放互斥锁cons_consume->cons_release_mutexcons_release_mutex->mutexV操作cons_signal_emptyV(empty)增加空位计数cons_release_mutex->cons_signal_emptycons_signal_empty->emptyV操作cons_signal_empty->cons_wait_full循环note1关键点:• empty信号量防止缓冲区溢出• full信号量防止空缓冲区消费• mutex信号量保证临界区互斥

读者 - 写者问题

reader
reader
reader
reader
writer
writer
x
reader
writer
writer
x
x
writer
可以多个读者一起读,
但是此时写者不能写
系统一个时刻最多允许一个写者在写,
并且此时其他读者和写者都不允许操作

读者写者问题 是另一个经典的并发编程问题,涉及到对 共享数据资源 的访问,这些 资源 可以被 多个读者 同时读取,但只能被 一个写者 写入,而且当 写者 正在写入数据时,没有其他 读者写者 可以访问该 资源

这个问题的挑战在于两点:

  • 允许多个读者同时读取资源。
  • 确保当有一个写者访问资源时,没有其他读者或写者可以同时访问。
int read_count = 0;
semaphore wrt  = 1;
semphore mutex = 1;

reader() {
    while (1) {
        P(mutex)                     // 获取互斥访问权,以修改 read_count
        read_count += 1
        if (read_count == 1) {       // 如果这是第一个读者,需要锁定资源,防止写者写入
            P(wrt)
        }
        V(mutex)                     // 释放互斥访问权
        ...                          // 读取资源
        P(mutex)                     // 获取互斥访问权,以修改 read_count
        read_count -= 1
        if (read_count == 0) {       // 如果没有读者在读取,释放资源,允许写者写入
            V(wrt)
        }
        V(mutex)                     // 释放互斥访问权
    }
}

writer() {
    while (1) {
        P(wrt)                       // 获取资源的互斥访问权
        ...                          // 写入资源
        V(wrt)                       // 释放资源的互斥访问权
    }
}
int read_count = 0;
int write_count = 0;
semaphore wrt = 1;
semaphore mutex = 1;
semaphore write_mutex = 1;

reader() {
    while (1) {
        P(write_mutex)                  // 在读取之前,确保没有写者正在等待或写入
        P(mutex)                        // 获取互斥访问权,以修改 read_count
        read_count += 1
        if (read_count == 1) {
            P(wrt)
        }
        V(mutex)
        V(write_mutex)

        ...                             // 读取资源

        P(mutex)                        // 获取互斥访问权,以修改read_count
        read_count -= 1
        if (read_count == 0) {
            V(wrt)
        }
        V(mutex)
    }
}

writer() {
    while (1) {
        P(write_mutex)                  // 获取互斥访问权,以修改 write_count
        write_count += 1
        if (write_count == 1) {         // 如果这是第一个写者,锁定资源,防止新的读者读取
            P(wrt)
        }
        V(write_mutex)

        ...                             // 写入资源

        P(write_mutex)                  // 获取互斥访问权,以修改write_count
        write_count -= 1
        if (write_count == 0) {         // 如果没有其他写者在等待或写入,释放资源
            V(wrt)
        }
        V(write_mutex)
    }
}
int read_count = 0;
int write_count = 0;
semaphore wrt = 1;
semaphore mutex = 1;
semaphore queue = 1; // 新增队列信号量,以确保公平性

reader() {
    while (1) {
        P(queue);                 // 进入队列
        P(mutex);                 // 获取互斥访问权,以修改 read_count
        read_count += 1;
        if (read_count == 1) {
            P(wrt);               // 如果是第一个读者,锁定资源
        }
        V(mutex);
        V(queue);                 // 离开队列
        ...                       // 读取资源
        P(mutex);                 // 获取互斥访问权,以修改 read_count
        read_count -= 1;
        if (read_count == 0) {
            V(wrt);               // 如果是最后一个读者,释放资源
        }
        V(mutex);
    }
}

writer() {
    while (1) {
        P(queue);                 // 进入队列
        P(wrt);                   // 锁定资源
        ...                       // 写入资源
        V(wrt);                   // 释放资源
        V(queue);                 // 离开队列
    }
}

试题中如果考察读者写者问题的话,一般考察的还是读者优先,读者优先的同步实现方案可以通过以下流程图进行理解:

ReaderWriterProblemcluster_shared共享资源区域cluster_semaphores信号量cluster_reader读者进程流程cluster_writer写者进程流程cluster_legend图例说明shared_resource共享资源(数据文件/数据库)wrt_semwrt = 1(写入互斥)mutex_semmutex = 1(读者计数互斥)read_countread_count = 0(当前读者数)reader_start读者开始reader_p_mutex1P(mutex)获取计数器锁reader_start->reader_p_mutex1reader_p_mutex1->mutex_semreader_incread_count++reader_p_mutex1->reader_increader_inc->read_countreader_check1read_count == 1?reader_inc->reader_check1reader_p_wrtP(wrt)锁定资源reader_check1->reader_p_wrtreader_v_mutex1V(mutex)释放计数器锁reader_check1->reader_v_mutex1reader_p_wrt->wrt_sem竞争reader_p_wrt->reader_v_mutex1reader_read读取资源reader_v_mutex1->reader_readreader_read->shared_resource读取reader_p_mutex2P(mutex)获取计数器锁reader_read->reader_p_mutex2reader_p_mutex2->mutex_semreader_decread_count--reader_p_mutex2->reader_decreader_dec->read_countreader_check2read_count == 0?reader_dec->reader_check2reader_v_wrtV(wrt)释放资源锁reader_check2->reader_v_wrtreader_v_mutex2V(mutex)释放计数器锁reader_check2->reader_v_mutex2reader_v_wrt->reader_v_mutex2reader_end读者结束(循环)reader_v_mutex2->reader_endreader_end->reader_startwriter_start写者开始writer_p_wrtP(wrt)获取资源独占锁writer_start->writer_p_wrtwriter_p_wrt->wrt_sem竞争writer_write写入资源writer_p_wrt->writer_writewriter_write->shared_resource写入writer_v_wrtV(wrt)释放资源锁writer_write->writer_v_wrtwriter_end写者结束(循环)writer_v_wrt->writer_endwriter_end->writer_startlegend1🔵 读者操作legend2🔴 写者操作legend3🟡 信号量操作legend4🟢 共享资源note核心机制说明:1. 多个读者可以同时读取2. 写者需要独占访问3. 第一个读者锁定wrt4. 最后一个读者释放wrt5. mutex保护read_count

哲学家就餐问题

假设有五位 哲学家 坐在一个 圆桌 周围,每两位哲学家之间有一把 叉子。哲学家的生活由 思考和吃饭 两种活动组成。为了吃饭,一个哲学家需要两把叉子——左边和右边的一把。问题在于,如何设计一个算法使得哲学家们可以正常就餐,而不会因为竞争叉子而导致死锁或饥饿。

哲学家就餐问题有多种解法,这里只提供一种 最直观的解法,对于包含 N 位哲学家的问题:- 前 N-1 个哲学家先拿起 左边的叉子,再拿起 右边的叉子 - 最后一个 哲学家先拿起 右边的叉子,再拿起 左边的叉子

semaphore fork[5] = {1, 1, 1, 1, 1};      // 五个叉子,初始都是可用的

void philosopher(int i) {
    if (i < 5) {
        // 对于前面的哲学家,先左后右
        first = i;
        second = (i + 1) % 5;
    } else {
        // 对于最后一个哲学家,先右后左
        first = (i + 1) % 5;
        second = i;
    }
    while (1) {
        think();
        P(fork[first]);
        P(fork[second]);
        eat();
        V(fork[first]);
        V(fork[second]);
    }
}

生产者消费者过程可以参考以下流程图理解:

DiningPhilosopherscluster_solution解决方案cluster_table哲学家就餐问题cluster_states哲学家状态solution解决死锁的策略:• 前4个哲学家:先拿左叉子,再拿右叉子• 最后1个哲学家:先拿右叉子,再拿左叉子这样可以打破循环等待条件,避免死锁P0哲学家0先拿左(0)后右(1)F0叉子0P0->F0F4叉子4P0->F4P1哲学家1先拿左(1)后右(2)P1->F0F1叉子1P1->F1P2哲学家2先拿左(2)后右(3)P2->F1F2叉子2P2->F2P3哲学家3先拿左(3)后右(4)P3->F2F3叉子3P3->F3P4哲学家4先拿右(0)后左(4)P4->F3P4->F4F0->P1F1->P2F2->P3F3->P4F4->P0thinking思考hungry饥饿thinking->hungry变饿eating进餐hungry->eating拿到两把叉子eating->thinking放下叉子