一种消除潜在死锁和实际死锁的方法是使用一种令牌机制,该机制要求哲学家必须收到令牌后才能尝试进餐。可用令牌的数量必须少于餐桌前哲学家的人数。当哲学家收到令牌后,他可以根据餐桌规则尝试进餐。每位哲学家在进餐后归还令牌,然后重复该过程。以下伪代码显示了使用令牌机制时每位哲学家的逻辑。
while (there is still food on the table) { get token grab right fork grab left fork eat some food put down left fork put down right fork return token }
以下几节将详细介绍令牌机制的两种不同实现。
下面列出了使用令牌机制的哲学家就餐程序的修正版本。该解决方案结合了四个令牌,比就餐人数少一个,因此最多有四位哲学家可同时尝试进餐。该版本的程序称为 din_philo_fix1.c:
1 /* 2 * Copyright (c) 2006, 2011, Oracle and/or its affiliates. All Rights Reserved. 3 */ 4 5 #include <pthread.h> 6 #include <stdio.h> 7 #include <unistd.h> 8 #include <stdlib.h> 9 #include <errno.h> 10 #include <assert.h> 11 12 #ifdef __linux__ 13 #include <stdint.h> 14 #endif 15 16 #define PHILOS 5 17 #define DELAY 5000 18 #define FOOD 100 19 20 void *philosopher (void *id); 21 void grab_chopstick (int, 22 int, 23 char *); 24 void down_chopsticks (int, 25 int); 26 int food_on_table (); 27 int get_token (); 28 void return_token (); 29 30 pthread_mutex_t chopstick[PHILOS]; 31 pthread_t philo[PHILOS]; 32 pthread_mutex_t food_lock; 33 pthread_mutex_t num_can_eat_lock; 34 int sleep_seconds = 0; 35 uint32_t num_can_eat = PHILOS - 1; 36 37 38 int 39 main (int argn, 40 char **argv) 41 { 42 int i; 43 44 pthread_mutex_init (&food_lock, NULL); 45 pthread_mutex_init (&num_can_eat_lock, NULL); 46 for (i = 0; i < PHILOS; i++) 47 pthread_mutex_init (&chopstick[i], NULL); 48 for (i = 0; i < PHILOS; i++) 49 pthread_create (&philo[i], NULL, philosopher, (void *)i); 50 for (i = 0; i < PHILOS; i++) 51 pthread_join (philo[i], NULL); 52 return 0; 53 } 54 55 void * 56 philosopher (void *num) 57 { 58 int id; 59 int i, left_chopstick, right_chopstick, f; 60 61 id = (int)num; 62 printf ("Philosopher %d is done thinking and now ready to eat.\n", id); 63 right_chopstick = id; 64 left_chopstick = id + 1; 65 66 /* Wrap around the chopsticks. */ 67 if (left_chopstick == PHILOS) 68 left_chopstick = 0; 69 70 while (f = food_on_table ()) { 71 get_token (); 72 73 grab_chopstick (id, right_chopstick, "right "); 74 grab_chopstick (id, left_chopstick, "left"); 75 76 printf ("Philosopher %d: eating.\n", id); 77 usleep (DELAY * (FOOD - f + 1)); 78 down_chopsticks (left_chopstick, right_chopstick); 79 80 return_token (); 81 } 82 83 printf ("Philosopher %d is done eating.\n", id); 84 return (NULL); 85 } 86 87 int 88 food_on_table () 89 { 90 static int food = FOOD; 91 int myfood; 92 93 pthread_mutex_lock (&food_lock); 94 if (food > 0) { 95 food--; 96 } 97 myfood = food; 98 pthread_mutex_unlock (&food_lock); 99 return myfood; 100 } 101 102 void 103 grab_chopstick (int phil, 104 int c, 105 char *hand) 106 { 107 pthread_mutex_lock (&chopstick[c]); 108 printf ("Philosopher %d: got %s chopstick %d\n", phil, hand, c); 109 } 110 111 112 113 void 114 down_chopsticks (int c1, 115 int c2) 116 { 117 pthread_mutex_unlock (&chopstick[c1]); 118 pthread_mutex_unlock (&chopstick[c2]); 119 } 120 121 122 int 123 get_token () 124 { 125 int successful = 0; 126 127 while (!successful) { 128 pthread_mutex_lock (&num_can_eat_lock); 129 if (num_can_eat > 0) { 130 num_can_eat--; 131 successful = 1; 132 } 133 else { 134 successful = 0; 135 } 136 pthread_mutex_unlock (&num_can_eat_lock); 137 } 138 } 139 140 void 141 return_token () 142 { 143 pthread_mutex_lock (&num_can_eat_lock); 144 num_can_eat++; 145 pthread_mutex_unlock (&num_can_eat_lock); 146 }
尝试编译这一修正版本的哲学家就餐程序,并多次运行该程序。令牌机制限制了尝试使用筷子的就餐人数,从而可避免实际死锁和潜在死锁。
要进行编译,请使用以下命令:
% cc -g -o din_philo_fix1 din_philo_fix1.c
收集实验:
% collect -r deadlock -o din_philo_fix1.1.er din_philo_fix1
即使是使用令牌机制,线程分析器也会在不存在死锁时报告该实现存在潜在死锁。这是一个误报。请参考以下包含潜在死锁详细信息的屏幕抓图。
图 10 误报的潜在死锁
选择循环链中的第一个线程(线程 2),然后单击 "Dual Source"(双源)视图,可以看到线程 2 在地址 0x216a8 持有锁的对应源代码位置,以及该线程在 0x216c0 请求锁的对应源代码位置。下图显示了对应于线程 2 的 "Dual Source"(双源)视图。
图 11 误报的潜在死锁的源代码
din_philo_fix1.c 中的 get_token() 函数使用 while 循环来同步线程。线程在成功获得令牌之前不会离开 while 循环(当 num_can_eat 大于 0 时可成功获得令牌)。while 循环将同时就餐的人数限制为 4。但是,线程分析器无法识别由 while 循环实现的同步。它认为所有五位哲学家同时尝试抓取筷子并进餐,因此报告了一个潜在死锁。下一节将详细说明如何使用线程分析器可识别的同步来限制同时就餐的人数。
下面列出了令牌机制的另一种实现。该实现仍然使用四个令牌,所以最多有四个就餐者可同时尝试进餐。但是,该实现使用 sem_wait() 和 sem_post() 信号例程限制就餐的哲学家人数。该版本的源文件称为 din_philo_fix2.c。
下面列出了详细的 din_philo_fix2.c:
1 /* 2 * Copyright (c) 2006, 2011, Oracle and/or its affiliates. All Rights Reserved. 3 */ 4 5 #include <pthread.h> 6 #include <stdio.h> 7 #include <unistd.h> 8 #include <stdlib.h> 9 #include <errno.h> 10 #include <assert.h> 11 #include <semaphore.h> 12 13 #define PHILOS 5 14 #define DELAY 5000 15 #define FOOD 100 16 17 void *philosopher (void *id); 18 void grab_chopstick (int, 19 int, 20 char *); 21 void down_chopsticks (int, 22 int); 23 int food_on_table (); 24 int get_token (); 25 void return_token (); 26 27 pthread_mutex_t chopstick[PHILOS]; 28 pthread_t philo[PHILOS]; 29 pthread_mutex_t food_lock; 30 int sleep_seconds = 0; 31 sem_t num_can_eat_sem; 32 33 34 int 35 main (int argn, 36 char **argv) 37 { 38 int i; 39 40 pthread_mutex_init (&food_lock, NULL); 41 sem_init(&num_can_eat_sem, 0, PHILOS - 1); 42 for (i = 0; i < PHILOS; i++) 43 pthread_mutex_init (&chopstick[i], NULL); 44 for (i = 0; i < PHILOS; i++) 45 pthread_create (&philo[i], NULL, philosopher, (void *)i); 46 for (i = 0; i < PHILOS; i++) 47 pthread_join (philo[i], NULL); 48 return 0; 49 } 50 51 void * 52 philosopher (void *num) 53 { 54 int id; 55 int i, left_chopstick, right_chopstick, f; 56 57 id = (int)num; 58 printf ("Philosopher %d is done thinking and now ready to eat.\n", id); 59 right_chopstick = id; 60 left_chopstick = id + 1; 61 62 /* Wrap around the chopsticks. */ 63 if (left_chopstick == PHILOS) 64 left_chopstick = 0; 65 66 while (f = food_on_table ()) { 67 get_token (); 68 69 grab_chopstick (id, right_chopstick, "right "); 70 grab_chopstick (id, left_chopstick, "left"); 71 72 printf ("Philosopher %d: eating.\n", id); 73 usleep (DELAY * (FOOD - f + 1)); 74 down_chopsticks (left_chopstick, right_chopstick); 75 76 return_token (); 77 } 78 79 printf ("Philosopher %d is done eating.\n", id); 80 return (NULL); 81 } 82 83 int 84 food_on_table () 85 { 86 static int food = FOOD; 87 int myfood; 88 89 pthread_mutex_lock (&food_lock); 90 if (food > 0) { 91 food--; 92 } 93 myfood = food; 94 pthread_mutex_unlock (&food_lock); 95 return myfood; 96 } 97 98 void 99 grab_chopstick (int phil, 100 int c, 101 char *hand) 102 { 103 pthread_mutex_lock (&chopstick[c]); 104 printf ("Philosopher %d: got %s chopstick %d\n", phil, hand, c); 105 } 106 107 void 108 down_chopsticks (int c1, 109 int c2) 110 { 111 pthread_mutex_unlock (&chopstick[c1]); 112 pthread_mutex_unlock (&chopstick[c2]); 113 } 114 115 116 int 117 get_token () 118 { 119 sem_wait(&num_can_eat_sem); 120 } 121 122 void 123 return_token () 124 { 125 sem_post(&num_can_eat_sem); 126 }
这一新实现使用信号 num_can_eat_sem 来限制可同时就餐的哲学家人数。信号 num_can_eat_sem 初始化为 4,比哲学家人数少一个。尝试进餐前,哲学家调用 get_token(),后者又调用 sem_wait(&num_can_eat_sem)。调用 sem_wait() 会导致执行调用的哲学家进入等待状态,直到信号值变为正值,然后将信号值减 1。哲学家进餐完毕后,他将调用 return_token(),后者又调用 sem_post(&num_can_eat_sem)。调用 sem_post() 可将信号值加 1。线程分析器可识别对 sem_wait() 和 sem_post() 的调用,并判定并非所有的哲学家都同时尝试进餐。
要编译 din_philo_fix2.c,请使用以下命令:
% cc -g -lrt -o din_philo_fix2 din_philo_fix2.c
如果多次运行程序 din_philo_fix2 的这一新实现,将会发现它每次都正常终止,不会挂起。
在这一新的二进制代码上创建实验:
% collect -r deadlock -o din_philo_fix2.1.er din_philo_fix2
您将发现线程分析器不报告 din_philo_fix2.1.er 实验中的任何实际死锁或潜在死锁,如下图所示。
图 12 未报告 din_philo_fix2.c 中的任何死锁
有关线程分析器可识别的线程和内存分配 API 的列表,请参见线程分析器可识别的 API。