Blogged by Ujihisa. Standard methods of programming and thoughts including Clojure, Vim, LLVM, Haskell, Ruby and Mathematics written by a Japanese programmer. github/ujihisa

Sunday, March 7, 2010

How To Run An External Command Asynchronously

This issue looks easy, but is actually complex.

On shell, you may run a command asynchronously just by adding & at the end of command.

$ sleep 10 &
[1] 8048

You can obtain the process ID, so you can stop the command.

$ kill 8048

How can we run an external command on Ruby?

system()

The easiest way to run an external command is system().

system 'sleep 10 &'

The return value of system() is the exit code. In this case, the system() returns true immediately after that ran, but it doesn't return process ID. To obtain the process ID, you can use $?.

system 'sleep 10 &'
p $? #=> 8050

But unfortunatelly the $? is not the processs ID of sleep, but sh -c sleep 10. There is a cussion. You cannot obtain the process ID of sleep directly.

One more problem: the notation is available only in UNIX based platform.

system() without shell

system 'sleep 10' uses shell but system 'sleep', '10' doesn't use shell. Separate the arguments. But system 'sleep', '10', '&' is not available.

Thread

To emulate '&' feature, how about using Ruby's asynchronous features? Thread is the easiest way of that.

t = Thread.start do
  system 'sleep', '10'
end
t.kill

Looks good, but it doesn't work. Thread certainly will stop, but the external command booted by the thread will still alive.

exec()

How about using another way of booting an external command? There is exec(), which roughly means the combination of system() and exit().

exec 'ls'

roughly means

system 'ls'
exit

So you think that the following code will work well.

t = Thread.start do
  exec 'sleep', '10'
end
t.kill

Unfortunatelly it doesn't work. You cannot use exec() in a child thread.

fork()

Look for another way of supporting asynchronous feature on Ruby. fork(). fork() copies the Ruby process itself.

pid = fork do
  exec 'sleep', '10'
end
Process.kill pid

It works! fork() is awesome!

But there is a bad news. Thread works on all platforms, but fork works on only some platforms. I'm sorry Windows users and NetBSD users.

spawn()

spawn = fork + exec. spawn() works on all platforms! It's perfect! But... ruby 1.8 doesn't have spawn()...

Summary

  • Ruby 1.9 + UNIX: Use fork and exec, or use spawn.
  • Ruby 1.8 + UNIX: Use fork and exec.
  • Ruby 1.9 + Windows: Use spawn.
  • Ruby 1.8 + Windows: Use other OS, other Ruby, or consider using this library

3 comments:

  1. How about win32-processes for Ruby 1.8 + Windows?

    http://rubyforge.org/projects/win32utils/

    ReplyDelete
  2. Oh yet. I should have written about it. Thank you!

    ReplyDelete
  3. http://github.com/rdp/windows_backport_process_spawn

    is an attempt to use win32-process to emulate Process.spawn

    I agree--Process.spawn quite useful.

    ReplyDelete

Followers