ruby并發(fā)編程一般使用Thread實現(xiàn),但是Thread默認使用時通過共享內(nèi)存
的使用的,即在子線程和主線程(或其他子線程)是get/set同一套變量的,不使用鎖則會因數(shù)據(jù)競爭
導(dǎo)致執(zhí)行不可控,執(zhí)行結(jié)果不正確:
counter = 0
threads = 10.times.map do
Thread.new do
1000.times do
counter += 1 # 非原子操作:讀、加、寫
end
end
end
threads.each(&:join)
puts counter # 期望是 10000,但結(jié)果可能小于 10000
但ruby的GIL保證只有一個線程執(zhí)行,我運行了很久都沒遇到小于10000的情況(但不能保證不會遇到)
使用鎖又會導(dǎo)致性能低下,以及死鎖等問題(多個共享資源、多個鎖的情況);并發(fā)執(zhí)行多個且有分支控制時,也會導(dǎo)致代碼邏輯過于復(fù)雜,容易出bug且難以調(diào)試。
concurrent-ruby
是一個并發(fā)編程的工具集,可以使用其提供的并發(fā)原語,方便地實現(xiàn)多線程編程。
Concurrent::Async
Async模塊是一種將簡單但強大的異步功能混合到任何普通的舊式 Ruby 對象或類中的方法,將每個對象變成一個簡單的 Actor。方法調(diào)用在后臺線程上處理。調(diào)用者可以在后臺進行處理時自由執(zhí)行其他操作。
require 'concurrent-ruby'
class A
# 引入Async module
include Concurrent::Async
def say_it(word)
raise 'a is wrong' if word == 'a'
sleep(1)
puts "say #{word}"
true
end
end
# 異步方式調(diào)用
a = A.new.async.say_it('a') #異步執(zhí)行
# value可傳遞timeout參數(shù)->超時秒數(shù),執(zhí)行到此時如果異步任務(wù)執(zhí)行完,則返回結(jié)果,正在執(zhí)行,則等待(阻塞)
puts a.value # 調(diào)用失敗時 value為nil,如果是value!方法直接拋出異常
puts a.reason # error
b = A.new.async.say_it('b')
puts b.value # true
puts b.reason # 錯誤為nil
# 阻塞方式調(diào)用
c = A.new.await.say_it('c')
puts c
執(zhí)行的結(jié)果是一個IVar對象,能夠檢查執(zhí)行狀態(tài),返回結(jié)果等
Concurrent::ScheduledTask & Concurrent::TimerTask
ScheduledTask
為在指定的延遲后執(zhí)行
require 'concurrent-ruby'
task = Concurrent::ScheduledTask.new(2){ 'What does the fox say?' } # 2秒延時
puts task.class
puts task.state #=> :unscheduled
task.execute # 開始執(zhí)行
puts task.state #=> pending
# wait for it...
sleep(3)
puts task.unscheduled? #=> false
puts task.pending? #=> false
puts task.fulfilled? #=> true
puts task.rejected? #=> false
puts task.value #=> 'What does the fox say?'
TimerTask
運行一個定期執(zhí)行任務(wù)
require 'concurrent-ruby'
# execution_interval 執(zhí)行間隔(秒數(shù)),默認60秒
# interval_type 間隔方式默認fixed_delay
# fixed_delay: 上一次執(zhí)行結(jié)束和下一次執(zhí)行開始之間的間隔
# fixed_rate:上一次執(zhí)行開始到下一次開始執(zhí)行的間隔(如果執(zhí)行時間超過間隔,則下一次執(zhí)行將在前一次執(zhí)行完成后立即開始。不會并發(fā)運行)
#
task = Concurrent::TimerTask.new(execution_interval: 1, interval_type: :fixed_rate){ t = Time.now.sec; raise "aaa" if t % 5 == 0 ; puts t; t }
# 觀察者類
class TaskObserver
# time 執(zhí)行時間
# result 執(zhí)行結(jié)果
# ex 異常
def update(time, result, ex)
if result
print "(#{time}) Execution successfully returned #{result}\n"
else
print "(#{time}) Execution failed with error #{ex}\n"
end
end
end
# 添加觀察者
task.add_observer(TaskObserver.new) # 調(diào)用其update方法
task.execute # 開始執(zhí)行
# 異步執(zhí)行,防止主線程結(jié)束
gets
# 關(guān)閉
task.shutdown
Concurrent::Promises
提供優(yōu)雅的方式實現(xiàn)處理異步計算和任務(wù)鏈式執(zhí)行。
require 'concurrent-ruby'
x = Concurrent::Promises.
future(2) { |v| raise 'aa' }.
# 順序執(zhí)行
then(&:succ).then{|x| x -= 1}.
# 異常時
rescue { |error| 999 }
puts x.result.inspect # 3
# 分支執(zhí)行
head = Concurrent::Promises.fulfilled_future(-1) #
branch1 = head.then(&:abs).then(&:succ) # 分支1(絕對值 -> +1)
branch2 = head.then(&:succ).then(&:abs) # 分支2(+1 -> 絕對值)
puts head.value # -1
puts branch1.value # 2
puts branch2.value # 0
# 壓縮分支
puts (branch1 & branch2).value.inspect # [2, 0]
# 任意分支
puts (branch1.then{raise 'a'} | branch2).value.inspect # 0
線程池:限制線程數(shù)量
require 'concurrent-ruby'
pool = Concurrent::FixedThreadPool.new(5) # 最大5 threads
# 可以指定最大,最小線程數(shù),回收空閑時間,
# pool = Concurrent::ThreadPoolExecutor.new(
# min_threads: 5, # 最小線程數(shù)
# max_threads: 5, # 最大線程數(shù)
# idletime: 60, # 回收空閑時間
# max_queue: 0 # 最大隊列大小
# fallback_policy: :abort # 等待隊列滿時策略:abort(異常),discard(丟棄),caller_runs(調(diào)用者線程執(zhí)行)
# )
# 使用線程池執(zhí)行異步任務(wù)
promises = (1..10).map do |i|
# 在pool上執(zhí)行
Concurrent::Promises.future_on(pool, i) do |i|
sleep 1 # 模擬耗時操作
raise "x" if i %2 == 0
puts "Task #{i} completed by #{Thread.current.object_id}"
i * 2
end
end
puts pool.running? # true
# 等待所有任務(wù)完成并獲取結(jié)果
results = promises.map(&:value) # [2, nil, 6, nil, 10, nil, 14, nil, 18, nil]
puts pool.running? # true
puts "Results: #{results}"
# pool.wait_for_termination # 等待執(zhí)行完關(guān)閉(但測試時報錯,未知原因)
puts pool.running? # true
pool.shutdown # 需要手動關(guān)閉
go風格channel
實驗版本edge才有的功能。
require 'concurrent-edge'
# 輸入channel,容量為2
in_c = Concurrent::Channel.new(capacity: 2)
# 輸出channel
out_c = Concurrent::Channel.new(capacity: 2)
# 寫入數(shù)據(jù)
Concurrent::Channel.go do
10.times do |i|
in_c.put(i)
end
in_c.close
end
Concurrent::Channel.go do
loop do
# 讀取channel數(shù)據(jù)
msg = ~ in_c
break if msg.nil? # close時發(fā)送的nil數(shù)據(jù)
out_c << (msg ** 2)
end
# 等效寫法,each(忽略掉close的)
# in_c.each do |msg|
# out_c << msg ** 2
# end
out_c.close # 關(guān)閉channel
end
# 讀取
loop do
v = ~out_c
break if v.nil?
puts v
end
Ractor
上面說的花里胡哨的,但是因為ruby的 GIL (Global Interpreter Lock),多線程其實在同一時間,只有一個在執(zhí)行。
這就使多線程僅在IO阻塞時起到作用,多CPU其實是用不到的。
Ractor 是 Ruby 3 新引入的特性。Ractor 顧名思義是 Ruby 和 Actor 的組合詞。Actor 模型是一個基于通訊的、非鎖同步的并發(fā)模型。
NUM = 100000
THREAD_NUM = 4
BATCH_SIZE = NUM / THREAD_NUM
def ractor_run(num)
Ractor.new(num) do |start_index|
sum = (start_index...start_index + BATCH_SIZE).inject(0) do |sum, i|
sum += i ** 2
end
Ractor.yield(sum)
end
end
def thread_run(num)
Thread.new(num) do |start_index|
(start_index...start_index+BATCH_SIZE).inject(0) do |sum, i|
sum += i ** 2
end
end
end
def ractor_sum
THREAD_NUM.times.map do |i|
ractor_run(i * BATCH_SIZE)
end.map(&:take).sum
end
def thread_sum
THREAD_NUM.times.map do |i|
thread_run(i * BATCH_SIZE)
end.map{|t| t.join.value}.sum
end
def normal_sum
(0...NUM).inject(0) do |sum, i|
sum += i ** 2
end
end
puts thread_sum
puts ractor_sum
puts normal_sum
require 'benchmark'
Benchmark.bmbm do |x|
# sequential version
x.report('normal'){ 100.times{normal_sum} }
# parallel version with thread
x.report('thread'){ 100.times{thread_sum}}
# parallel version with ractors
x.report('ractor'){ 100.times{ractor_sum} }
end
我們看到 ractor對比thread和normal提升超過三倍,而thread對比normal甚至稍慢。
但Ractor也是有代價的,Ractor之間時數(shù)據(jù)隔離的:
- 只能通過消息傳遞(send 和 receive)進行通信。
- 數(shù)據(jù)傳遞時,必須是 深拷貝 或 不可變 的對象(如數(shù)字、符號等)。
ractor內(nèi)發(fā)送消息(from 1),最后發(fā)送4
r1 = Ractor.new {Ractor.yield 'from 1';puts 'go on';4}
r1.take # from 1
# print go on
r1.take # get 4
ractor內(nèi)接收消息,最后發(fā)送5
r1 = Ractor.new {msg = Ractor.receive;puts msg;5}
r1.send('ok')
# print ok
r1.take # get 5
take未能接收到消息時,阻塞
可共享的object_id是一樣的
one = '3'.freeze
r = Ractor.new(one) do |one|
one
end
two = r.take
puts one.object_id # 60
puts two.object_id # 60
不可共享的object_id就不一樣了(復(fù)制了一份)
one = []
r = Ractor.new(one) do |one|
one
end
two = r.take
puts one.object_id # 80
puts two.object_id # 100
move 移動,這會將 對象移動到接收方,使發(fā)送方無法訪問它。
one = []
r = Ractor.new do
x = Ractor.receive
x
end
r.send(one, move: true)
two = r.take
puts two.object_id # 60
# 移動了,不能再使用
puts one.object_id # `method_missing': can not send any methods to a moved object (Ractor::MovedError)
結(jié)語
-
Concurrent::Async
,可以以簡單的方式實現(xiàn)異步調(diào)用(或修改已有代碼為異步方式), -
Concurrent::ScheduledTask & Concurrent::TimerTask
能夠制定延時執(zhí)行和定期執(zhí)行 -
Concurrent::Promises
實現(xiàn)鏈式調(diào)用、分支處理 - thread pool 實現(xiàn)對并發(fā)的數(shù)量控制
- go風格channel實現(xiàn)消息機制的異步調(diào)用
- Ractor實現(xiàn)真正的并行。