缺少的两种转换分别是:
先说就绪 → 阻塞。因为就绪状态下的进程尚未获得CPU,所以无法发起读盘等可引发阻塞的操作。
再说阻塞 → 运行。阻塞进程转换为运行需要两种资源同时就位:
我认为理论上这是可实现的,比如外部事件发生时CPU也是空闲的。
在借助中断进行进程切换的实现中,两个进程的所有信息都借助进程表进行周转。在这种新的计算机体系中,不妨仍然使用进程表。
当一个进程从运行态转为就绪态或阻塞态时,将对应表项的指针传递给硬件,由硬件将当前的进程信息写入进程表表项。
当一个进程从就绪态转换为运行态时,也是通过该指针,从对应的表项恢复进程运行的所有信息。
因为 C语言等高级语言未提供操纵寄存器的函数,而处理中断时又需要保存寄存器的值,设置程序计数器,设置新的堆栈等操作。因此这一部分需要由汇编语言编写。
这样中断处理程序或系统调用的堆栈可完全由内核控制和保护。可以规避如下情形的发生:
假设这些进程进入阻塞状态的时间点是随机的,那么浪费比例为:
1
2
5
=
1
32
\frac{1}{2}^5 = \frac{1}{32}
215=321
假设进程的 I/O 等待时间与停留在内存中时间比例为
P
P
P,则有
1
−
P
4
G
B
−
512
M
B
256
M
B
=
0.99
1
−
P
14
=
0.99
P
14
=
0.01
P
≈
0.72
\begin{aligned} 1-P^{\frac{4GB-512MB}{256MB}} &= 0.99 \\ 1-P^{14} &= 0.99 \\ P^{14} &= 0.01 \\ P &≈ 0.72 \end{aligned}
1−P256MB4GB−512MB1−P14P14P=0.99=0.99=0.01≈0.72
易得,串行执行时间为 40 + 40 = 80 m i n 40+40=80\ min 40+40=80 min。
假设并行执行的期望时间为 n n n 分钟,那在这 n n n 分钟内,两个进程 A 和 B 分别会有以下状态:
又有,CPU 利用率
u
s
e
=
1
−
1
2
2
=
0.75
use = 1 - \frac{1}{2}^2 = 0.75
use=1−212=0.75
因此有
r
u
n
A
+
r
u
n
B
n
=
0.75
40
n
=
0.75
n
≈
53.33
m
i
n
\begin{aligned} \frac{run_A + run_B}{n} &= 0.75 \\ \frac{40}{n} &= 0.75 \\ n &≈ 53.33\ min\\ \end{aligned}
nrunA+runBn40n=0.75=0.75≈53.33 min
u s e = 1 − 0. 4 6 ≈ 0.996 use = 1-0.4^6 ≈ 0.996 use=1−0.46≈0.996
可以编写一个这样的的下载工具:
涉及读写一致的场景,在进程之间同步没有线程来的方便,而且也不能有自己的缓存。
不会。单线程进程无法同时做两件事:当它 fork 时一定没有因等待键盘而阻塞,当它因等待键盘而阻塞时也一定没有在 fork。
内核级线程。使用内核级线程时,内核可以感知到有那些线程处于阻塞态以及那些线程处于就绪态,因此可以进行调度。反之,使用用户级线程,内核只能感知到一个线程存在,无法进行调度。
本章中,给出的三种实现如下:
在单核机器上执行纯计算任务,显然单线程模式更好。多线程不会执行的更快,只会增加编码的难度。
寄存器总是和堆栈一起工作的。因此当线程在运行态和非运行态之间切换时,寄存器应该和栈等其他数据一起恢复或保存。
在一个进程中的线程应该是互相协作的关系,一个线程可以通过该方式将时间片让渡给其他线程。
尤其是在用户级线程中,线程不会因时间中断等将CPU让渡给其他线程,此时 thread_yield 就显得很有必要了。
用户级线程:当进程的时间片用完时会发生抢占,但内核无法随意调度进程中的线程,因此在下一个时间片,仍然是前一个线程继续运行,除非它自己将 CPU 让渡给其他线程。
内核级线程:当进程的时间片用完时会发生抢占,内核也可以随意调度进程中的任意线程使用CPU。
有
1
3
\frac{1}{3}
31 的请求,需耗时
75
+
12
=
87
m
s
75+12 = 87ms
75+12=87ms。
有
2
3
\frac{2}{3}
32 的请求,需耗时
12
m
s
12ms
12ms。
请求的平均耗时为
87
∗
1
3
+
12
∗
2
3
=
27
m
s
87*\frac{1}{3} + 12*\frac{2}{3} = 27ms
87∗31+12∗32=27ms。
因此,对于单线程服务器: Q P S = 1000 27 ≈ 37.03 请求 / 秒 QPS=\frac{1000}{27} ≈ 37.03\ 请求/秒 QPS=271000≈37.03 请求/秒。
多线程服务的QPS取决于线程数量和CPU核数,题目中并未给出,我们不妨假设线程数远高于请求的并发数,且只有一个核的CPU。
在此基础上,CPU一直在处理请求,因此 Q P S = 1000 12 ≈ 83.33 请求 / 秒 QPS=\frac{1000}{12}≈83.33\ 请求/秒 QPS=121000≈83.33 请求/秒。
优点:线程切换不涉及系统调用,效率更高。
缺点:一个线程被阻塞,所有线程均被阻塞。
可以借助 pthread_join 函数等待线程退出。完整代码如下:
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
using namespace std;
#define NUMBER_OF_THREADS 10
void *print_hello_world(void *tid) {
printf("Hello World. Greeting from thread %d\n", tid);
pthread_exit(NULL);
}
int main() {
for (int i = 0; i < NUMBER_OF_THREADS; i++) {
printf("Main here. Creating thread %d\n", i);
pthread_t thread;
// 创建线程
int status = pthread_create(&thread, NULL, print_hello_world, (void*)i);
if (status != 0) {
printf("Oops. pthread_create returned error code %d\n", status);
exit(-1);
}
// 等待线程退出,可能会阻塞调用线程。
pthread_join(thread, NULL);
}
return 0;
}
可使用如下命令进行编译:
gcc -o main main.cc -pthread
create_global 出现在 2.2.9 小节,用于创建线程级别的全局变量,通常和 set_global 以及 get_global 一起使用。
我认为存储变量自身也可行。
可将 create_global 的接口改造为:
int create_global(const char *name, int size);
create_global 会创建可存储 size 个字节的内存片段,并与 name 绑定。
get_global 改造为:
int get_global(const char *name, void **ptr);
使用 get_global 获取与 name 绑定的内存地址,将该地址写入 ptr 指向的指针。
废弃 set_global 接口。
原书答案认为是不可行的,该答案认为变量的 size 和 type 难以处理,答案原文如下:
The pointers are really necessary because the size of the global variable is unknown. It could be anything from a character to an array of floating-point numbers. If the value were stored, one would have to give the size to create_global, which is all right, but what type should the second parameter of
set_global be, and what type should the value of read_global be?
啥是运行时系统?一个管理线程的函数的集合,比如pthread_create,phtread_join 等等。
运行时系统在收到时钟中断信号后,可能执行的一种操作是进行线程状态切换:
假设这样一种场景,当前线程正在执行 thread_yield,正在将自己的状态修改为就绪状态。此时收到了时钟中断信号,中断处理程序也会修改当前线程的状态。这可能会导致数据不一致。如果这段代码有互斥量保护,还可能导致死锁。
可以通过缓存信号的方式解决上述冲突。
比如,新增一个标记位,和一个中断缓存队列。
当线程调用运行时系统中的某个函数时,先设置标记位,执行完成后,复位标记位,并处理中断缓存队列。
当中断到达时,先检查标记位是否被设置,若被设置则放入缓存队列,反之则立即被处理。
从而保证,中断不会丢失,运行时系统中的函数也不会被并发执行。
要实现一个用户级的线程包,就需要保证线程在被阻塞时,可以将CPU交给其他线程使用。
为了实现上述目标,在执行可能阻塞的系统调用前,先设置一个定时器。
如果在定时器触发前,阻塞仍未解决,则通过定时器的中断机制将CPU的控制权交由线程包重新分配。
如果在定时器出发前,系统调用完成了,那就取消定时器。
定时器的设置与取消,中断机制的执行,线程的阻塞状态的转换。三者应该保证原子性,这虽然比较麻烦,但我觉得可以实现。
turn 变量的忙等待解决方案还有效吗?仍然有效。turn 的修改仍然是原子的,因此对 turn 的赋值和检测与单处理器没有区别,所以仍然有效。
非抢占式:挑选一个进程,并让其开始运行直到被阻塞,或者自动放弃CPU。
不可行。
假设有两个进程 A 和 B,A 先进入了临界区,在临界区内进入了阻塞状态。此时 B 进入了运行状态,并一直忙等待,由于 B 不会被抢占,因此 A 也无法离开临界区,从而导致 A 和 B 互相等待,都无法正常执行下去。
抢占式:挑选一个进程,并让其开始运行,但为其运行时间设置上限。如果在运行时间达到上限时仍在运行,则将其挂起。
可行。
优先级反转问题是指,进入临界区的低优线程因无法获得CPU使用权导致自身无法退出临界区,进而导致高优线程一直忙等待的现象。
假设这样一种场景,在临界区内进行系统调用导致低优线程阻塞,而后另一个线程开始执行并忙等待。这种场景也会造成两个线程都无法正常运行的问题。
原书的英文答案认为不会发生,原文如下:
The priority inversion problem occurs when a low-priority process is in its critical region and suddenly a high-priority process becomes ready and is scheduled. If it uses busy waiting, it will run forever. With user-level threads, it cannot happen that a low-priority thread is suddenly preempted to allow a high-priority thread run. There is no preemption. With kernel-level threads this problem can arise.
不会发生。L 总会获得CPU使用权,从而退出临界区。这解除了 H 和 L 对 「CPU使用权和临界区」的循环等待。
无论是用户级还是内核级,每个线程都需要一段内存来存储那些被调用但还未返回的函数的数据。这段内存就是栈。
Yes. The simulated computer could be multiprogrammed. For example, while process A is running, it reads out some shared variable. Then a simulated clock tick happens and process B runs. It also reads out the same variable. Then it adds 1 to the variable. When process A runs, if it also adds 1 to the variable, we have a race condition.
可行,因为 down 和 up 是原子的,这仍然保证了在任意时间点,只有一个线程在临界区内。
/* other code */
while (turn != 0) {} /* Do nothing and wait */
Critical Section /* ... */
turn = 0;
/* Other Code */
P1 的代码是将上述代码中的 0 替换为 1。该方法是否能处理互斥问题中所有可能的情形?
感觉这题写错了。
turn 的初始值为 0,因此 P0 先执行,P0执行完 turn 还是 0,P1 根本不能进入临界区。
感觉把代码改成下面这样才是对的。
P0 的代码:
/* other code */
while (turn != 0) {} /* Do nothing and wait */
Critical Section /* ... */
turn = 1;
/* Other Code */
P1 的代码:
```c
/* other code */
while (turn != 1) {} /* Do nothing and wait */
Critical Section /* ... */
turn = 0;
/* Other Code */
这样 P0 和 P1 就会交替进入临界区,这样是比较可行的。
To do a semaphore operation, the operating system first disables interrupts. Then it reads the value of the semaphore. If it is doing a down and the semaphore is equal to zero, it puts the calling process on a list of blocked processes associated with the semaphore. If it is doing an up, it must check to see if any processes are blocked on the semaphore. If one or more processes are blocked, one of them is removed from the list of blocked processes and made runnable. When all these operations have been completed, interrupts can be enabled again.
二元信号量的定义(74页):
用 std::mutex 和 std::condition_variable 实现信号量 Semaphore:
#include <mutex>
#include <condition_variable>
class BinarySemaphore {
// 互斥量,保护数据
std::mutex mutex;
// 条件变量,用于阻塞线程
std::condition_variable cv;
int64_t value = 1;
public:
void Up() {
{
std::unique_lock<std::mutex> lock(mutex);
++value;
}
if (value > 0) {
cv.notify_one();
}
}
void Down() {
std::unique_lock<std::mutex> lock(mutex);
while (value == 0) {
cv.wait(lock, [this]() -> bool { return value != 0; });
}
--value;
}
};
上述实现是符合信号量的标准的:
down 操作:将 value 减一,当且仅当 value 为 0 时调用线程进入睡眠。up 操作:将 value 加一,而且绝对不会阻塞调用线程。mutex 保证了 down 和 up 的原子性。接下来,使用两个二元信号量和两个整型变量实现题目要求的计数信号量。
int64_t value,计数字段。int64_t blocking,有多少个线程阻塞在 block 上面。BinarySemaphore mutex :保护 value 和 block,在每个时刻最多只有一个线程读写。BinarySemaphore block:当 value 为 0 时,阻塞调用线程。class Semaphore {
BinarySemaphore mutex;
BinarySemaphore block;
int64_t value = 0;
int64_t blocking = 0;
public:
Semaphore() {
// 因为 BinarySemaphore::value 的初始值是 0,
// 所以先执行一次 mutex.Up,表示没有线程持有该锁。
mutex.Up();
}
~Semaphore() {
std::cout << "value: " << value << ", blocking: " << blocking << std::endl;
}
void Down() {
mutex.Down();
if (value) {
value--;
mutex.Up();
} else {
blocking++;
mutex.Up();
block.Down();
}
}
void Up() {
mutex.Down();
if (blocking) {
blocking--;
mutex.Up();
block.Up();
} else {
value++;
mutex.Up();
}
}
};
修改 value 的定义为 value-blocking,从而省略掉 blocking 字段。新的实现如下:
class Semaphore {
BinarySemaphore mutex;
BinarySemaphore block;
int64_t value = 0;
public:
Semaphore() {
// 先 Down 一次,把初始值消耗掉
block.Down();
}
~Semaphore() {
std::cout << "value: " << value << std::endl;
}
int64_t Value() const {
return value;
}
void Down() {
mutex.Down();
if (value > 0) {
value--;
mutex.Up();
} else {
value--;
mutex.Up();
block.Down();
}
}
void Up() {
mutex.Down();
if (value < 0) {
value++;
block.Up();
mutex.Up();
} else {
value++;
mutex.Up();
}
}
};
屏障通常用于同步一组进程的多阶段的动作。当这一组进程都完成特定阶段的动作后,才会一起进入下一阶段。
因此能否使用屏障和线程数量没有关系,只要进程的工作有明显的阶段性,就很适合使用屏障。
wait 和 signal。一种更通用的同步方式是只用一条原语 waituntil,它以任意的布尔谓词作为参数。例如 waitutil x < 0 or y+z < n,这样就不需要 signal 原语了。这种方法为何从未被采用过。为什么?试想系统应以何种机制来检查布尔谓词是否为真呢?
int 类型的变量设置回调呢。signal 原语了。还有其他方法吗?暂时没想到。
综合来看,signal 原语的方案是最可用的。其他两种都不太具备实操性。
进程间的通信方式有哪些:信号量,互斥量,管程,消息传递,屏障。
基于消息传递可以很好的实现该场景。实现了一个简易 demo,放到了 github。
竞态条件的定义:两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,成为竞态条件。(67页)
显然这种忙等待的方式不会触发竞态条件。
单个进程需要使用 T 秒的 CPU,且进程之间切换无额外耗时。因此总共需要 n T nT nT 秒。
void main() {
fork();
fork();
exit();
}
设有进程号为 100 的主进程开始执行 main 函数。
执行完第一个 fork 后,会产出一个新的进程,设其进程号为 201。
接着,100 和 201 均执行到了第二个 fork,分别产生一个新进程,设进程号分别为 301 和 202。
因此总共产生了三个子进程,四个进程的关系如下所示:
100
|___ 201
| |___202
|___ 301
相当于对该进程提权了,可以分到更多的时间片。
通过源代码分析可能不太准确吧,比较还有启动参数,输入输出的数据规模等影响因素。
通过进程的运行时长和使用的CPU时间作比较,可以判断其类型。
为了保证CPU的利用率,时间片长度总是上下文切换时间的若干倍。
CPU 利用率就是进程使用CPU的时间除以CPU总时间,即
进程使用
C
P
U
的时间
进程使用
C
P
U
的时间
+
进程切换使用的时间
\frac{进程使用CPU的时间}{进程使用CPU的时间 + 进程切换使用的时间}
进程使用CPU的时间+进程切换使用的时间进程使用CPU的时间
当
Q
=
∞
Q = ∞
Q=∞ 或者
Q
>
T
Q \gt T
Q>T 时,显然每隔时间 T 则发生一次切换,因此有
u
t
i
l
i
z
a
t
i
o
n
=
T
T
+
S
utilization = \frac{T}{T+S}
utilization=T+ST
当 Q < T Q \lt T Q<T 时,则进程每运行时间 T,则会发生 T Q \frac{T}{Q} QT 次切换,因此有
u t i l i z a t i o n = T T Q S + T = Q Q + S utilization = \frac{T}{\frac{T}{Q}S + T} = \frac{Q}{Q+S} utilization=QTS+TT=Q+SQ
同上,当
Q
=
S
Q = S
Q=S 时有
u
t
i
l
i
z
a
t
i
o
n
=
T
T
Q
S
+
T
=
1
2
utilization = \frac{T}{\frac{T}{Q}S + T}=\frac{1}{2}
utilization=QTS+TT=21
当 Q Q Q 趋近于 0 0 0,每运行时间 T,需要无穷多切换次数,显然利用率趋近于 0。
响应时间:作业从提交到完成的时间间隔。
假设五个作业同时提交,则采用最短作业优先的策略可保证最短的平均响应时间。
0
<
X
≤
3
:
X
,
3
,
5
,
6
,
9
3
<
X
≤
5
:
3
,
X
,
5
,
6
,
9
5
<
X
≤
6
:
3
,
5
,
X
,
6
,
9
6
<
X
≤
9
:
3
,
5
,
6
,
X
,
9
9
<
X
:
3
,
5
,
6
,
9
,
X
.
\begin{array}{c} 0 \lt X \le 3&:X,3,5,6,9 \\ 3 \lt X \le 5&:3,X,5,6,9 \\ 5 \lt X \le 6&:3,5,X,6,9 \\ 6 \lt X \le 9&:3,5,6,X,9 \\ 9 \lt X&:3,5,6,9,X\\ \end{array}.
0<X≤33<X≤55<X≤66<X≤99<X:X,3,5,6,9:3,X,5,6,9:3,5,X,6,9:3,5,6,X,9:3,5,6,9,X.
a. 轮转法: 10 + 18 + 24 + 28 + 30 5 = 22.0 m i n u t e s \frac{10+18+24+28+30}{5} = 22.0 minutes 510+18+24+28+30=22.0minutes
初始有五个任务使用轮转CPU,因此 C 完成于
2
/
1
5
=
10
2/\frac{1}{5} = 10
2/51=10 分钟。
然后有四个任务使用轮转CPU,因此 D 完成于
(
4
−
2
)
/
1
4
+
10
=
18
(4-2)/\frac{1}{4} + 10 = 18
(4−2)/41+10=18 分钟。
然后有三个任务使用轮转CPU,因此 B 完成于
(
6
−
4
)
/
1
3
+
18
=
24
(6-4)/\frac{1}{3} + 18 = 24
(6−4)/31+18=24 分钟。
然后有两个任务使用轮转CPU,因此 E 完成于
(
8
−
6
)
/
1
2
+
24
=
28
(8-6)/\frac{1}{2} + 24 = 28
(8−6)/21+24=28 分钟。
最后有一个任务使用轮转CPU,因此 A 完成于
(
8
−
6
)
/
1
1
+
28
=
30
(8-6)/\frac{1}{1} + 28 = 30
(8−6)/11+28=30 分钟。
b. 优先级调度: 6 + 14 + 24 + 26 + 30 5 = 20.0 m i n u t e s \frac{6+14+24+26+30}{5} = 20.0 minutes 56+14+24+26+30=20.0minutes
c. FIFO (按照10、6、2、4、8次序运行): 10 + 16 + 18 + 22 + 30 5 = 19.2 m i n u t e s \frac{10+16+18+22+30}{5} = 19.2 minutes 510+16+18+22+30=19.2minutes
d. 最短优先作业: 2 + 6 + 12 + 20 + 30 5 = 14.0 m i n u t e s \frac{2+6+12+20+30}{5} = 14.0minutes 52+6+12+20+30=14.0minutes
第 1 次调度,运行 1 时间片,还需 29 个。
第 2 次调度,运行 2 时间片,还需 27 个。
第 3 次调度,运行 4 时间片,还需 23 个。
第 4 次调度,运行 8 时间片,还需 15 个。
第 5 次调度,运行 15 时间片,还需 0 个。
综上,总共调入 5 次。
92页给出的是否可调度的衡量标准是:若有 m m m 个事件,事件 i i i 以周期 P i P_i Pi 发生,并需要 C i C_i Ci 秒的 CPU 时间,若满足 ∑ i = 1 m C i P i ≤ 1 \sum_{i=1}^{m}\frac{C_i}{P_i} \le 1 ∑i=1mPiCi≤1 则可调度,反之则不可。
这个式子可以理解为:对于任意一类事件,在一个周期内,总是能分配到足够的CPU时间。
根据上述式子,可得出:
1
5
+
1
5
+
11
33
≈
0.733
≤
1
\frac{1}{5} + \frac{1}{5} + \frac{11}{33} ≈ 0.733 \le 1
51+51+3311≈0.733≤1
因此,该系统可以调度。
1 5 + 1 5 + 11 33 + 11 33 ≈ 1.067 > 1 \frac{1}{5} + \frac{1}{5} + \frac{11}{33} + \frac{11}{33} ≈ 1.067 \gt 1 51+51+3311+3311≈1.067>1
因此,再加入一个视频流后,系统不可调度。
T = 15 ∗ 1 2 + 40 ∗ 1 4 + 20 ∗ 1 8 + 40 ∗ 1 8 = 25.0 m s T = 15*\frac{1}{2} + 40*\frac{1}{4} + 20 * \frac{1}{8} + 40 *\frac{1}{8} = 25.0ms T=15∗21+40∗41+20∗81+40∗81=25.0ms
根据可调度性估算公式 ∑ i = 1 m C i P i ≤ 1 \sum_{i=1}^{m}\frac{C_i}{P_i} \le 1 ∑i=1mPiCi≤1 ,可得
35 100 + 20 100 + 10 200 + x 250 = 1 x = ( 1 − 35 100 − 20 100 − 10 200 ) ∗ 500 x = 200 m s \begin{aligned} \frac{35}{100} + \frac{20}{100} + \frac{10}{200} + \frac{x}{250} &= 1\\ x &= (1 - \frac{35}{100} - \frac{20}{100} - \frac{10}{200})*500 \\ x &= 200 ms \end{aligned} 10035+10020+20010+250xxx=1=(1−10035−10020−20010)∗500=200ms
因此,x 的上限为 200ms。
死锁的四个条件:互斥条件、占有和等待条件、不可抢占条件、环路等待条件。
不难发现,上述策略不存在环路等待条件。因此可以避免。
1 6 + 1 6 + 20 1000 25 = 5 6 ≤ 1 \frac{1}{6} + \frac{1}{6} + \frac{20}{\frac{1000}{25}} = \frac{5}{6} \le 1 61+61+25100020=65≤1
因此是可调度的。
内核提供 n 个就绪队列,编号从 1 到 n。编号为 i 的队列最多可以连续获得 i 个时间片,队列内的线程FIFO的获得时间片。
用户通过指定线程进入哪个队列来控制其优先级。
首先,中文第四版省略了一段代码,在英文版原书中,philosopher 函数的实现如下:
void philosopher(int i) { // i: philosopher number, from 0 to N-1
where (TRUE) { // repeat forever
think(); // philosopher is thinking
take_forks(i); // acquire two forks or block
eat(); // yum-yum, spaghetti
put_forks(i); // put both forks back on table
}
}
置为 HUNGRY,是为了配合相邻的哲学家。当相邻哲学家放下叉子时,可根据该状态唤醒自己。
导致 test 时无法唤醒被挂起的相邻的哲学家。
Variation 1: readers have priority. No writer may start when a reader is active. When a new reader appears, it may start immediately unless a writer is currently active. When a writer finishes, if readers are waiting, they are all started, regardless of the presence of waiting writers.
Variation 2: Writers have priority. No reader may start when a writer is waiting. When the last active process finishes, a writer is started, if there is one; otherwise, all the readers (if any) are started.
Variation 3: symmetric version. When a reader is active, new readers may start immediately. When a writer finishes, a new writer has priority, if one is waiting. In other words, once we have started reading, we keep reading until there are no readers left. Similarly, once we have started writing, all pending writers are allowed to run.
个人感觉第三种做法并不妥当。在该做自称是 symmetric ,但仍可能会导致一种操作一直饥饿。比如 Reader 先来了,且在 Reader 结束前一直有Reader 到来,则 Writer 永远不会被执行。
应该改成 FIFO 比较合适,先来的先被执行,如果和当前的操作互斥,则在当前操作结束后再执行。
无锁版本
# 第一个参数是文件名
# 向文件末尾追加一百个数字
repeat=0
while True
do
last=`tail -n 1 $1 2>&1`;
if [[ -z `echo $last | grep "^[0-9][0-9]*$"` ]]; then
last=0;
fi
if [ $repeat -ge 100 ]; then
break;
fi
repeat=`expr ${repeat} + 1`;
next=`expr ${last} + 1`;
echo $next >> $1;
done
临界区包含三个步骤:
有锁版本。一个并不鲁棒的锁——借助mkdir的结果判断是否获得锁:
# 第一个参数是文件名
# 向文件末尾追加一百个数字
repeat=0
while True
do
last=`tail -n 1 $1 2>&1`;
if [[ -z `echo $last | grep "^[0-9][0-9]*$"` ]]; then
last=0;
fi
if [ $repeat -ge 100 ]; then
break;
fi
repeat=`expr ${repeat} + 1`;
next=`expr ${last} + 1`;
echo $next >> $1;
done
可以参见
02-message-passing.h
02.problem.36.h
现基于 Semaphore 实现哲学家就餐问题。见 02-philosopher-semaphore.h
按照定义,它应该具有以下几个特点:
管程代码见 02-monitor.h
再基于 Monitor 实现一遍。见 02.problem.59.h
代码见 02.problem.60.h
C/C++ 中对 build-in 类型的变量赋值是原子的,利用这一点实现严格轮转。代码见 02.problem.61.h
电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。 准备工作: 1、U盘一个(尽量使用8G以上的U盘)。 2、一台正常联网可使用的电脑。 3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。 4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。 U盘启动盘制作步骤: 注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc
我有一个使用SeleniumWebdriver和Nokogiri的Ruby应用程序。我想选择一个类,然后对于那个类对应的每个div,我想根据div的内容执行一个Action。例如,我正在解析以下页面:https://www.google.com/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=puppies这是一个搜索结果页面,我正在寻找描述中包含“Adoption”一词的第一个结果。因此机器人应该寻找带有className:"result"的div,对于每个检查它的.descriptiondiv是否包含单词“adoption
我正在我的Rails项目中安装Grape以构建RESTfulAPI。现在一些端点的操作需要身份验证,而另一些则不需要身份验证。例如,我有users端点,看起来像这样:moduleBackendmoduleV1classUsers现在如您所见,除了password/forget之外的所有操作都需要用户登录/验证。创建一个新的端点也没有意义,比如passwords并且只是删除password/forget从逻辑上讲,这个端点应该与用户资源。问题是Grapebefore过滤器没有像except,only这样的选项,我可以在其中说对某些操作应用过滤器。您通常如何干净利落地处理这种情况?
在我做的一些网络开发中,我有多个操作开始,比如对外部API的GET请求,我希望它们同时开始,因为一个不依赖另一个的结果。我希望事情能够在后台运行。我找到了concurrent-rubylibrary这似乎运作良好。通过将其混合到您创建的类中,该类的方法具有在后台线程上运行的异步版本。这导致我编写如下代码,其中FirstAsyncWorker和SecondAsyncWorker是我编写的类,我在其中混合了Concurrent::Async模块,并编写了一个名为“work”的方法来发送HTTP请求:defindexop1_result=FirstAsyncWorker.new.async.
因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实
a=[3,4,7,8,3]b=[5,3,6,8,3]假设数组长度相同,是否有办法使用each或其他一些惯用方法从两个数组的每个元素中获取结果?不使用计数器?例如获取每个元素的乘积:[15,12,42,64,9](0..a.count-1).eachdo|i|太丑了...ruby1.9.3 最佳答案 使用Array.zip怎么样?:>>a=[3,4,7,8,3]=>[3,4,7,8,3]>>b=[5,3,6,8,3]=>[5,3,6,8,3]>>c=[]=>[]>>a.zip(b)do|i,j|c[[3,5],[4,3],[7,6],
我有一个非常简单的Controller来管理我的Rails应用程序中的静态页面:classPagesController我怎样才能让View模板返回它自己的名字,这样我就可以做这样的事情:#pricing.html.erb#-->"Pricing"感谢您的帮助。 最佳答案 4.3RoutingParametersTheparamshashwillalwayscontainthe:controllerand:actionkeys,butyoushouldusethemethodscontroller_nameandaction_nam
有没有办法跳过CSV文件的第一行,让第二行作为标题?我有一个CSV文件,第一行是日期,第二行是标题,所以我需要能够在遍历它时跳过第一行。我尝试使用slice但它会将CSV转换为数组,我真的很想将其读取为CSV,以便我可以利用header。 最佳答案 根据您的数据,您可以使用另一种方法和skip_lines-option此示例跳过所有以#开头的行require'csv'CSV.parse(DATA.read,:col_sep=>';',:headers=>true,:skip_lines=>/^#/#Markcomments!)do|