为什么使用不同进程并发写入文件会产生奇怪的结果?

Dav*_*mar 1 ruby csv parallel-processing process

我正在尝试使用 Ruby 来理解流程。我正在从我的父进程创建 4 个子进程。主进程首先写入文件,然后创建子进程,每个子进程都写入同一个文件:

require 'csv'
a = [1, 2, 3, 4]
CSV.open("temp_and_cases_batch_parallel.csv", "ab") do |target_file|
  target_file << ["hello from parent process #{Process.pid}"]
  a.each do |num|
    pid = Process.fork do
      target_file << ["hello from child Process #{Process.pid}"]
    end
    puts "parent, pid #{Process.pid}, waiting on child pid #{pid}"
  end
end
Process.wait
puts "parent exiting"
Run Code Online (Sandbox Code Playgroud)

我期望的文件输出

hello from parent process 3336
hello from child Process 3350
hello from child Process 3351
hello from child Process 3349
hello from child Process 3352
Run Code Online (Sandbox Code Playgroud)

我实际得到的文件输出:

hello from parent process 3336
hello from parent process 3336
hello from child Process 3350
hello from parent process 3336
hello from child Process 3351
hello from parent process 3336
hello from child Process 3349
hello from parent process 3336
hello from child Process 3352
Run Code Online (Sandbox Code Playgroud)

似乎来自父进程的插入重新运行了 5 次。这怎么可能?这里发生了什么 ?

Cas*_*per 5

让多个进程写入同一个文件通常不是一个好主意。在大多数情况下,除非您完全知道自己在做什么,否则结果将是不可预测的,正如您刚刚通过示例演示的那样。

你得到奇怪结果的原因是 Ruby IO 对象有它自己的内部缓冲区。此缓冲区保存在内存中,并不能保证在您调用时写入磁盘<<

这里发生的是字符串hello from parent只写入内部缓冲区,而不是写入磁盘。然后,当您调用 时fork,您将将此缓冲区复制到子进程中。然后子进程将附加hello from child到缓冲区,只有然后缓冲区才会刷新到磁盘。

结果是hello from parent除了write之外,所有孩子都会 write ,hello from child因为这是 Ruby 决定将缓冲区写入磁盘时内部内存缓冲区将包含的内容。

要解决此问题,您可以IO.flush在分叉之前调用,以确保内存缓冲区为空并在分叉之前刷新到磁盘。这可确保子进程中的缓冲区为空,您现在将获得预期的输出:

CSV.open(...) do |target_file|
  target_file << ...
  target_file.flush  # <-- Make sure the internal buffer is flushed to disk before forking

  a.each do |num|
    ... Process.fork ...
  end
end
...
Run Code Online (Sandbox Code Playgroud)