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

使用Ruby执行JS风格的异步/非阻塞回调,而不使用像线程这样的重型机器?,ruby,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 == r

我是一名前端开发人员,对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"
目标是让回调在不阻塞主脚本执行的情况下运行。换句话说,我希望“Hello”行出现在“All 5 requests…”行上方的输出中。此外,回调应该并发运行,以便最快完成的回调首先进入结果数组

使用JavaScript,我只需将回调调用包装成一个具有零延迟的
setTimeout

setTimeout( function() { callback(item); }, 0);
这种JS方法不能实现真正的多线程/并发/并行执行。在后台,回调将在一个线程中顺序运行,或者更确切地说是在低级别上交错运行

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

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

我试图探讨如何使用Ruby实现JS风格的异步方法,并得到了使用建议:

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

  • 手动定义线程的数组
  • 手动定义互斥锁
  • 为每个回调启动一个新线程,并将其添加到数组中
  • 将互斥对象传递到每个回调中
  • 在回调中使用互斥来进行线程同步
  • 确保在程序完成之前完成所有线程
  • 与JavaScript的
    setTimeout()
    相比,这实在是太多了。因为我不需要真正的并行执行,所以我不想每次异步执行proc时都构建那么多的脚手架

  • 一个复杂的Ruby库,如赛璐珞和事件机。它们看起来需要几个星期才能学会

  • 自定义解决方案,如(作者、,apeiros@freenode,声称它与setTimeout在引擎盖下的功能非常接近)。它几乎不需要搭建脚手架,也不涉及螺纹。但它似乎按照回调的执行顺序同步运行回调

  • 我一直认为Ruby是最接近我理想的编程语言,而JS是穷人的编程语言。这让我有点气馁,Ruby不能在不涉及重型机器的情况下,用JS做一件琐碎的事情

    因此,问题是:在不涉及线程或复杂库等复杂机制的情况下,用Ruby进行异步/非阻塞回调最简单、最直观的方法是什么


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

    正如人们所说,如果不使用线程或抽象其功能的库,就不可能实现您想要的。但是,如果只是您想要的
    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
    
    正如您所看到的,您使用它的方式本质上是相同的,我唯一改变的是我添加了一个互斥锁,以防止结果数组上出现竞争条件

    旁白:为什么在使用示例中需要互斥体 即使javascript只在单个内核上运行,这也不会因为操作的原子性而阻止竞争条件。推送到数组不是一个原子操作,因此执行多条指令

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

    好吧,在研究了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!");
    
    请注意,回调以相反的顺序完成

    我们还将假设使用
    结果
    数组可能会产生竞争条件。在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:延迟块的执行 如果您需要设置超时的延迟功能,您可以这样做

    JS

    setTimeout(function(){ console.log('Foo'); }, 2000);
    
    branch(2) { puts 'Foo' }
    
    Ruby

    setTimeout(function(){ console.log('Foo'); }, 2000);
    
    branch(2) { puts 'Foo' }
    
    示例4:等待所有线程完成 对于JS,没有简单的wa
    branch(2) { puts 'Foo' }
    
    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!"
    
    # (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