|
11 | 11 |
|
12 | 12 | 信号量是一种特殊的变量,可用于线程同步。它只取自然数值,并且只支持两种操作:P(SV): 如果信号量SV大于0,将它减一;如果SV值为0,则挂起该线程。V(SV): 如果有其他进程因为等待SV而挂起,则唤醒,然后将SV+1;否则直接将SV+1。其系统调用为:
|
13 | 13 |
|
14 |
| -- sem_wait(sem_t *sem):以原子操作的方式将信号量减1,如果信号量值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。 |
15 |
| -- sem_post(sem_t *sem):以原子操作将信号量值+1。当信号量大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。 |
| 14 | +- `sem_wait(sem_t *sem)`:以原子操作的方式将信号量减1,如果信号量值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。 |
| 15 | +- `sem_post(sem_t *sem)`:以原子操作将信号量值+1。当信号量大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。 |
16 | 16 |
|
17 | 17 | 互斥量:互斥量又称互斥锁,主要用于线程互斥,不能保证按序访问,可以和条件锁一起实现同步。当进入临界区 时,需要获得互斥锁并且加锁;当离开临界区时,需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程。其主要的系统调用如下:
|
18 | 18 |
|
19 |
| -- pthread_mutex_init: 初始化互斥锁 |
20 |
| -- pthread_mutex_destroy: 销毁互斥锁 |
| 19 | +- `pthread_mutex_init`: 初始化互斥锁 |
| 20 | +- `pthread_mutex_destroy`: 销毁互斥锁 |
21 | 21 | - pthread_mutex_lock: 以原子操作的方式给一个互斥锁加锁,如果目标互斥锁已经被上锁,pthread_mutex_lock调用将阻塞,直到该互斥锁的占有者将其解锁。
|
22 |
| -- pthread_mutex_unlock: 以一个原子操作的方式给一个互斥锁解锁。 |
| 22 | +- `pthread_mutex_unlock`: 以一个原子操作的方式给一个互斥锁解锁。 |
23 | 23 |
|
24 | 24 |
|
25 | 25 | 条件变量:条件变量,又称条件锁,用于在线程之间同步共享数据的值。条件变量提供一种线程间通信机制:当某个共享数据达到某个值时,唤醒等待这个共享数据的一个/多个线程。即,当某个共享变量等于某个值时,调用 signal/broadcast。此时操作共享变量时需要加锁。其主要的系统调用如下:
|
26 | 26 |
|
27 |
| -- pthread_cond_init: 初始化条件变量 |
28 |
| -- pthread_cond_destroy: 销毁条件变量 |
29 |
| -- pthread_cond_signal: 唤醒一个等待目标条件变量的线程。哪个线程被唤醒取决于调度策略和优先级。 |
30 |
| -- pthread_cond_wait: 等待目标条件变量。需要一个加锁的互斥锁确保操作的原子性。该函数中在进入wait状态前首先进行解锁,然后接收到信号后会再加锁,保证该线程对共享资源正确访问。 |
| 27 | +- `pthread_cond_init`: 初始化条件变量 |
| 28 | +- `pthread_cond_destroy`: 销毁条件变量 |
| 29 | +- `pthread_cond_signal`: 唤醒一个等待目标条件变量的线程。哪个线程被唤醒取决于调度策略和优先级。 |
| 30 | +- `pthread_cond_wait`: 等待目标条件变量。需要一个加锁的互斥锁确保操作的原子性。该函数中在进入wait状态前首先进行解锁,然后接收到信号后会再加锁,保证该线程对共享资源正确访问。 |
31 | 31 |
|
32 | 32 | 1. `**` 在Linux环境下,请用信号量实现哲学家就餐的多线程应用程序。
|
33 | 33 | 2. `**` 在Linux环境下,请用互斥锁和条件变量实现哲学家就餐的多线程应用程序。
|
|
118 | 118 | 编程作业
|
119 | 119 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
120 | 120 |
|
121 |
| -并发 |
| 121 | +银行家算法——分数更新 |
122 | 122 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
123 | 123 |
|
124 |
| -这一章我们实现了并发机制... |
| 124 | +.. note:: |
| 125 | + |
| 126 | + 本实验为用户态实验,请在 Linux 环境下完成。 |
| 127 | + |
| 128 | +背景:在智能体大赛平台 `Saiblo <https://www.saiblo.net>`_ 网站上每打完一场双人天梯比赛后需要用 ELO 算法更新双方比分。由于 Saiblo 的评测机并发性很高,且 ELO 算法中的分值变动与双方变动前的分数有关,因此更新比分前时必须先为两位选手加锁。 |
| 129 | + |
| 130 | +作业:请模拟一下上述分数更新过程,简便起见我们简化为有 p 位选手参赛(编号 [0, p) 或 [1, p] ),初始分值为 1000 分,有 m 个评测机线程(生产者)给出随机的评测结果(两位不同选手的编号以及胜负结果,结果可能为平局),有 n 个 worker 线程(消费者)获取结果队列并更新数据库(全局变量等共享数据)记录的分数。m 个评测机各自模拟 k 场对局结果后结束线程,全部对局比分更新完成后主线程打印每位选手最终成绩以及所有选手分数之和。 |
| 131 | + |
| 132 | +上述参数 p、m、n、k 均为可配置参数(命令行传参或程序启动时从stdin输入)。 |
| 133 | + |
| 134 | +简便起见不使用 ELO 算法,简化更新规则为:若不为平局,当 胜者分数 >= 败者分数 时胜者 +20,败者 -20,否则胜者 +30,败者 -30;若为平局,分高者 -10,分低者+10(若本就同分保持则不变)。 |
| 135 | + |
| 136 | +消费者核心部分可参考如下伪码: |
| 137 | + 获取选手A的锁 |
| 138 | + 获取选手B的锁 |
| 139 | + 更新A、B分数 |
| 140 | + 睡眠 1ms(模拟数据库更新延时) |
| 141 | + 释放选手B的锁 |
| 142 | + 释放选手A的锁 |
| 143 | + |
| 144 | +tips: |
| 145 | + - 由于 ELO 以及本题中给出的简化更新算法均为零和算法,因此出现冲突后可以从所有选手分数之和明显看出来,正确处理时它应该永远为 1000p |
| 146 | + - 将一个 worker 线程看作哲学家,将 worker 正在处理的一场对局的两位选手看作两根筷子,则得到了经典的哲学家就餐问题 |
| 147 | + |
| 148 | +实现 eventfd |
| 149 | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 150 | + |
| 151 | +在 Linux 中有一种用于事件通知的文件描述符,称为 eventfd 。其核心是一个 64 位无符号整数的计数器,在非信号量模式下,若计数器值不为零,则 `read` 函数会从中读出计数值并将其清零,否则读取失败; `write` 函数将缓冲区中的数值加入到计数器中。在信号量模式下,若计数器值非零,则 `read` 操作将计数值减一,并返回 1 ; `write` 将计数值加一。我们将实现一个新的系统调用: `sys_eventfd2` 。 |
| 152 | + |
| 153 | +**eventfd**: |
| 154 | + |
| 155 | + * syscall ID: 290 |
| 156 | + * 功能:创建一个 eventfd, `eventfd 标准接口 <https://linux.die.net/man/2/eventfd>`_ 。 |
| 157 | + * C 接口: ``int eventfd(unsigned int initval, int flags)`` |
| 158 | + * Rust 接口: ``fn eventfd(initval: u32, flags: i32) -> i32`` |
| 159 | + * 参数: |
| 160 | + * initval: 计数器的初值。 |
| 161 | + * flags: 可以设置为 0 或以下两个 flag 的任意组合(按位或): |
| 162 | + * EFD_SEMAPHORE (1) :设置该 flag 时,将以信号量模式创建 eventfd 。 |
| 163 | + * EFD_NONBLOCK (2048) :若设置该 flag ,对 eventfd 读写失败时会返回 -2 ,否则将阻塞等待直至读或写操作可执行为止。 |
| 164 | + * 说明: |
| 165 | + * 通过 `write` 写入 eventfd 时,缓冲区大小必须为 8 字节。 |
| 166 | + * 进程 `fork` 时,子进程会继承父进程创建的 eventfd ,且指向同一个计数器。 |
| 167 | + * 返回值:如果出现了错误则返回 -1,否则返回创建成功的 eventfd 编号。 |
| 168 | + * 可能的错误 |
| 169 | + * flag 不合法。 |
| 170 | + * 创建的文件描述符数量超过进程限制 |
| 171 | + |
| 172 | +.. note:: |
| 173 | + 还有一个 `sys_eventfd` 系统调用(调用号 284),与 `sys_eventfd2` 的区别在于前者不支持传入 flags 。 |
| 174 | + |
| 175 | + Linux 中的原生异步 IO 接口 libaio 就使用了 eventfd 作为内核完成 IO 操作之后通知应用程序的机制。 |
| 176 | + |
| 177 | + |
125 | 178 |
|
126 | 179 | 实验要求
|
127 | 180 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
128 | 181 |
|
| 182 | +- 完成分支: ch8-lab |
| 183 | +- 实验目录要求不变。 |
| 184 | +- 通过所有测例。 |
129 | 185 |
|
130 | 186 | 问答作业
|
131 | 187 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
132 | 188 |
|
| 189 | +无 |
133 | 190 |
|
134 | 191 | 实验练习的提交报告要求
|
135 | 192 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
0 commit comments