ruby中如何锁定fork共享的IO

发布于 2024-08-02 08:11:51 字数 1206 浏览 1 评论 0原文

我们如何锁定多个 ruby​​ 进程共享的 IO?

考虑这个脚本:

#!/usr/bin/ruby -w
# vim: ts=2 sw=2 et
if ARGV.length != 2
  $stderr.puts "Usage: test-io-fork.rb num_child num_iteration"
  exit 1
end
CHILD = ARGV[0].to_i
ITERATION = ARGV[1].to_i

def now
  t = Time.now
  "#{t.strftime('%H:%M:%S')}.#{t.usec}"
end

MAP = %w(nol satu dua tiga empat lima enam tujuh delapan sembilan)

IO.popen('-', 'w') {|pipe|
  unless pipe
    # Logger child
    File.open('test-io-fork.log', 'w') {|log|
      log.puts "#{now} Program start"
      $stdin.each {|line|
        log.puts "#{now} #{line}"
      }
      log.puts "#{now} Program end"
    }
    exit!
  end
  pipe.sync = true
  pipe.puts "Before fork"
  CHILD.times {|c|
    fork {
      pid = Process.pid
      srand
      ITERATION.times {|i|
        n = rand(9)
        sleep(n / 100000.0)
        pipe.puts "##{c}:#{i} #{MAP[n]} => #{n}, #{n} => #{MAP[n]} ##{c}:#{i}"
      }
    }
  }

}

并像这样尝试:

./test-io-fork.rb 200 50

正如预期的那样,test-io-fork.log 文件将包含 IO 竞争条件的标志。

我想要实现的是为自定义 GPS 协议创建一个 TCP 服务器,将 GPS 点保存到数据库中。因为该服务器将处理 1000 个并发客户端,所以我想将数据库连接限制为仅一个子级,而不是同时打开 1000 个数据库连接。该服务器将在 Linux 上运行。

How can we lock an IO that has been shared by multiple ruby process?

Consider this script:

#!/usr/bin/ruby -w
# vim: ts=2 sw=2 et
if ARGV.length != 2
  $stderr.puts "Usage: test-io-fork.rb num_child num_iteration"
  exit 1
end
CHILD = ARGV[0].to_i
ITERATION = ARGV[1].to_i

def now
  t = Time.now
  "#{t.strftime('%H:%M:%S')}.#{t.usec}"
end

MAP = %w(nol satu dua tiga empat lima enam tujuh delapan sembilan)

IO.popen('-', 'w') {|pipe|
  unless pipe
    # Logger child
    File.open('test-io-fork.log', 'w') {|log|
      log.puts "#{now} Program start"
      $stdin.each {|line|
        log.puts "#{now} #{line}"
      }
      log.puts "#{now} Program end"
    }
    exit!
  end
  pipe.sync = true
  pipe.puts "Before fork"
  CHILD.times {|c|
    fork {
      pid = Process.pid
      srand
      ITERATION.times {|i|
        n = rand(9)
        sleep(n / 100000.0)
        pipe.puts "##{c}:#{i} #{MAP[n]} => #{n}, #{n} => #{MAP[n]} ##{c}:#{i}"
      }
    }
  }

}

And try it like this:

./test-io-fork.rb 200 50

Like expected, the test-io-fork.log files would contains sign of IO race condition.

What I want to achieve is to make a TCP server for custom GPS protocol that will save the GPS points to database. Because this server would handle 1000 concurrent clients, I would like to restrict database connection to only one child instead opening 1000 database connection simultaneously. This server would run on linux.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

眼眸里的快感 2024-08-09 08:11:51

更新

在接受答案后进行更新可能是不好的形式,但原文有点误导。 ruby 是否对自动附加的换行符进行单独的 write(2) 调用取决于输出 IO 对象的缓冲状态。

$stdout (当连接到 tty 时)通常是行缓冲的,因此 puts() 的效果——给定合理大小的字符串——隐式添加换行符是对 write(2) 的一次调用。然而,正如 OP 发现的那样,IO.pipe$stderr 并非如此。

原始答案

将您的主要pipe.puts()参数更改为换行符字符串:

pipe.puts "##{c} ... #{i}\n"  # <-- note the newline

为什么?您设置 pipe.sync 希望管道写入是原子的且非交错的,因为它们(大概)小于 PIPE_BUF 字节。但它不起作用,因为 ruby​​ 的管道 puts() 实现单独调用 write(2) 来附加尾随换行符,这就是为什么你的写入有时会失败在您期望换行的地方交错。

这是从脚本的 fork-following strace 中摘录的证实摘录:

$ strace -s 2048 -fe trace=write ./so-1326067.rb
....
4574  write(4, "#0:12 tiga => 3, 3 => tiga #0:12", 32) = 32
4574  write(4, "\n", 1)
....

但是放入自己的换行符可以解决问题,确保您的整个记录​​在一个系统调用中传输:

....
5190  write(4, "#194:41 tujuh => 7, 7 => tujuh #194:41\n", 39 <unfinished ...>
5179  write(4, "#183:38 enam => 6, 6 => enam #183:38\n", 37 <unfinished ...>
....

如果由于某种原因无法为您工作,您将拥有协调进程间互斥体(如File.flock())。

UPDATE

It may be bad form to update after the answer was accepted, but the original is a bit misleading. Whether or not ruby makes a separate write(2) call for the automatically-appended newline is dependent upon the buffering state of the output IO object.

$stdout (when connected to a tty) is generally line-buffered, so the effect of a puts() -- given reasonably sized string -- with implicitly added newline is a single call to write(2). Not so, however, with IO.pipe and $stderr, as the OP discovered.

ORIGINAL ANSWER

Change your chief pipe.puts() argument to be a newline terminated string:

pipe.puts "##{c} ... #{i}\n"  # <-- note the newline

Why? You set pipe.sync hoping that the pipe writes would be atomic and non-interleaved, since they are (presumably) less than PIPE_BUF bytes. But it didn't work, because ruby's pipe puts() implementation makes a separate call to write(2) to append the trailing newline, and that's why your writes are sometimes interleaved where you expected a newline.

Here's a corroborating excerpt from a fork-following strace of your script:

$ strace -s 2048 -fe trace=write ./so-1326067.rb
....
4574  write(4, "#0:12 tiga => 3, 3 => tiga #0:12", 32) = 32
4574  write(4, "\n", 1)
....

But putting in your own newline solves the problem, making sure that your entire record is transmitted in one syscall:

....
5190  write(4, "#194:41 tujuh => 7, 7 => tujuh #194:41\n", 39 <unfinished ...>
5179  write(4, "#183:38 enam => 6, 6 => enam #183:38\n", 37 <unfinished ...>
....

If for some reason that cannot work for you, you'll have to coordinate an interprocess mutex (like File.flock()).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文