c - 为什么父级中的 printf() 几乎总是在 fork() 之后赢得竞争条件?
问题描述
有一个有点著名的 Unix 脑筋急转弯:写一个if
表达式,让下面的程序打印Hello, world!
在屏幕上。expr
in必须是合法的if
C 表达式并且不应包含其他程序结构。
if (expr)
printf("Hello, ");
else
printf("world!\n");
答案是fork()
。
当我年轻的时候,我只是笑了笑就忘记了。但是重新考虑它,我发现我无法理解为什么这个程序比它应该的更可靠。之后的执行顺序fork()
无法保证,并且存在竞争条件,但在实践中,您几乎总是会看到Hello, world!\n
, never world!\nHello,
。
为了证明这一点,我运行了该程序 100,000 轮。
for i in {0..100000}; do
./fork >> log
done
在 Linux 5.9 (Fedora 32, gcc 10.2.1, -O2
) 上,执行 100001 次后,孩子只赢了 146 次,父母赢的概率为 99.9985%。
$ uname -a
Linux openwork 5.9.14-1.qubes.x86_64 #1 SMP Tue Dec 15 17:29:47 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$ wc -l log
100001 log
$ grep ^world log | wc -l
146
结果在 FreeBSD 12.2 (clang 10.0.1, -O2
) 上是相似的。孩子只赢了 68 次,或 0.00067% 的时间,而父母赢了 99.993% 的所有处决。
一个有趣的旁注是ktrace ./fork
立即将主要结果更改为world\nHello,
(因为仅跟踪父项),证明了问题的 Heisenbug 性质。然而,通过跟踪这两个进程ktrace -i ./fork
会将行为恢复回来,因为这两个进程都被跟踪并且同样慢。
$ uname -a
FreeBSD freebsd 12.2-RELEASE-p1 FreeBSD 12.2-RELEASE-p1 GENERIC amd64
$ wc -l log
100001 log
$ grep ^world log | wc -l
68
独立于缓冲?
一个答案表明缓冲可以影响这种竞争条件的行为。\n
但是从 printf() 中删除后,该行为仍然存在。
if (expr)
printf("Hello");
else
printf("World");
stdbuf
并通过FreeBSD关闭标准输出的缓冲。
for i in {0..10000}; do
stdbuf -i0 -o0 -e0 ./fork >> log
echo > log
done
$ wc -l log
10001 log
$ grep -v "^HelloWorld" log | wc -l
30
为什么在亲本练习printf()
后几乎总是赢得比赛条件?是不是和C标准库中fork()
的内部实现细节有关?printf()
系统write()
调用?还是 Unix 内核中的进程调度?
解决方案
当fork
被执行时,执行它的进程(新的父进程)正在执行(当然),而新创建的子进程不是。要让子进程运行,要么必须停止父进程并为子进程分配处理器,要么必须在另一个处理器上启动子进程,这需要时间。同时,父进程继续执行。
除非发生一些不相关的事件,例如父级用尽了分配给它的共享处理器的时间片,否则它会赢得比赛。
推荐阅读
- javascript - dispatchEvent如何同步执行addEventListener的handler?
- c - Windows 服务无法访问 UNC 路径上的文件 (redux)
- powershell - 使用 Powershell 的 Sharepoint 签入文档
- c# - Tilemaps.SetTile 方法仅在 Update() 方法中部分工作
- python - 可以在使用点而不是文件顶部导入依赖项吗?
- css - CSS Calc 根据当前 z-index 值设置新的 z-index 值
- node.js - MongoDB 重新计算计算数据的最佳实践
- javascript - Tone JS - Transport.stop(); 不适用于预定事件
- python - 对于不和谐的python,如何每x秒打印一条带有任务的消息?
- c++ - 我们如何在 CS50 ide 上编译和运行程序?