我是一名前端开发人员,对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风格的异步方法,并给出了使用建议:
多线程。这可能是Ruby的方法,但它需要大量的脚手架:
与JavaScript setTimeout()
相比,这太过分了。因为我不需要真正的并行执行,所以每次我想要异步执行一个proc时,我都不想构建那么多的脚手架。
像Celluloid和Event Machine这样复杂的Ruby库。他们看起来需要数周才能学会它们。
像this one这样的自定义解决方案(作者,apeiros @ freenode,声称它非常接近setTimeout的内幕)。它几乎不需要构建脚手架,也不涉及线程。但它似乎按照它们执行的顺序同步运行回调。
我一直认为Ruby是一种最接近我理想的编程语言,而JS则是一个穷人的编程语言。并且有点让我不鼓励Ruby在没有涉及重型机械的情况下无法做一件与JS无关的事情。
所以问题是:使用Ruby进行异步/非阻塞回调的最简单,最直观的方法是什么,而不涉及线程或复杂库等复杂的机制?
PS如果在赏金期间没有令人满意的答案,我将通过apeiros挖掘#3,并可能使其成为公认的答案。
答案 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仅在单个核心上运行,也不会因操作的原子性而阻止竞争条件。推送到数组不是原子操作,因此执行多个指令。
SET
,INC
)。SET1 INC1 SET2 INC2
SET1 SET2 INC1 INC2
答案 1 :(得分:1)
好吧,经过一些摆弄线索和学习apeiros和asQuirreL的贡献后,我想出了一个适合我的解决方案。
我将首先展示样本用法,最后是源代码。
首先,我试图模仿的 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 { ... }
包装器。
现在我们假设我们正在使用线程之间共享的一些输入和输出。
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!"
注意如何创建线程,等待它们完成,创建互斥体并将它们传递到块中。
如果您需要setTimeout
的延迟功能,可以这样做。
<强> JS 强>:
setTimeout(function(){ console.log('Foo'); }, 2000);
<强>红宝石强>:
branch(2) { puts 'Foo' }
使用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