演示GIF(1.57MB),控制台输出效果(还有parser/vm/ui)自己实现:
信号量与PV原语
信号量为正数代表有空闲资源,为零与负数代表没有空闲资源。负数的绝对值代表等待的进程个数。信号量用(S,Q)表示,S是非负初值的整型变量,Q为进程等待队列。
P原语(阻塞):令S=S-1,若S>=0,进程继续执行,否则该进程变为等待状态,入队列Q
V原语(唤醒):令S=S+1,若S>0,进程继续执行,否则释放队列Q中第一个等待信号量的进程
实现互斥:令S=1。
用管道语义实现PV操作
当前jMiniOS支持的管道操作:create_pipe,创建管道,返回句柄
write_pipe,向管道中写入一个字符(非堵塞)
read_pipe,从管道中不断读取字符流(堵塞)
read_pipe_once,从管道中读取一个字符(堵塞)
destroy_pipe,销毁管道
用管道实现信号量:
var g_create_semaphore = func ~(name, n) {
if (call g_query_pipe(name)) {
return;
}
var pipe = call g_create_pipe(name);
call g_write_pipe(pipe, call g_string_rep("*", n));
return pipe;
};
export "g_create_semaphore";
var g_use_semaphore = func ~(name) {
return call g_create_pipe(name);
};
export "g_use_semaphore";
var g_destroy_semaphore = func ~(handle) {
call g_destroy_pipe_once(handle);
};
export "g_destroy_semaphore";
var g_lock_semaphore = func ~(handle) {
call g_read_pipe_once(handle);
};
export "g_lock_semaphore";
var g_unlock_semaphore = func ~(handle) {
call g_write_pipe(handle, "*");
};
export "g_unlock_semaphore";
关键:创建管道后,信号量的初始值=管道中存储的字符个数,即管道中的内容=信号量中可用资源。
P操作 = 向管道中写一字符
V操作 = 从管道中读一字符
Mutex实现,create_mutex = create_semaphore(1)
实现哲学家就餐问题
/* 哲学家就餐问题 */
var stage_philo = func ~() {
call word_typewrite("演示哲学家就餐问题! \n", 100);
var print_mutex = call g_create_mutex("philo_print");
foreach (var i : call g_range(1, 5)) {
call g_create_semaphore("fork_" + i, 1); // 每个叉最多能使用一次
}
var handles = [];
var philo = func ~(args) { // philo = 新建进程, args = 参数
var eat = call g_map_get(args, "eating");
var o = call g_map_get(args, "out");
var id = call g_map_get(args, "id");
var left_id = call g_map_get(args, "left_id"); // 左叉ID
var right_id = call g_map_get(args, "right_id"); // 右叉ID
var left = call g_use_semaphore("fork_" + left_id); // 左叉信号量
var right = call g_use_semaphore("fork_" + right_id); // 右叉信号量
call eat(o, "Philosophy#" + id + " ready");
foreach (var j : call g_range(1, 10)) { // 吃十次才饱
call g_lock_semaphore(left); // P左叉
call eat(o, "Philosophy#" + id + " is using fork#" + left_id);
call g_lock_semaphore(right); // P右叉
call eat(o, "Philosophy#" + id + " is using fork#" + right_id);
call eat(o, "Philosophy#" + id + " is eating. Process: " + j + "0%");
call g_unlock_semaphore(left); // V左叉
call g_unlock_semaphore(right); // V右叉
}
call eat(o, "Philosophy#" + id + " OK");
};
var eating = func ~(out, str) { // 输出
var pm = call g_use_mutex("philo_print");
call g_lock_mutex(pm);
foreach (var c : call g_range_string(str)) {
call g_write_pipe(out, c);
}
call g_write_pipe(out, '\n');
call g_task_sleep_ms(100);
call g_unlock_mutex(pm);
};
foreach (var j : call g_range(1, 5)) {
var args = {}; // 传参
call g_map_put(args, "eating", eating);
call g_map_put(args, "out", out);
call g_map_put(args, "id", j);
call g_map_put(args, "left_id", (j == 1) ? 5 : (j - 1));
call g_map_put(args, "right_id", (j == 5) ? 1 : (j + 1));
var h = call g_create_user_process_args(philo, args); // 新建进程
call g_array_add(handles, h);
call g_task_sleep(1);
}
call g_join_process_array(handles);
foreach (var k : call g_range(1, 5)) {
call g_destroy_semaphore(call g_use_semaphore("fork_" + k)); // 销毁
}
call g_destroy_mutex(call g_use_mutex("philo_print")); // 销毁
};
总结
从实例上看,哲学家不是同时开始就餐的,而是陆续就餐,同时就餐肯定会导致死锁问题。所以要有一定的防死锁和饥饿的解法,网上也有很多,有一种管程的解法。
例子中每个哲学家不会知道其他哲学家的状态,因此肯定会有缺陷啦,假如先让A就餐,然后A吃完再唤醒其他人,那就好办了。
唤醒语义,好东西,想想linux中的进程状态实现,soga。因此,我们的程序除了要支持lock/unlock之外,还要支持wait/notify,下回分解。
===========================================
0903 更新:用管程实现(jMiniLang语言)
/* 哲学家就餐问题 - 管程解决 */
var stage_philo2 = func ~() {
call word_typewrite("【管程】演示哲学家就餐问题! \n", 100);
call g_create_mutex("philo_print");
call g_create_mutex("philo_monitor");
var states = [];
call g_array_add(states, g_null);
foreach (var i : call g_range(1, 5)) {
call g_create_semaphore("philo_" + i, 1); // 记录每个哲学家的状态(管程)
call g_array_add(states, "thinking"); // 开始时哲学家都在思考
}
var handles = [];
var philo2 = func ~(args) {
var eat = call g_map_get(args, "eating"); // 拿参数
var states = call g_map_get(args, "states");
var o = call g_map_get(args, "out");
var id = call g_map_get(args, "id");
var left_id = call g_map_get(args, "left_id");
var right_id = call g_map_get(args, "right_id");
var monitor = call g_use_mutex("philo_monitor");
call eat(o, "Philosophy#" + id + " ready");
var enter = func ~() { // 进入临界区
var monitor = call g_use_mutex("philo_monitor");
call g_lock_mutex(monitor); // mutex自带等待队列
call g_printdn("Philosophy#" + id + " entered critical section");
};
var leave = func ~() { // 离开临界区
call g_printdn("Philosophy#" + id + " leaved critical section");
var monitor = call g_use_mutex("philo_monitor");
call g_unlock_mutex(monitor);
};
var wait = func ~(_id) { // 等待信号
var sem = call g_use_semaphore("philo_" + _id);
call g_printdn("Philosophy#" + _id + " waiting");
call g_lock_mutex(sem); // semaphore自带等待队列
};
var signal = func ~(_id) { // 发出信号
var sem = call g_use_semaphore("philo_" + _id);
call g_unlock_mutex(sem);
call g_printdn("Philosophy#" + _id + " received signal");
};
var test = func ~(_id) { // 测试哲学家是否具备进餐条件
var _left_id = (_id == 1) ? 5 : (_id - 1);
var _right_id = (_id == 5) ? 1 : (_id + 1);
if ((call g_array_get(states, _left_id) != "eating") && // 如果左右都不在进餐
(call g_array_get(states, _right_id) != "eating") &&
(call g_array_get(states, _id) == "hungry")) { // 且自己为饥饿状态
call signal(_id); // 发出就餐信号
} else {
call g_printdn("Test failed. #" + _left_id + ": " + call g_array_get(states, _left_id) +
", #" + _right_id + ": " + call g_array_get(states, _right_id) + ", #" +
_id + ": " + call g_array_get(states, _id));
}
};
var pickup = func ~() { // 拿起叉子
call enter();
call g_array_set(states, id, "hungry"); // 设置状态是饥饿
call test(id); // 看看自己能否用餐
call leave();
if (call g_array_get(states, id) != "eating") { // 如果尝试失败
call wait(id); // 等待
call g_array_set(states, id, "eating"); // 设置为进餐状态
} // 这里设置状态不会冲突,因为pickup只能由一个哲学家调用
};
var putdown = func ~() { // 放下叉子
call enter();
call g_array_set(states, id, "thinking"); // 设置状态是思考
call test(left_id); // 测试左边的哲学家可否就餐
call test(right_id); // 测试右边的哲学家可否就餐
call leave();
};
foreach (var j : call g_range(1, 10)) {
call eat(o, "Philosophy#" + id + " is thinking");
call pickup();
call eat(o, "Philosophy#" + id + " is eating. Process: " + j + "0%");
call putdown();
}
call eat(o, "Philosophy#" + id + " OK");
};
var eating = func ~(out, str) {
var pm = call g_use_mutex("philo_print");
call g_lock_mutex(pm);
foreach (var c : call g_range_string(str)) {
call g_write_pipe(out, c);
}
call g_write_pipe(out, '\n');
call g_task_sleep_ms(100);
call g_unlock_mutex(pm);
};
foreach (var j : call g_range(1, 5)) {
var args = {};
call g_map_put(args, "eating", eating);
call g_map_put(args, "states", states);
call g_map_put(args, "out", out);
call g_map_put(args, "id", j);
call g_map_put(args, "left_id", (j == 1) ? 5 : (j - 1));
call g_map_put(args, "right_id", (j == 5) ? 1 : (j + 1));
var h = call g_create_user_process_args(philo2, args); // fork
call g_array_add(handles, h);
}
call g_join_process_array(handles);
foreach (var k : call g_range(1, 5)) {
call g_destroy_semaphore(call g_use_semaphore("fork_" + k));
}
call g_destroy_mutex(call g_use_mutex("philo_print"));
};
从输出来看,哲学家就餐的顺序是1->3->5->2->4,是非常有规律的、重复的、完美的顺序,这说明了管程的使用是非常有效的,同时证明了wait/signal机制的优越性:降低了锁的竞争程度(引入休眠),同时唤醒机制保证了进程同步中各进程的运行顺序。也就是说,如果一开始就出现了1->3->5->2->4循环,那么这个循环就一直存在直到程序结束。
【深入】如果哲学家是6个、7个,那么就餐顺序又如何呢?