Ruby多線程庫(kù)(Thread)使用方法詳解
Thread是Ruby的線程庫(kù),Thread庫(kù)已經(jīng)內(nèi)置在Ruby中,但如果想要使用線程安全的Queue、Mutex以及條件變量等,則需要手動(dòng)require 'thread'
。
主線程main
默認(rèn)情況下,每個(gè)Ruby進(jìn)程都具備一個(gè)主線程main,如果沒(méi)有創(chuàng)建新的線程,所有的代碼都將在這個(gè)主線程分支中執(zhí)行。
使用Thread.main()
類方法可獲取當(dāng)前線程組的主線程,使用Thread.current()
可以獲取當(dāng)前正在執(zhí)行的線程分支。使用Thread.list()
可獲取當(dāng)前進(jìn)程組中所有存活的線程。
p Thread.main p Thread.current p Thread.main == Thread.current =begin #<Thread:0x0000000001d9ae58 run> #<Thread:0x0000000001d9ae58 run> true =end
可見(jiàn),線程其實(shí)是一個(gè)Thread類的實(shí)例對(duì)象。
創(chuàng)建Ruby線程
使用Thread庫(kù)的new()、start()、fork()可創(chuàng)建線程,它們幾乎等價(jià),且后兩者是別名關(guān)系。
創(chuàng)建線程時(shí)需傳遞一個(gè)代碼塊或Proc對(duì)象參數(shù), 它們是要執(zhí)行的任務(wù),它們將在新的線程分支中執(zhí)行。如果需要,可以為代碼塊或Proc對(duì)象傳遞參數(shù)。
arr=[] a,b,C=1,2,3 Thread.new(a,b,c) { |d,e,f| arr << d << e << f } sleep 1 p arr#=> [1,2,3]
如果主線程先執(zhí)行完成,主線程將直接退出,主線程的退出將會(huì)終止進(jìn)程,使得其它線程也會(huì)退出。
Thread.new {puts "hello"} puts "world"
上述代碼幾乎總是會(huì)輸出world
,然后退出,主線程的退出使得子線程不會(huì)輸出"hello"
。之所以總是會(huì)輸出world而不是輸出hello,這和Ruby的線程調(diào)度有關(guān),在后面的文章中會(huì)詳細(xì)解釋Ruby中的線程調(diào)度。
join()和value()等待線程
如果想要等待某個(gè)線程先執(zhí)行完成,可使用t.join()
,如果線程t尚未退出,則join()會(huì)阻塞??梢栽谌我饩€程中調(diào)用t.join()
,誰(shuí)調(diào)用誰(shuí)等待。
t = Thread.new { puts "I am Child" } t.join # 等待子線程執(zhí)行完成 puts "I am Parent"
還可以將多個(gè)線程對(duì)象放進(jìn)數(shù)組,然后執(zhí)行遍歷join,另一種常見(jiàn)的做法是使用map{}.each(&:join)
的方式:
threads = [] 3.times do |i| # 將多個(gè)線程加入到數(shù)組中 threads << Thread.new { puts "Thread #{i}" } end # 在main線程中join每個(gè)線程, # 因此只有3個(gè)線程全都完成后,main線程才會(huì)繼續(xù),即退出 threads.each(&:join) =begin Thread 1 Thread 0 Thread 2 =end # 另一種常見(jiàn)方式 3.times.map {|i| Thread.new { puts "Thread #{i}" } }.each(&:join) Array.new(3) {|i| Thread.new { puts "Thread #{i}" } }.each(&:join)
t.value()
和t.join()
類似,不同之處在于t.value()
在內(nèi)部調(diào)用t.join()
等待線程t之后,還會(huì)在等待成功時(shí)取得該線程的返回值。
a = Thread.new { 2 + 2 } p a.value#=> 4
注意,對(duì)于Ruby來(lái)說(shuō),無(wú)論是否執(zhí)行join()操作,任務(wù)執(zhí)行完成的線程都會(huì)馬上被操作系統(tǒng)回收(從OS線程表中刪除),但被回收的線程仍然能夠使用value()
方法來(lái)獲取被回收線程的返回值。之所以會(huì)這樣,我個(gè)人猜想,也許是因?yàn)镽uby內(nèi)部已經(jīng)幫我們執(zhí)行了join操作并將線程返回值保存在Ruby內(nèi)部,這樣對(duì)于用戶來(lái)說(shuō)就更加安全,而且用戶執(zhí)行join()或value()操作,可能是在等待Ruby內(nèi)部的這個(gè)值的出現(xiàn)。
線程的異常處理
默認(rèn)情況下,當(dāng)某個(gè)非main線程中拋出異常后,該線程將因異常而終止,但是它的終止不會(huì)影響其它線程。
t = Thread.new {raise "hello"} # 拋出異常 sleep 1 # 仍然睡眠1秒后退出
如果使用了t.join()
或t.value()
去等待拋出異常的線程t,異常將會(huì)傳播給調(diào)用這兩個(gè)方法的線程。例如主線程調(diào)用t.join
,如果t會(huì)拋出一次異常,那么主線程在等待過(guò)程中還會(huì)拋出一次異常。
t = Thread.new {raise "hello"} # 拋出異常 t.join() # 子線程拋異常后,main線程也拋異常
如果想要讓任意線程出現(xiàn)異常時(shí)終止整個(gè)程序,可設(shè)置類方法Thread.abort_on_exception
為true,它會(huì)在任意子線程拋出異常后自動(dòng)傳播給main線程,從而終止進(jìn)程:
Thread.abort_on_exception = true Thread.new { raise "Error" } sleep 1# 不會(huì)睡眠完1秒,而是子線程異常后立即異常退出
如果想要讓某個(gè)特定的線程出現(xiàn)異常時(shí)終止整個(gè)程序,可設(shè)置同名的實(shí)例方法t.abort_on_exception
為true,只有t線程異常時(shí)才會(huì)終止程序。
t1 = Thread.new { raise "Error from t1" } t1.abort_on_exception = true sleep 1
另外,線程實(shí)例方法t.raise()
可以直接在線程t拋出異常。
需注意,Ruby線程有一個(gè)巨大的缺點(diǎn):無(wú)論是raise拋出異常還是各種終止(比如kill、exit),都不會(huì)執(zhí)行ensure子句。
線程的狀態(tài)和生命周期
Ruby中的線程具有5種狀態(tài),可通過(guò)t.status()
查看,該方法有5種對(duì)應(yīng)的返回值:
- run: 線程正在運(yùn)行(running)或可運(yùn)行(runnable) - sleep: 線程處于睡眠態(tài),比如阻塞(如sleep,mutex,io block) - false: 線程正常退出后的狀態(tài),包括執(zhí)行完流程、手動(dòng)退出(t.exit)、信號(hào)終止(t.kill) - nil: 線程因拋出異常(比如raise)而退出的狀態(tài) - aborting: 線程被完全kill之前的過(guò)渡狀態(tài),不考慮這種狀態(tài)的存在
另外,還有兩種統(tǒng)稱狀態(tài):
- alive:存活的線程,等價(jià)于run + sleep
- stop:已停止的線程,等價(jià)于sleep + dead(false+nil)
可分別使用alive?()
和stop?()
來(lái)判斷線程是否屬于這兩種統(tǒng)稱狀態(tài)。
此外:
Kernel.sleep:讓當(dāng)前線程睡眠指定時(shí)長(zhǎng),無(wú)參數(shù)則永久睡眠,線程將進(jìn)入睡眠隊(duì)列 Thread.stop:讓當(dāng)前線程睡眠,進(jìn)入睡眠隊(duì)列,等價(jià)于無(wú)參數(shù)的sleep Thread.pass:轉(zhuǎn)讓CPU,當(dāng)前線程進(jìn)入就緒隊(duì)列而不是睡眠隊(duì)列 t.run:?jiǎn)拘丫€程t使其進(jìn)入就緒隊(duì)列,同時(shí)讓當(dāng)前線程放棄CPU,調(diào)度程序?qū)⒅匦抡{(diào)度 t.wakeup:?jiǎn)拘丫€程t使其進(jìn)入就緒隊(duì)列,但不會(huì)讓當(dāng)前線程放棄CPU,調(diào)度程序?qū)⒉粫?huì)立即重新調(diào)度 Thread.kill:終止指定線程,它將不再被調(diào)度 Thread.exit:終止當(dāng)前線程,它將不再被調(diào)度 t.exit,t.kill,t.terminate:終止線程t,t將不再被調(diào)度
幾個(gè)注意事項(xiàng):
- 這里5個(gè)終止線程的方式效果上是完全等價(jià)的,三個(gè)實(shí)例方法是別名關(guān)系,而兩個(gè)類方法的內(nèi)部也都是調(diào)用線程對(duì)象的kill
- 最好要不加區(qū)分地看待run和wakeup
- 對(duì)于Thread.pass,除了知道它轉(zhuǎn)讓CPU的行為是確定的,不要對(duì)它假設(shè)任何額外的行為,比如不要認(rèn)為出讓CPU后一定會(huì)調(diào)度到其它Ruby線程,很有可能會(huì)在調(diào)度其它一些非Ruby線程后再次先調(diào)度到本線程而非其它Ruby線程
- 需注意,無(wú)論是raise拋出異常還是各種終止(比如kill、exit),都不會(huì)執(zhí)行ensure子句
線程私有變量和局部變量
Ruby進(jìn)程內(nèi)的所有線程共享進(jìn)程的虛擬地址空間,所以共享了一些數(shù)據(jù)。
但線程是語(yǔ)句塊或者Proc對(duì)象,所以語(yǔ)句塊內(nèi)部創(chuàng)建的變量是在當(dāng)前線程棧內(nèi)部的,是每個(gè)線程私有的變量。
# 主線程中的變量 a = 1 # 子線程 t1 = Thread.new(3) do |x| a += 1 b=3 x=4 end # 主線程 t1.join p a# 2 #p b # 報(bào)錯(cuò),b不存在 #p x # 報(bào)錯(cuò),x不存在
Ruby為線程提供了局部變量共享的概念,每個(gè)線程對(duì)象都可以有自己的局部數(shù)據(jù)空間(即線程本地變量),線程對(duì)象的局部空間互不影響,比如兩個(gè)線程中同時(shí)進(jìn)行正則匹配,兩個(gè)線程的$~
是不一樣且互不影響的。
線程對(duì)象t
的局部數(shù)據(jù)空間是t[key]=value
,即一個(gè)名為t的hash結(jié)構(gòu),因?yàn)閷?duì)象t是可以共享的,所以它的局部空間也是共享的。
t1 = Thread.new do t = Thread.current t[:name] = "junmajinlong" t[:age] = 23 end t1.join p t1.keys # [:name, :age] p t1.key? :gender # false p t1[:name] # "junmajinlong" t1[:age] = 24 p t1[:age]# 24
所以,有這么幾個(gè)方法:
t[key] t[key]= t.keys t.key?
此外還有一個(gè)fetch()方法,類似于Hash的fetch(),默認(rèn)情況下訪問(wèn)不存在的key會(huì)異常,可指定默認(rèn)值或通過(guò)語(yǔ)句塊返回默認(rèn)值。
嚴(yán)格來(lái)說(shuō),從Ruby 1.9出現(xiàn)Fiber之后,t[]
不再是線程本地變量(thread-local),而是纖程(Fiber)本地變量(fiber-local)。但也支持使用線程本地變量:
t.thread_variables t.thread_variable? t.thread_variable_get t.thread_variable_set
線程組
默認(rèn)情況下,所有線程都在默認(rèn)的線程組中,這個(gè)默認(rèn)線程組是Ruby程序啟動(dòng)時(shí)創(chuàng)建的??墒褂?code>ThreadGroup::Default獲取默認(rèn)線程組。
t1 = Thread.new do Thread.stop end p t1.group p Thread.current.group p ThreadGroup::Default =begin #<ThreadGroup:0x00000000019bcb60> #<ThreadGroup:0x00000000019bcb60> #<ThreadGroup:0x00000000019bcb60> =end
- 使用
ThreadGroup.new
可創(chuàng)建一個(gè)自定義的線程組 - 使用
tg.add(t)
可將線程t加入線程組tg,這將會(huì)從原來(lái)的線程組移除t再加入新組tg - 使用
tg.list
可列出線程組tg中的所有線程 - 使用
t.group
可獲取線程t所屬的線程組 - 子線程會(huì)繼承父線程的線程組,即子線程也會(huì)加入父線程所在的線程組
tg = ThreadGroup.new t1 = Thread.new { Thread.stop } t2 = Thread.new { Thread.stop } tg.add t1 tg.add t2 pp tg.list pp t1.group =begin [#<Thread:0x000000000196c480 a.rb:4 sleep_forever>, #<Thread:0x000000000196c3b8 a.rb:5 sleep_forever>] #<ThreadGroup:0x000000000196c520> =end
線程組還有一個(gè)功能:可使用tg.enclose
封閉線程組tg,封閉后的線程組將不允許內(nèi)部線程移出加入其它組,也不允許外界線程加入該組,只允許在該組中創(chuàng)建新線程。使用tg.enclosed?
測(cè)試線程組tg是否已封閉。
其實(shí),使用線程組可以將多個(gè)線程分類統(tǒng)一管理,線程組本質(zhì)是一個(gè)線程數(shù)組加一些額外屬性。比如,可以為線程組定義一些額外的針對(duì)線程組中所有線程的功能:wakeup組中的所有線程、join所有線程、kill所有線程。
class ThreadGroup def wakeup list.each(&:wakeup) end def join list.each { |th| th.join if th != Thread.current } end def kill list.each(&:kill) end end
更多關(guān)于Ruby多線程知識(shí)請(qǐng)查看下面的相關(guān)鏈接
版權(quán)聲明:本站文章來(lái)源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請(qǐng)保持原文完整并注明來(lái)源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來(lái)源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來(lái),僅供學(xué)習(xí)參考,不代表本站立場(chǎng),如有內(nèi)容涉嫌侵權(quán),請(qǐng)聯(lián)系alex-e#qq.com處理。