使用Ruby执行JS风格的异步/非阻塞回调,而不使用像线程这样的重型机器?
我是一名前端开发人员,对Ruby有点熟悉。我只知道如何以同步/顺序的方式进行Ruby,而在JS中,我习惯于异步/非阻塞回调 下面是示例Ruby代码:使用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
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