使用Ruby的JS风格的异步/非阻塞回调执行,没有像线程这样的重型机器?

时间:2014-07-26 23:54:55

标签: ruby

我是一名前端开发人员,对Ruby有些熟悉。我只知道如何以同步/顺序方式执行Ruby,而在JS中我习惯于异步/非阻塞回调。

以下是Ruby代码示例:

results = []
rounds = 5

callback = ->(item) {
  # This imitates that the callback may take time to complete
  sleep rand(1..5)

  results.push item

  if results.size == rounds
    puts "All #{rounds} requests have completed! Here they are:", *results
  end
}

1.upto(rounds) { |item| callback.call(item) }

puts "Hello"

目标是在不阻止主脚本执行的情况下运行回调。换句话说,我想要"你好"行出现在"所有5个请求之上的输出中......"线。此外,回调应该同时运行,以便最快完成的回调使其首先进入结果数组。

使用JavaScript,我只需将回调调用包装为setTimeout,零延迟:

setTimeout( function() { callback(item); }, 0);

这种JS方法没有实现真正的多线程/并发/并行执行。在引擎盖下,回调将在一个线程中顺序运行,或者在低级别上交错运行。

但是在实际级别上它会显示为并发执行:生成的数组将按照与每个回调花费的时间量相对应的顺序填充,即。即生成的数组将按每次回调完成的时间排序。

请注意,我只想要setTimeout()的异步功能。我不需要内置于setTimeout() 中的睡眠功能(不要与回调示例中使用的sleep混淆模仿耗时的操作)。

我试图探究如何使用Ruby进行JS风格的异步方法,并给出了使用建议:

  1. 多线程。这可能是Ruby的方法,但它需要大量的脚手架:

    1. 手动为线程定义数组。
    2. 手动定义互斥锁。
    3. 为每个回调开始一个新线程,将其添加到数组中。
    4. 将互斥锁传递给每个回调。
    5. 在回调中使用互斥锁进行线程同步。
    6. 确保在程序完成之前完成所有线程。
    7. 与JavaScript setTimeout()相比,这太过分了。因为我不需要真正的并行执行,所以每次我想要异步执行一个proc时,我都不想构建那么多的脚手架。

    8. 像Celluloid和Event Machine这样复杂的Ruby库。他们看起来需要数周才能学会它们。

    9. this one这样的自定义解决方案(作者,apeiros @ freenode,声称它非常接近setTimeout的内幕)。它几乎不需要构建脚手架,也不涉及线程。但它似乎按照它们执行的顺序同步运行回调。

    10. 我一直认为Ruby是一种最接近我理想的编程语言,而JS则是一个穷人的编程语言。并且有点让我不鼓励Ruby在没有涉及重型机械的情况下无法做一件与JS无关的事情。

      所以问题是:使用Ruby进行异步/非阻塞回调的最简单,最直观的方法是什么,而不涉及线程或复杂库等复杂的机制?

      PS如果在赏金期间没有令人满意的答案,我将通过apeiros挖掘#3,并可能使其成为公认的答案。

2 个答案:

答案 0 :(得分:3)

就像人们所说的那样,如果不使用Threads或抽象其功能的库,就无法实现您的目标。但是,如果它只是您想要的setTimeout功能,那么实现实际上非常小。

我试图在ruby中模仿Javascript的setTimeout

require 'thread'
require 'set'

module Timeout
  @timeouts = Set[]
  @exiting = false

  @exitm = Mutex.new
  @mutex = Mutex.new

  at_exit { wait_for_timeouts }

  def self.set(delay, &blk)
    thrd = Thread.start do
      sleep delay
      blk.call
      @exitm.synchronize do
        unless @exiting
          @mutex.synchronize { @timeouts.delete thrd }
        end
      end
    end

    @mutex.synchronize { @timeouts << thrd }
  end

  def self.wait_for_timeouts
    @exitm.synchronize { @exiting = true }
    @timeouts.each(&:join)
    @exitm.synchronize { @exiting = false }
  end
end

以下是如何使用它:

$results = []
$rounds = 5

mutex = Mutex.new
def callback(n, mutex)
  -> {
    sleep rand(1..5)

    mutex.synchronize {
      $results << n
      puts "Fin: #{$results}" if $results.size == $rounds
    }
  }
end

1.upto($rounds) { |i| Timeout.set(0, &callback(i, mutex)) }

puts "Hello"

输出:

Hello
Fin: [1, 2, 3, 5, 4]

正如您所看到的,您使用它的方式基本相同,我唯一改变的是我添加了一个互斥锁来防止结果数组上的竞争条件。

除此之外:为什么我们需要使用示例中的互斥锁

即使javascript仅在单个核心上运行,也不会因操作的原子性而阻止竞争条件。推送到数组不是原子操作,因此执行多个指令。

  • 假设它是两条指令,将元素放在最后,并递增大小。 (SETINC)。
  • 考虑两次推送可以交错的所有方式(考虑对称性):
    • SET1 INC1 SET2 INC2
    • SET1 SET2 INC1 INC2
  • 第一个是我们想要的,但第二个会导致第二个覆盖第一个。

答案 1 :(得分:1)

好吧,经过一些摆弄线索和学习apeiros和asQuirreL的贡献后,我想出了一个适合我的解决方案。

我将首先展示样本用法,最后是源代码。

示例1:简单的非阻塞执行

首先,我试图模仿的 JS 示例:

setTimeout( function() {
  console.log("world");
}, 0);

console.log("hello");

// 'Will print "hello" first, then "world"'.

以下是我用我的小 Ruby 库来实现的目标:

# You wrap all your code into this...
Branch.new do

  # ...and you gain access to the `branch` method that accepts a block.
  # This block runs non-blockingly, just like in JS `setTimeout(callback, 0)`.
  branch { puts "world!" }

  print "Hello, "

end

# Will print "Hello, world!"

请注意您不必专心创建线程,等待它们完成。唯一需要的脚手架是Branch.new { ... }包装器。

示例2:使用互斥锁

同步线程

现在我们假设我们正在使用线程之间共享的一些输入和输出。

JS 代码我试图用Ruby重现:

var
  results = [],
  rounds = 5;

for (var i = 1; i <= rounds; i++) {

  console.log("Starting thread #" + i + ".");

  // "Creating local scope"
  (function(local_i) {
    setTimeout( function() {

      // "Assuming there's a time-consuming operation here."

      results.push(local_i);
      console.log("Thread #" + local_i + " has finished.");

      if (results.length === rounds)
        console.log("All " + rounds + " threads have completed! Bye!");

    }, 0);
  })(i);
}

console.log("All threads started!");

此代码生成以下输出:

Starting thread #1.
Starting thread #2.
Starting thread #3.
Starting thread #4.
Starting thread #5.
All threads started!
Thread #5 has finished.
Thread #4 has finished.
Thread #3 has finished.
Thread #2 has finished.
Thread #1 has finished.
All 5 threads have completed! Bye!

请注意,回调以相反的顺序完成。

我们还假设工作results数组可能会产生竞争条件。在JS中,这绝不是问题,但在多线程Ruby中,必须使用互斥锁来解决这个问题。

Ruby 等同于上述内容:

Branch.new 1 do

  # Setting up an array to be filled with that many values.
  results = []
  rounds = 5

  # Running `branch` N times:
  1.upto(rounds) do |item|

    puts "Starting thread ##{item}."

    # The block passed to `branch` accepts a hash with mutexes 
    # that you can use to synchronize threads.
    branch do |mutexes|

      # This imitates that the callback may take time to complete.
      # Threads will finish in reverse order.
      sleep (6.0 - item) / 10

      # When you need a mutex, you simply request one from the hash.
      # For each unique key, a new mutex will be created lazily.
      mutexes[:array_and_output].synchronize do
        puts "Thread ##{item} has finished!"
        results.push item

        if results.size == rounds
          puts "All #{rounds} threads have completed! Bye!"
        end
      end
    end
  end

  puts "All threads started."
end

puts "All threads finished!"

注意如何创建线程,等待它们完成,创建互斥体并将它们传递到块中。

示例3:延迟执行块

如果您需要setTimeout的延迟功能,可以这样做。

<强> JS

setTimeout(function(){ console.log('Foo'); }, 2000);

<强>红宝石

branch(2) { puts 'Foo' }

示例4:等待所有线程完成

使用JS,没有简单的方法让脚本等待所有线程完成。你需要一个await / defer库。

但是在Ruby中它是可能的,而Branch使它更简单。如果在Branch.new{}包装器之后编写代码,它将在包装器中的所有分支完成之后执行。您不需要手动确保所有线程都已完成,Branch会为您执行此操作。

Branch.new do
  branch { sleep 10 }
  branch { sleep 5 }

  # This will be printed immediately
  puts "All threads started!"
end

# This will be printed after 10 seconds (the duration of the slowest branch).
puts "All threads finished!"

顺序Branch.new{}包装器将按顺序执行。

来源

# (c) lolmaus (Andrey Mikhaylov), 2014
# MIT license http://choosealicense.com/licenses/mit/

class Branch
  def initialize(mutexes = 0, &block)
    @threads = []
    @mutexes = Hash.new { |hash, key| hash[key] = Mutex.new }

    # Executing the passed block within the context
    # of this class' instance.
    instance_eval &block

    # Waiting for all threads to finish
    @threads.each { |thr| thr.join }
  end

  # This method will be available within a block
  # passed to `Branch.new`.
  def branch(delay = false, &block)

    # Starting a new thread 
    @threads << Thread.new do

      # Implementing the timeout functionality
      sleep delay if delay.is_a? Numeric

      # Executing the block passed to `branch`,
      # providing mutexes into the block.
      block.call @mutexes
    end
  end
end