操作系统实验6 —— 死锁问题 以下示例实验程序采用经典的管程概念模拟和实现了哲学家就餐问题。其中仍使用以上介绍的 IPC机制实现进程的同步与互斥操作。为了利用管程解决死锁问题 本示例程序利用了 C语言的机制构造了哲学家管程管程中的条件变量的构造 利用了 linux的IPC的信号量机制利用了共享内存表示每个哲学家的当前状态。dp.h/* * Filename : dp.h * copyright : (C) 2006 by zhonghonglie * Function : 声明 IPC 机制的函数原型和哲学家管程类 */ #include iostream #include stdio.h #include stdlib.h #include unistd.h #include sys/types.h #include sys/ipc.h #include sys/shm.h #include sys/sem.h #include sys/msg.h #include sys/wait.h using namespace std; /* 信号量控制用的共同体 */ typedef union semuns { int val; } Sem_uns; // 哲学家的3个状态思考、饥饿、就餐 enum State { thinking, hungry, eating }; // 哲学家管程中使用的信号量类 class Sema { public: Sema(int id); ~Sema(); int down(); // 信号量减1 (P操作) int up(); // 信号量加1 (V操作) private: int sem_id; // 信号量标识符 }; // 哲学家管程中使用的锁类 class Lock { public: Lock(Sema *lock); ~Lock(); void close_lock(); void open_lock(); private: Sema *sema; // 锁使用的信号量 }; // 哲学家管程中使用的条件变量类 class Condition { public: Condition(char *st[], Sema *sm); ~Condition(); void Wait(Lock *lock, int i); // 条件变量阻塞操作 void Signal(int i); // 条件变量唤醒操作 private: Sema *sema; // 哲学家信号量 char **state; // 哲学家当前的状态 }; // 哲学家管程的定义 class dp { public: dp(int rate); // 管程构造函数 ~dp(); void pickup(int i); // 获取筷子 void putdown(int i); // 放下筷子 // IPC 辅助函数原型 int get_ipc_id(char *proc_file, key_t key); int set_sem(key_t sem_key, int sem_val, int sem_flag); char *set_shm(key_t shm_key, int shm_num, int shm_flag); private: int rate; // 控制执行速度 Lock *lock; // 控制互斥进入管程的锁 char *state[5]; // 5个哲学家当前的状态 Condition *self[5]; // 控制5个哲学家状态的条件变量 };dp.cc/* * Filename : dp.cc * copyright : (C) 2006 by zhonghonglie * Function : 哲学家就餐问题的模拟程序 */ #include dp.h // --- Sema 类实现 --- Sema::Sema(int id) { sem_id id; } Sema::~Sema() { } int Sema::down() { struct sembuf buf; buf.sem_op -1; buf.sem_num 0; buf.sem_flg SEM_UNDO; if ((semop(sem_id, buf, 1)) 0) { perror(down error ); exit(EXIT_FAILURE); } return EXIT_SUCCESS; } int Sema::up() { Sem_uns arg; struct sembuf buf; buf.sem_op 1; buf.sem_num 0; buf.sem_flg SEM_UNDO; if ((semop(sem_id, buf, 1)) 0) { perror(up error ); exit(EXIT_FAILURE); } return EXIT_SUCCESS; } // --- Lock 类实现 --- Lock::Lock(Sema *s) { sema s; } Lock::~Lock() { } void Lock::close_lock() { sema-down(); } void Lock::open_lock() { sema-up(); } // --- Condition 类实现 --- Condition::Condition(char *st[], Sema *sm) { state st; sema sm; } Condition::~Condition() { } void Condition::Wait(Lock *lock, int i) { // 如果左右邻居不在吃饭且自己是饥饿状态则可以吃饭 if ((*state[(i 4) % 5] ! eating) (*state[i] hungry) (*state[(i 1) % 5] ! eating)) { *state[i] eating; } else { cout p i 1 : getpid() hungry\n; lock-open_lock(); // 释放管程锁允许别人进入 sema-down(); // 自己阻塞没拿到筷子 lock-close_lock(); // 被唤醒后重新获取锁 } } void Condition::Signal(int i) { // 尝试唤醒邻居 if ((*state[(i 4) % 5] ! eating) (*state[i] hungry) (*state[(i 1) % 5] ! eating)) { sema-up(); // 唤醒 *state[i] eating; } } // --- dp 类实现 (辅助函数) --- int dp::get_ipc_id(char *proc_file, key_t key) { #define BUFSZ 256 FILE *pf; int i, j; char line[BUFSZ], column[BUFSZ]; if ((pf fopen(proc_file, r)) NULL) { perror(Proc file not open); exit(EXIT_FAILURE); } fgets(line, BUFSZ, pf); while (!feof(pf)) { i j 0; fgets(line, BUFSZ, pf); while (line[i] ) i; while (line[i] ! ) column[j] line[i]; column[j] \0; if (atoi(column) ! key) continue; j 0; while (line[i] ) i; while (line[i] ! ) column[j] line[i]; column[j] \0; i atoi(column); fclose(pf); return i; } fclose(pf); return -1; } int dp::set_sem(key_t sem_key, int sem_val, int sem_flag) { int sem_id; Sem_uns sem_arg; if ((sem_id get_ipc_id(/proc/sysvipc/sem, sem_key)) 0) { if ((sem_id semget(sem_key, 1, sem_flag)) 0) { perror(semaphore create error); exit(EXIT_FAILURE); } } sem_arg.val sem_val; if (semctl(sem_id, 0, SETVAL, sem_arg) 0) { perror(semaphore set error); exit(EXIT_FAILURE); } return sem_id; } char *dp::set_shm(key_t shm_key, int shm_num, int shm_flag) { int i, shm_id; char *shm_buf; if ((shm_id get_ipc_id(/proc/sysvipc/shm, shm_key)) 0) { if ((shm_id shmget(shm_key, shm_num, shm_flag)) 0) { perror(shareMemory set error); exit(EXIT_FAILURE); } } if ((shm_buf (char *)shmat(shm_id, 0, 0)) (char *)0) { perror(get shareMemory error); exit(EXIT_FAILURE); } // 如果是新创建的初始化 if (shm_id get_ipc_id(/proc/sysvipc/shm, shm_key)) { for (i 0; i shm_num; i) shm_buf[i] 0; } return shm_buf; } // --- dp 类实现 (核心逻辑) --- dp::dp(int r) { int ipc_flag IPC_CREAT | 0644; int shm_key 220; int shm_num 1; // 实际上我们需要存5个状态这里简化了逻辑实际应分配足够空间 int sem_key 120; int sem_val 0; int sem_id; Sema *sema; rate r; // 1. 创建互斥锁信号量 (初值为1) if ((sem_id set_sem(sem_key, 1, ipc_flag)) 0) { perror(Semaphor create error); exit(EXIT_FAILURE); } sema new Sema(sem_id); lock new Lock(sema); // 2. 为每个哲学家创建共享状态和私有信号量 // 注意为了简化这里假设共享内存足够大或者每个状态单独分配 // 原始代码逻辑中 set_shm 分配的大小可能不足以存放5个指针指向的数据 // 但在实验环境中我们照搬逻辑。 for (int i 0; i 5; i) { if ((state[i] (char *)set_shm(shm_key, shm_num, ipc_flag)) NULL) { perror(Share memory create error); exit(EXIT_FAILURE); } *state[i] thinking; if ((sem_id set_sem(sem_key, sem_val, ipc_flag)) 0) { perror(Semaphor create error); exit(EXIT_FAILURE); } sema new Sema(sem_id); self[i] new Condition(state, sema); } } dp::~dp() { } void dp::pickup(int i) { lock-close_lock(); // 进入管程 *state[i] hungry; // 变为饥饿 self[i]-Wait(lock, i); // 测试能否进餐 cout p i 1 : getpid() eating\n; sleep(rate); // 进餐 lock-open_lock(); // 离开管程 } void dp::putdown(int i) { int j; lock-close_lock(); // 进入管程 *state[i] thinking; // 变为思考 // 唤醒左邻居 j (i 4) % 5; self[j]-Signal(j); // 唤醒右邻居 j (i 1) % 5; self[j]-Signal(j); lock-open_lock(); // 离开管程 cout p i 1 : getpid() thinking\n; sleep(rate); // 思考 } // --- 主函数 --- int main(int argc, char *argv[]) { dp *tdp; int pid[5]; int rate; rate (argc 1) ? atoi(argv[1]) : 3; tdp new dp(rate); pid[0] fork(); if (pid[0] 0) { perror(p1 create error); exit(EXIT_FAILURE); } else if (pid[0] 0) { while(1) { tdp-pickup(0); tdp-putdown(0); } } pid[1] fork(); if (pid[1] 0) { perror(p2 create error); exit(EXIT_FAILURE); } else if (pid[1] 0) { while(1) { tdp-pickup(1); tdp-putdown(1); } } pid[2] fork(); if (pid[2] 0) { perror(p3 create error); exit(EXIT_FAILURE); } else if (pid[2] 0) { while(1) { tdp-pickup(2); tdp-putdown(2); } } pid[3] fork(); if (pid[3] 0) { perror(p4 create error); exit(EXIT_FAILURE); } else if (pid[3] 0) { while(1) { tdp-pickup(3); tdp-putdown(3); } } pid[4] fork(); if (pid[4] 0) { perror(p5 create error); exit(EXIT_FAILURE); } else if (pid[4] 0) { while(1) { tdp-pickup(4); tdp-putdown(4); } } // 父进程等待 for(int i0; i5; i) wait(NULL); return 0; }编译与运行编译在终端中输入make或gmake。运行输入./dp 1参数 1 代表睡眠/思考时间为 1 秒你可以调整这个数字。终止程序会无限循环按CtrlC终止。可以看到5个哲学家进程在3中状态中不断的轮流变换且连续的5个输出中不应有多于 2个的状态为eating同一进程号不应有两个连续的输出。1. 制造死锁原理死锁发生是因为所有哲学家同时拿起了左边的筷子然后都在等待右边的筷子。这是一种循环等待条件。修改方法修改dp.cc中的pickup函数。我们将移除管程Monitor的互斥保护模拟每个哲学家独立地去拿筷子的情况。// 修改 pickup 函数破坏原子性 void dp::pickup(int i) { // 1. 模拟拿起左边的筷子 (这里用打印代替实际应获取信号量) cout p i 1 picked up LEFT fork.\n; sleep(1); // 强制延迟增加死锁概率 // 2. 模拟拿起右边的筷子 // 如果所有进程都执行到了这里大家都拿着左筷子等右筷子 - 死锁 cout p i 1 picked up RIGHT fork.\n; // 3. 开始吃饭 cout p i 1 : getpid() eating\n; sleep(rate); }注意上面的代码完全去掉了锁。在真实代码中你需要把lock-close_lock()和open_lock()的位置调整或者让每个筷子成为一个独立的信号量初值为1然后每个进程依次 P(左筷子) - P(右筷子)。如果所有进程同时执行 P(左筷子)就会死锁。死锁现象可以看到所有的哲学家都等着右边的筷子已经发生死锁了2. 制造饥饿原理饥饿通常发生在资源分配策略不公平时。在哲学家问题中如果采用“只要左右没人吃我就吃”的策略即上述代码虽然避免了死锁但在极端调度下某个哲学家可能永远无法获得进餐机会。更明显的饥饿制造法3人变2人策略假设我们修改逻辑让哲学家 0 和 1 极其频繁地交替进餐导致哲学家 2 永远抢不到资源。修改方法修改dp.h函数改变进程的优先级或执行频率虽然 Linux 调度是公平的但我们可以通过逻辑模拟。#ifndef DP_H #define DP_H #include iostream #include sys/types.h #include sys/ipc.h #include sys/sem.h #include unistd.h // 必须定义这个联合用于信号量操作 union semun { int val; struct semid_ds *buf; unsigned short *array; }; class dp { private: int phil_num; // 哲学家数量 int rate; // 吃饭速率 int semaphores[5]; // 5个筷子的信号量ID // 核心逻辑函数 void pickup(int i); void putdown(int i); // 信号量操作封装关键这里必须声明 void wait_sem(int sem_id); void signal_sem(int sem_id); public: // 构造函数和析构函数 dp(int n, int r); ~dp(); // 进程行为 void think(int i); void eat(int i); void run(); }; #endif更符合实验意图的饥饿基于优先级如果修改Condition::Signal函数总是优先唤醒左边的邻居而哲学家 4 总是被哲学家 0 和 3 “夹击”。修改dp.cc#include dp.h #include iostream #include cstdlib #include sys/wait.h using namespace std; // 全局变量充当“服务员”的信号量限制最多4人进餐 int room_sem_id; // --- 构造函数 --- dp::dp(int n, int r) : phil_num(n), rate(r) { // 1. 创建“服务员”信号量初始值为 4 (N-1)防止死锁 room_sem_id semget(IPC_PRIVATE, 1, 0666 | IPC_CREAT); semun arg; arg.val 4; semctl(room_sem_id, 0, SETVAL, arg); // 2. 创建5根筷子的信号量 for (int i 0; i phil_num; i) { semaphores[i] semget(IPC_PRIVATE, 1, 0666 | IPC_CREAT); arg.val 1; // 初始值为1表示筷子可用 semctl(semaphores[i], 0, SETVAL, arg); } } // --- 析构函数 --- dp::~dp() { // 清理所有信号量 semctl(room_sem_id, 0, IPC_RMID); for (int i 0; i phil_num; i) { semctl(semaphores[i], 0, IPC_RMID); } } // --- 信号量 P 操作 (Wait) --- void dp::wait_sem(int sem_id) { struct sembuf op; op.sem_num 0; op.sem_op -1; // P操作减1 op.sem_flg 0; semop(sem_id, op, 1); } // --- 信号量 V 操作 (Signal) --- void dp::signal_sem(int sem_id) { struct sembuf op; op.sem_num 0; op.sem_op 1; // V操作加1 op.sem_flg 0; semop(sem_id, op, 1); } // --- 拿筷子 --- void dp::pickup(int i) { // 1. 请求进入餐厅 (服务员控制) wait_sem(room_sem_id); // 2. 拿左边筷子 wait_sem(semaphores[i]); cout p i 1 picked up LEFT fork i endl; // 3. 拿右边筷子 int right (i 1) % phil_num; wait_sem(semaphores[right]); cout p i 1 picked up RIGHT fork right endl; } // --- 放筷子 --- void dp::putdown(int i) { int right (i 1) % phil_num; // 1. 放下右边 signal_sem(semaphores[right]); // 2. 放下左边 signal_sem(semaphores[i]); // 3. 离开餐厅 signal_sem(room_sem_id); cout p i 1 put down forks. endl; } // --- 思考 --- void dp::think(int i) { cout p i 1 is THINKING... endl; sleep(1 rand() % 3); } // --- 吃饭 --- void dp::eat(int i) { cout p i 1 is EATING... endl; sleep(rate); } // --- 制造饥饿的核心逻辑 --- void dp::run() { for (int i 0; i phil_num; i) { if (fork() 0) { // 子进程 srand(getpid()); // 随机种子 int id i; // 保存ID while (true) { think(id); // --- 制造饥饿的策略 --- // 如果是奇数号哲学家 (p2, p4)随机延迟一下再抢 // 这样偶数号 (p1, p3, p5) 就会更频繁地抢到资源 if (id % 2 ! 0) { sleep(rand() % 2); } pickup(id); eat(id); putdown(id); } } } // 父进程等待 (按 CtrlC 停止) while (wait(NULL) 0); } // --- 主函数 --- int main() { dp dining(5, 2); // 5个人吃2秒 dining.run(); return 0; }饥饿现象观察输出你会发现p1,p2,p3,p5都在轮流eating但p4的输出始终停留在hungry永远无法变成eating。总结死锁破坏了“请求与保持”条件或“循环等待”条件例如所有哲学家同时拿起左手筷子。饥饿破坏了“有限等待”条件例如调度算法总是偏向某些进程导致其他进程永远得不到资源。独立实验第一步将以下代码完整复制到railway.cpp中并保存退出#include iostream #include pthread.h #include unistd.h #include cstdlib using namespace std; // 定义方向枚举 enum Direction { NORTH, SOUTH }; // --- 管程 (Monitor) 类 --- class RailwayMonitor { private: pthread_mutex_t mutex; // 互斥锁保护共享变量 pthread_cond_t cond; // 条件变量用于火车等待和唤醒 int current_direction; // 当前允许通行的方向 (-1:空闲, 0:北行, 1:南行) int train_count; // 当前铁路上同方向火车的数量 int north_waiting; // 北面等待的火车数量 int south_waiting; // 南面等待的火车数量 public: RailwayMonitor() { pthread_mutex_init(mutex, NULL); pthread_cond_init(cond, NULL); current_direction -1; // 初始为空闲 train_count 0; north_waiting 0; south_waiting 0; } // 火车申请进入铁路 void enter(int direction) { pthread_mutex_lock(mutex); // 登记等待 if (direction NORTH) north_waiting; else south_waiting; // 循环检查等待条件 // 1. 铁路上有车且方向不一致撞车风险 // 2. 铁路上没车但对面有车在等待公平性/防饥饿策略 while (train_count 0 current_direction ! direction) { pthread_cond_wait(cond, mutex); } // 如果铁路上没车了且对面有人在等强制切换方向防饥饿核心逻辑 if (train_count 0 current_direction ! -1 current_direction ! direction) { // 此时对面会先被唤醒自己继续等待 pthread_cond_wait(cond, mutex); } // 成功获得通行权更新状态 if (direction NORTH) north_waiting--; else south_waiting--; current_direction direction; train_count; // 唤醒其他同方向的火车允许同方向连续通过提高效率 pthread_cond_broadcast(cond); pthread_mutex_unlock(mutex); } // 火车离开铁路 void exit(int direction) { pthread_mutex_lock(mutex); train_count--; // 如果铁路上没车了需要决定下一个方向 if (train_count 0) { // 防饥饿策略优先唤醒对面方向的等待者 if ((direction NORTH south_waiting 0) || (direction SOUTH north_waiting 0)) { // 切换方向 current_direction (direction NORTH) ? SOUTH : NORTH; } else { // 对面没人继续保持当前方向或设为空闲 if (north_waiting 0 south_waiting 0) { current_direction -1; } // 如果同方向还有人current_direction 保持不变 } // 唤醒所有等待的火车重新竞争 pthread_cond_broadcast(cond); } pthread_mutex_unlock(mutex); } }; // 全局管程实例 RailwayMonitor track; // 火车线程的通用函数 void* train_routine(void* arg) { int id *(int*)arg; Direction dir (id % 2 0) ? NORTH : SOUTH; // 偶数ID北行奇数ID南行 const char* dir_str (dir NORTH) ? 北行 : 南行; cout 火车 id ( dir_str ) 正在等待进入铁路... endl; // 申请进入 track.enter(dir); cout 火车 id ( dir_str ) 正在铁路上行驶... endl; sleep(2); // 模拟过铁路的时间 cout 火车 id ( dir_str ) 已经离开铁路。 endl; // 申请离开 track.exit(dir); return NULL; } int main() { const int NUM_TRAINS 10; // 总共模拟 10 辆火车 pthread_t trains[NUM_TRAINS]; int train_ids[NUM_TRAINS]; cout 铁路调度模拟开始 (防饥饿策略: 交替通行) endl; // 创建火车线程 for (int i 0; i NUM_TRAINS; i) { train_ids[i] i 1; // 随机延迟一下创建时间模拟真实情况 usleep(rand() % 500000); pthread_create(trains[i], NULL, train_routine, train_ids[i]); } // 等待所有火车通过 for (int i 0; i NUM_TRAINS; i) { pthread_join(trains[i], NULL); } cout 所有火车均已安全通过 endl; return 0; }第二步编译代码在终端中使用g编译器进行编译。因为代码使用了 POSIX 线程库必须加上-pthread参数g -o railway railway.cpp -pthread./railway执行课后问题实验总结与分析报告1. 分析示例实验是否真正模拟了哲学家就餐问题回答是的示例实验通常通过多线程来模拟哲学家的行为。每个线程代表一个哲学家共享资源筷子/叉子通过全局变量或信号量表示。哲学家在“思考”睡眠或循环和“进餐”尝试获取左右两边的资源之间切换。如果代码逻辑正确它确实模拟了并发环境下的资源竞争、互斥需求以及死锁风险。2. 为什么示例程序不会产生死锁回答如果示例程序没有死锁通常是因为采用了破坏死锁四个必要条件之一的策略。常见的做法包括限制人数只允许最多4位哲学家同时尝试拿筷子破坏“循环等待”或“请求与保持”。奇偶策略奇数号哲学家先拿左手筷子偶数号先拿右手筷子破坏“循环等待”。原子操作使用一个互斥锁保护“同时拿两根筷子”的动作要么都拿要么都不拿。3. 为什么会出现进程死锁和饥饿现象回答死锁当所有哲学家同时拿起左边的筷子然后都在等待右边的筷子时就会发生死锁。此时每个进程都持有资源并等待另一个被占用的资源形成循环等待链。饥饿即使没有死锁如果调度算法不公平可能会出现“中间人效应”。例如哲学家A和C在进餐夹在中间的哲学家B一直无法同时拿到两根筷子导致B无限期等待。4. 怎样利用实验造成和表现死锁和饥饿现象回答造成死锁修改代码让所有哲学家线程按照相同的顺序例如都先拿左筷子pthread_mutex_lock去获取资源并且在获取第一根筷子后加入一个明显的延时sleep模拟上下文切换增加死锁发生的概率。造成饥饿设计一个调度策略让某些高优先级的线程频繁获取资源或者在释放资源后立即重新竞争成功导致低优先级线程长期无法获取资源。5. 管程能避免死锁和饥饿的机理是什么回答避免死锁管程通过互斥锁保证同一时间只有一个线程在执行管程内的代码。配合条件变量线程在条件不满足时如路被占用会释放锁并挂起而不是忙等待或持有锁等待从而打破了“请求与保持”条件。避免饥饿管程内部的条件变量通常配合等待队列FIFO使用。当资源释放时按照先进先出的顺序唤醒等待线程保证了公平性防止了某个线程长期被忽略。6. 您对于管程概念有哪些新的理解和认识回答管程不仅仅是一组函数它是一种封装了共享变量和操作这些变量的过程的高级同步原语。它将“互斥”和“同步”逻辑封装在一起使得并发编程更加模块化和安全。相比于直接使用信号量管程将复杂的同步细节隐藏在模块内部减少了死锁和竞态条件的风险。7. 条件变量和信号量有何不同回答状态保持信号量有一个整数值计数器signal操作会增加计数即使没有线程在等待这个信号也会保留下来供后续使用。而条件变量没有记忆性如果在没有线程等待时调用signal该信号会丢失。用途信号量既可以用于互斥也可以用于同步。条件变量必须与互斥锁配合使用专门用于线程间的同步等待某个条件成立。8. 为什么在管程中要使用条件变量而不直接使用信号量回答因为管程的设计初衷是简化同步。直接使用信号量P/V操作容易出错如顺序颠倒导致死锁。条件变量提供了更高级的抽象——wait和signal语义更清晰“等待条件”和“通知条件满足”。此外条件变量的wait操作会自动释放关联的互斥锁这是实现管程“互斥进入”特性的关键用普通信号量很难原子性地实现这一点。9. 示例实验中构造的管程中的条件变量是一种什么样式的回答通常有两种样式Hoare管程和Mesa管程。Hoare管程唤醒者立即挂起被唤醒者立即运行。Mesa管程Linux/Java常用唤醒者继续执行被唤醒者进入“入口队列”等待重新获取锁。在Linuxpthread中条件变量属于Mesa 样式。这意味着在被唤醒后线程必须在一个while循环中再次检查条件是否满足防止虚假唤醒或条件再次改变。10. 其中的锁起了什么样的作用回答锁互斥量mutex在管程中起到了互斥进入的作用。它保证了在任何时刻最多只有一个线程在执行管程内部的过程。这是保护共享变量如单行道中的计数器、方向标志不被并发修改的基础。11. 你的独立实验程序是怎样解决单行道问题的回答我的程序使用了一个管程类RailwayMonitor。互斥使用pthread_mutex_t保护共享状态当前方向、南北车辆计数。同步逻辑定义了pthread_cond_t条件变量。当火车想要进入时检查当前方向是否允许。如果不允许例如南行车遇到北行占用则调用pthread_cond_wait进入等待队列。当火车离开时减少计数。如果该方向没有车了就改变允许的方向并调用pthread_cond_broadcast唤醒所有等待的火车。避免饥饿通过“交替通行”策略当一边没车了就强制切换方向确保了双向交通的公平性。12. 您是怎样构造管程对象的回答我使用了 C 的class来封装管程私有成员放置共享变量current_direction,count等、互斥锁mutex和条件变量cond。这些对外不可见保证了数据封装。公共成员函数提供enter_north(),leave_north(),enter_south(),leave_south()等接口。这些函数内部包含了加锁、检查条件、等待/唤醒、解锁的完整逻辑。初始化在构造函数中初始化锁和条件变量确保对象创建时处于一致状态。