ruby并發(fā)編程

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

image.png

我們看到 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)真正的并行。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容