一、心得體會
1、今天完成了什么?
20頁鎬頭書
10個(gè)controller
-
回顧以前
- 什么是block?為什么要有block?
舉個(gè)栗子:
class Project default :time Time.now end這個(gè)時(shí)間就是服務(wù)器啟動的時(shí)間,每次new project這個(gè)類的時(shí)候,時(shí)間是不變的。
class Project default_value_for :time { Time.now } end - 什么是block?為什么要有block?
如果加個(gè)block ,就可以實(shí)現(xiàn)每次new的時(shí)候,時(shí)間是更新的。
- 拋出異常,用begin接
2、今天收獲了什么?
- 文件的讀寫
- 線程和進(jìn)程
- controller:客服手機(jī)、客服手機(jī)日志、客服日報(bào)(daylies)、客服手機(jī)目標(biāo)(target)、客服手機(jī)發(fā)圈(moment)、朋友圈分類(momennt)、朋友圈日志、訂單日志、訂單服務(wù)日志
- 明天好好看看訂單(找到個(gè)大boss)
- 沒找到朋友圈圖片
二、讀書筆記
第十章 基本輸入和輸出(Basic Input and Output)
Ruby提供了兩套乍看之下完全不同的I/O例程(routie)。第一套接口很簡單——至今我們已經(jīng)多次使用了。
print "Enter your name: "
name = gets
Kernel模塊實(shí)現(xiàn)了一整套I/O相關(guān)的方法:gets、open、print、printf、putc、puts、readline,readlines和test,它們使得Ruby變成簡便,這些方法通常對標(biāo)準(zhǔn)輸入和輸出進(jìn)行操作,因而很適合用它們來編寫過濾器。
第二種凡事是使用IO對象,這種凡事可以對IO進(jìn)行更多的控制。
10.1 什么是IO對象(What Is an IO Object)
Ruby定義了一個(gè)IO基類來處理輸入和輸出,類File和BasicSocket都是該類的子類,雖然它們提供了更具體的行為,但是基本原則是相同的,IO對象是Ruby程序和某些外部資源之間的一個(gè)雙向通道,當(dāng)然,一個(gè)IO對象不止是這些顯而易見的東西,但最終你所做的只是向之寫入或者從中讀取。
本章我們會重點(diǎn)介紹IO類和它最常用的子類File。如果想獲得更多關(guān)于使用套接字類進(jìn)行網(wǎng)絡(luò)操作的信息。
對于那些想知道操作細(xì)節(jié)的人來說,這意味著一個(gè)IO對象可以管理操作系統(tǒng)的多個(gè)文件描述符,例如,如果你打開一個(gè)對管道,那么一個(gè)IO對象可以既包括讀管道也可以包括寫管道。
10.2 文件打開和關(guān)閉(Opening and Closing Files)
正如你所想,你可以使用File.new創(chuàng)建一個(gè)新的文件對象。
file = File.new("testfile", "r")
# .. 處理該文件
file.close
根據(jù)打開模式,你可以創(chuàng)建一個(gè)用來讀、寫或者兼有兩者的文件對象(這里我們使用“r”模式打開testfile以從中讀取數(shù)據(jù))。我們也可以用“w”模式來表示寫文件,而“r+”表示讀寫。
當(dāng)創(chuàng)建一個(gè)文件時(shí),你還可以指定文件的許可權(quán)限。打開文件后,我們可以寫入或者讀取所需的數(shù)據(jù),最后,作為一個(gè)負(fù)責(zé)任的軟件開發(fā)人員,我們需要關(guān)閉文件,以確保所有緩存的數(shù)據(jù)被寫入一個(gè)文件,并釋放所有相關(guān)的資源。
但Ruby能使這些操作更簡單,方法File.open也可以打開文件,通常情況下,它和Tile.new行為相似,但是當(dāng)和block一起調(diào)用時(shí)就有區(qū)別了,與返回一個(gè)新的File對象不同,open會以剛打開的文件作為參數(shù)調(diào)用相關(guān)聯(lián)的block,當(dāng)block退出時(shí),文件會被自動關(guān)閉。
File.open("testfile, "r") do |file|
# .. 文件處理
end
第二種方式有額外的優(yōu)勢,在較前面的例子中,處理文件的過程中如果發(fā)生異常,可能file.close就不會被調(diào)用,一旦file變量出了其作用域,垃圾收集器最終會把它關(guān)閉,但可能一時(shí)半會兒不會發(fā)生,而與此同時(shí),資源一直被占用。
以block調(diào)用File.open的形式?jīng)]有這個(gè)問題,如果block內(nèi)引發(fā)異常,那么在異常傳遞給調(diào)用者之前文件會被關(guān)閉??此苚pen方法實(shí)現(xiàn)了類似下面的代碼:
class File
def File.open(*args)
result = f = File.new(*args)
if block_given?
begin
result = yield f
ensure
f.close
end
return result
end
end
10.3 文件讀寫(Reading and Writing Files)
我們用于”簡單“I/O的所有方法都適用于文件對象,gets從標(biāo)準(zhǔn)輸入讀取一行(或者從執(zhí)行腳本的命令行上指定的任意文件),而file.gets從文件對象file中讀取一行。
例如,我們可以創(chuàng)建一個(gè)程序copy.rb
while line = gets
puts line
end
如果不帶參數(shù)運(yùn)行此程序,將從終端讀取行然后再復(fù)制回終端顯示,注意下一旦按下回車鍵,每行就會回顯出來。
我們也可以從命令行傳入一個(gè)或多個(gè)文件名,這種情況下gets將依次從文件中讀入。
ruby copy.rb testfile
最后我們可以顯式地打開文件,并從中讀取數(shù)據(jù)。
File.open("testfile") do |file|
while line = file.gets
puts line
end
end
輸出結(jié)果:
This is line one
This is line two
和gets一樣,I/O對象還有一組額外的訪問方法,它們使得Ruby編程更容易。
10.3.1 讀取迭代器(Lterators for Reading)
你既可以使用普通的循環(huán)從IO流中讀取數(shù)據(jù),也可以使用各種各樣的RUby迭代器來讀取數(shù)據(jù)。
I/O#each_byte將以從IO對象中獲取下一個(gè)8位字節(jié)的參數(shù),調(diào)用相關(guān)聯(lián)的block(在下面的例子中,對象類型是file)。
File.open("testfile") do |file|
file.each_byte {|ch| putc ch; print "."}
end
輸出結(jié)果:
T.h.i.s
IO#each_line以文件的每一行為參數(shù)調(diào)用相關(guān)聯(lián)的block。在下面的例子中,我們將用String#dump來顯示換行符,這樣一來你就知道我們沒有騙人了。
File.open("testfile") do |file|
file.each_line { |line| puts "Got ${line.dump}" }
end
輸出結(jié)果:
Got "This is line one\n"
Got "This is line two\n"
Got "This is line three\n"
Got "And so on...\n"
你可以將任意字符序列傳遞給each_line作為行分隔符,然后它會根據(jù)該字符序列相應(yīng)地分割輸入字符串,并返回分割后的每一行,這也是為什么在前面的例子中你能看到\n字符的原因,在下面的例子中,我們將使用字符e作為行分隔符。
File.open("testfile") do |file|
file.each_line("e") { |line| puts "Got #{ line.dump }" }
end
輸出結(jié)果:
Got "This is line"
Got " one"
Got "\nThis is line"
Got " two\nThis is line"
Got " thre"
Got "e"
Got "\n And so on...\n"
如果你將迭代器思想和block自動關(guān)閉文件的特性結(jié)合在一起,你就獲得了IO。foreach,這個(gè)方法以I/O數(shù)據(jù)源的名字作為參數(shù),以讀模式打開它,并以文件中的每一行作為參數(shù)調(diào)用相關(guān)聯(lián)的迭代器,最后會自動關(guān)閉該文件。
IO.foreach("testfile") { |line| puts line }
輸出結(jié)果:
另外,如果你喜歡,你還可以將整個(gè)文件的內(nèi)容讀取到一個(gè)字符串或者一行數(shù)組中。
讀到字符串
str = IO.read("testfile")
str.length
str[0, 30]
讀到一個(gè)數(shù)組中
arr = IO.readlines("testfile")
arr.length
arr[0]
不要忘了在不確定的世界里,I/O也必然是不確定的——大多數(shù)的錯誤會引發(fā)異常,你需要采取適當(dāng)?shù)男袆右詮腻e誤中恢復(fù)。
10.3.1 寫文件(writing to Files)
為什么讀寫不正常?gets卡住了
原來gets是輸入函數(shù),等待鍵盤輸入的。
我也是被自己驚著了?。?!
到目前為止,我們已經(jīng)多次調(diào)用過puts和print,并傳遞任意已存在的對象作為參數(shù),我們相信Ruby會對之進(jìn)行正確的處理(當(dāng)然,他也是這么做的)。但它到底做了什么呢?
答案很簡單:除了少數(shù)幾個(gè)例外,你傳遞給puts和prints的任意對象都會被該對象的to_s方法轉(zhuǎn)換成一個(gè)字符串,如果由于某種原因,to_s方法未能返回一個(gè)合法的字符串,含有對象類名和ID的一個(gè)字符串就會被創(chuàng)建,類似于#<ClassName:0x123456>。
也有例外的情況,nil對象會輸出字符串”nil“,而傳遞給puts的數(shù)組會依次被它的元素打印出來,就像每個(gè)元素被分別傳遞給puts一樣。
如果你想寫入二進(jìn)制數(shù)據(jù)而不想被Ruby干擾怎么辦?通常來說調(diào)用IO#print,并以包括寫入字節(jié)的字符串作為參數(shù)。不過,你也可以使用底層的輸入輸出例程。
我們?nèi)绾尾拍軐⒍M(jìn)制數(shù)據(jù)存儲到字符串中呢?常用的3個(gè)方法:字符串的字面量,一個(gè)字節(jié)一個(gè)字節(jié)地的存入,或使用Array#pack。
str1 = "\001\002\003"
str2 = ""
str2 << 1 << 2 << 3
然而我懷念C++的iostream
有時(shí)實(shí)在無法計(jì)較個(gè)人的喜好......不過,就如你可以使用<<操作符添加一個(gè)對象到數(shù)組一樣,你也可以添加對象到IO輸出流。
endl = "\n"
STDOUT << 99 << " red balloons " << endl
輸出結(jié)果:
99 red balloons
同樣,方法<<使用to_s將它的參數(shù)轉(zhuǎn)換成字符串,然后再按照它的方式傳遞。
盡管一開始我們鄙視可憐的<<操作符,但確實(shí)有一些使用它的理由。因?yàn)槠渌悾ɡ鏢tring和Array)也實(shí)現(xiàn)了相似語義的<<操作符,所以你可以使用<<編寫代碼來附加某些東西,而不必關(guān)心是添加數(shù)組、文件還是字符串。
這種靈活性也使得單元測試比較簡單。
使用字符串I/O
很多時(shí)候,你需要處理假定讀寫一個(gè)或者多個(gè)文件的代碼,但問題是:數(shù)據(jù)并不位于文件中,它或許是SOAP服務(wù)產(chǎn)生的數(shù)據(jù),又或是從命令行傳遞來的參數(shù),也可能你正在運(yùn)行單元測試,而你并不想改變真正的文件系統(tǒng)。
來看看StringIO對象,它們的行為很像其他I/O對象,不同的是它們讀寫的是字符串而不是文件,如果你為讀而打開一個(gè)StringIO對象,你需要提供一個(gè)字符串給它,在此StringIO對象上進(jìn)行的所有讀操作都會從該字符串中讀。同樣,當(dāng)你想向StringIO對象寫入時(shí),你要傳遞一個(gè)待填充的字符串。
require 'stringio'
ip = StringIO.new("now is \nthe time\nto learn\n Ruby!")
op = StringIO.new("", "w")
ip.each_line do |line|
op.puts line.reverse
end
op.string
10.4 談?wù)劸W(wǎng)絡(luò)(Talking to Networks)
Ruby善于處理網(wǎng)絡(luò)協(xié)議,無論底層協(xié)議還是高層協(xié)議。
對那些需要處理網(wǎng)絡(luò)層的人來說,Ruby的套接字庫提供了一組類,這些類讓你可以訪問TCP、UDP、SOCK、Unix域字節(jié),以及在你的體系結(jié)果上支持的任意其他套接字節(jié)類型。
庫中還提供了輔助類,這使得寫服務(wù)器程序更容易,下面這個(gè)簡答的例子使用finger協(xié)議獲取本機(jī)機(jī)器上用戶”mysql“的信息。
require 'socket'
client = TCPSocket.open('127.0.0.1', 'finger')
client.send("mysql\n", 0)
puts client.readlines
client.close
輸出結(jié)果:
Login:mysql
Name:
Directory:/var/empty
Shell:
No Mail
No Plan
在較高的層次上,lib/net庫提供了一組模塊來處理應(yīng)用層協(xié)議(目前支持FTP,HTTP,POP,SMTP和telnet)。
下面的程序?qū)⒘谐鯬rogamatic和Programmer主頁上顯示的所有圖片。
require 'net/http'
h = Net::HTTP.new('www', 80)
response = h.get('/index.html', nil)
if response.message == "ok"
puts response.body.scan(/<img src="(.*?)"/m).uniq
end
輸出結(jié)果:
images/title_main.gif
盡管這個(gè)例子簡單的令人著迷,但是還有很大的空間,特別是,它還沒有做任何錯誤處理,它需要報(bào)告”Not Found“錯誤(聲名狼藉的404),還需要能處理重定向(當(dāng)web服務(wù)器為客戶端請求的頁面給出一個(gè)替代地址時(shí))。
我們還可以更高層進(jìn)行處理,通過裝載open-uri庫到程序中,方法Kernel.open立刻就可以識別文件名中的http://和ftp://等URL。不僅如此,它還自動處理重定向。
require 'open-uri'
open('http://www.pragmaticprogrammer.com') do |f|
puts f.read.scan(/img src="(.*?)"/m).uniq
end
第11章 線程和進(jìn)程(Threads and Processes)
Ruby給了你兩種基本的方式去組織程序,這樣你可以”同時(shí)“運(yùn)行程序的不同部分。使用多線程可以在程序內(nèi)部把相互協(xié)作的任務(wù)分開,或者可以使用多進(jìn)程將任務(wù)分解到不同的程序。
11.1 多線程(Multithreading)
通常同時(shí)做兩件事最簡單的方式是使用Ruby線程,這些線程完全在進(jìn)程內(nèi)部,并在Ruby解釋器內(nèi)部實(shí)現(xiàn),這使得Ruby線程是徹底可移植的——它們不依賴于操作系統(tǒng)。當(dāng)然,同時(shí)也無法獲得本地線程(native thread)所帶來的某些益處。這指的是什么?
你可能體驗(yàn)過線程餓死(低優(yōu)先級的線程得不到機(jī)會運(yùn)行),如果把線程搞死鎖了,整個(gè)進(jìn)程可能被折磨的宕掉(halt)。如果一個(gè)線程碰巧進(jìn)行了一個(gè)需要長時(shí)間完成的操作系統(tǒng)調(diào)用,所有線程會掛起直到解釋器重新得到控制為止。最后,如果機(jī)器有多個(gè)處理器,則Ruby線程利用不了這個(gè)事實(shí)——因?yàn)樗鼈冞\(yùn)行在一個(gè)進(jìn)程內(nèi)、并且是單獨(dú)一個(gè)的本地線程內(nèi),它們被約束每次只能運(yùn)行在一個(gè)處理器上。
所有這一切聽起來挺嚇人的,實(shí)際上,在很多情況下使用線程的益處遠(yuǎn)遠(yuǎn)超出了可能會出現(xiàn)的任何潛在困難,Ruby線程是一種有效的輕量級的方法,他可以使代碼達(dá)到并行化。只需要理解這些底層的實(shí)現(xiàn)問題并依次進(jìn)行設(shè)計(jì)!
11.1.1創(chuàng)建Ruby線程(Creating Ruby Threads)
創(chuàng)建新的線程非常直接。下面這段代碼是一個(gè)簡單的例子,它并行地下載了一組網(wǎng)頁,對要求下載每個(gè)的URl,這段代碼需創(chuàng)建一個(gè)單獨(dú)的線程去處理這個(gè)HTTP事務(wù)。
require 'net/http'
pages = %w(www.google.com www.baidu.com)
threads = []
for page_to_fetch in pages
threads << Thread.new(page_to_fetch) do |url|
h = Net::HTTP.new(url, 80)
puts "Fetching: #{url}"
resp = h.get('/', nil)
puts "Got #{url}: #{resp.message}"
end
end
threads.each {|thr| thr.join}
輸出結(jié)果:
Fetching:www.rubycentral.com
Fetching:
讓我們更仔細(xì)地看看這段代碼,因?yàn)槠陂g發(fā)生了一些微妙的事情。
使用Thread.new調(diào)用來創(chuàng)建新線程,為它提供一個(gè)含有將要在新線程中運(yùn)行的block。在這個(gè)例子中,block使用net/http庫取回我們指定的每個(gè)網(wǎng)站的主頁。我們跟蹤信息清楚地的顯示了這些獲取正在并行地進(jìn)行。
創(chuàng)建線程時(shí),把所需的URL作為參數(shù)傳遞給Thread.new。這個(gè)參數(shù)作為url變量被傳遞給block。我們?yōu)槭裁催@么做,而不是簡答地在block里直接使用page_to_fetch變量的值呢?
線程共享在它開始執(zhí)行時(shí)就已經(jīng)存在的所有全局變量、實(shí)例變量和局部變量中。就像任何一個(gè)有兒時(shí)兄弟的人可能告訴你的那樣,分享并不總是一件好事情,在這個(gè)例子中,所有3個(gè)線程會共享page_to_fetch變量,當(dāng)?shù)匾粋€(gè)線程開始運(yùn)行時(shí),page_to_fetch被設(shè)置為”www.rubycentralc.om“,同時(shí)創(chuàng)建線程的循環(huán)仍然運(yùn)行著,在第二個(gè)回合時(shí),page_to_fetch被設(shè)置為”slashdot.org“。如果使用page_to_fetch變量的第一個(gè)線程還沒有結(jié)束,它會突然開始使用新的值。很難發(fā)現(xiàn)這種類型的錯誤。
不過,在縣城block里創(chuàng)建的局部變量對線程來說是真正的局部變量——每個(gè)線程會有這些變量的私有備份。
在這個(gè)例子中,url變量會在每次線程創(chuàng)建時(shí)被設(shè)置,因而每個(gè)線程都有自己的網(wǎng)頁地址的備份??梢酝ㄟ^Thread.new傳遞任意個(gè)參數(shù)到block中。
操作線程
另外一個(gè)微妙的事發(fā)生在我們下載程序的最后一行,為什么要對創(chuàng)建的每個(gè)線程調(diào)用join呢?
當(dāng)Ruby程序終止時(shí),不管線程的狀態(tài)如何,所有線程都被殺死,當(dāng)然可以通過調(diào)用線程的Thread#join方法來等待特定線程的正常結(jié)束,調(diào)用join的線程會阻塞,直到指定的線程正常結(jié)束為止。
通過對每個(gè)請求者線程調(diào)用join,可以確保所有3個(gè)請求會在主程序終止之前結(jié)束,如果不想永遠(yuǎn)阻塞,可以給予join一個(gè)超時(shí)參數(shù)——如果超時(shí)在線程終止之前到期,join返回nil。join的另一個(gè)變種,Thread#value方法返回線程執(zhí)行的最后語句的值。
除了join之外,另有一些便利函數(shù)用來操作線程,使用Thread.current總是可以得到當(dāng)前線程,可以使用Thread.list得到一個(gè)所有線程的列表,返回包含所有可運(yùn)行或被停止的線程對象??梢允褂肨hread#status和Thread#alive? 去確定特定線程的狀態(tài)。
另外,可以使用Thread#priority=調(diào)整線程的優(yōu)先級。更高優(yōu)先級的線程會在低優(yōu)先級的線程前面運(yùn)行,我們稍后就會在更多地討論線程調(diào)度、停止和啟動線程。
線程變量
線程通常可以訪問在它創(chuàng)建時(shí)其作用范圍內(nèi)的任何變量,線程block里面的局部變量是線程的局部變量,它們沒有被共享。
但是如果需要線程局部變量能被別的線程訪問——包括主線程,怎么辦?Thread類提供了一種特別的設(shè)施,允許通過名字來創(chuàng)建和訪問線程局部變量??梢院唵蔚匕丫€程對象看作一個(gè)散列表,使用[]=寫入元素并使用[] 把它們讀出。
在下面的例子中,通過鍵mycount,線程把count變量的當(dāng)前值記錄到線程局部變量中,在讀取時(shí),代碼使用字符串”mycount“對線程對象進(jìn)行索引(這里存在一個(gè)鏡態(tài)條件(race condition),但是因?yàn)槲覀冞€沒有討論同步,所以閑雜只是悄悄忽略它)。
竟態(tài)條件出現(xiàn)在當(dāng)兩段或多段代碼(或硬件)都試圖訪問一些共享資源時(shí),在這里結(jié)果會隨著它們執(zhí)行的次序而改變,在這個(gè)例子中,有可能一個(gè)縣城將其mycount變量的值設(shè)置給count,但是在它有機(jī)會增加count之前,這個(gè)縣城被調(diào)度了出去,而另外一個(gè)線程重用了相同的值,這個(gè)問題可以通過對共享資源的訪問(如count變量)進(jìn)行同步解決。
count = 0
threads = [ ]
10.times do |i|
threads[i] = Thread.new do
sleep(rand(0.1))
Thread.current["mycount"] = count
count += 1
end
end
threads.each { |t| t.join; print t["mycount"], ", " }
puts "count = #{count}"
主線程等待子線程結(jié)束,然后打印出每個(gè)子線程獲得count的值。純粹是為了讓它變得更有趣些,在記錄這個(gè)值之前,我們讓每個(gè)線程都隨機(jī)等待了一段時(shí)間。
11.1.2 線程和異常(Threads and Exceptions)
如果線程引發(fā)了(raise)了未處理的異常,會發(fā)生些什么呢?依賴于abort_on_exception標(biāo)志和解釋器debug標(biāo)志的設(shè)置。
如果abort_on_exception是false,debug標(biāo)志沒有啟用(默認(rèn)條件),未處理的異常會簡單地殺死當(dāng)前線程——而所有其他線程繼續(xù)運(yùn)行,實(shí)際上,除非對所引發(fā)這個(gè)異常的線程調(diào)用了join,你甚至根本不知道這個(gè)異常存在。
在下面的例子中,線程2自爆了,同時(shí)沒有任何輸出,但是你仍然可以從其他線程看到蛛絲馬跡。
threads = [ ]
4.times do |number|
threads << Thread.new(number) do |i|
raise "Boom!" if i==2
print "#{i}\n"
end
end
threads.each {|t| t.join}
輸出結(jié)果:
0
1
3
in `block (2 levels) in irb_binding'
當(dāng)線程被join時(shí),我們可以rescue這個(gè)異常。
threads = [ ]
4.times do |number|
threads << Thread.new(number) do |i|
raise "Boom!" if i == 2
print "#{i}\n"
end
end
threads.each do |t|
begin
t.join
rescue RuntimeError => e
puts "Failed: #{e.message}"
end
end
結(jié)果:
0
1
3
Failed: Boom!
但是,設(shè)置abort_on_exception為true,或者使用-d選項(xiàng)打開debug標(biāo)志,未處理的異常會殺死所有正在運(yùn)行的線程。一旦線程2退出,不會產(chǎn)生更多的輸出。
Thread.abort_on_exception = true
threads = [ ]
4.times do |number|
threads << Thread.new(number) do |i|
raise "Boom!" if i == 2
print "#{i}\n"
end
end
threads.each {|t| t.join}
結(jié)果:
0
1
Boom!
這段代碼也說明了一個(gè)很容易犯錯的地方(gotcha)。在循環(huán)里面,線程使用print而不是puts去輸出數(shù)字,為很么呢?這是疑問,在幕后puts的工作被分為兩部分:輸出其參數(shù),然后再輸出回車換行符。
在這兩個(gè)動作之間,一個(gè)線程可能得到調(diào)度,這樣它們的輸出可能會交織在一塊。
用已經(jīng)包含回車換行符的字符串作為參數(shù)調(diào)用print則規(guī)避了這個(gè)問題。
11.2 控制線程調(diào)度器(controlling the Thread Scheduler)
在設(shè)計(jì)良好的程序中,你通常只是讓線程做它們該做的事情:多線程程序中建立時(shí)間依賴性(timing dependency)通常被認(rèn)為是糟糕的設(shè)計(jì),因?yàn)檫@會導(dǎo)致代碼復(fù)雜化的同時(shí)阻礙線程調(diào)度器優(yōu)化程序的執(zhí)行。
但是有時(shí)候需要顯式地控制線程,也許點(diǎn)唱機(jī)有一個(gè)顯示光束的線程。音樂停止時(shí)需要暫時(shí)停止它。因而這兩個(gè)線程可能處于一種經(jīng)典的生產(chǎn)者-消費(fèi)者關(guān)系中,如果生產(chǎn)者的訂單沒有完成,則消費(fèi)者必須暫停。
Thread類提供了若干種控制線程調(diào)度器的方法,調(diào)用Thread.stop停止當(dāng)前線程,調(diào)用Thread#run安排運(yùn)行特定的線程,Thread.pass把當(dāng)前線程調(diào)度出去。允許運(yùn)行特別的線程,Thread#join和Thread#value掛起調(diào)用它們的線程,直到指定的線程結(jié)束為止。
下面的程序說明了這些特性,它創(chuàng)建了t1和t2兩個(gè)子線程,每個(gè)子線程運(yùn)行Chaser類的一個(gè)實(shí)例,chaser方法count加1,但不會讓它比另外一個(gè)線程里的count值多兩個(gè)以上。
為了防止差距大于2,這個(gè)方法調(diào)用了Thread.pass,它允許線程中的chaser趕上來。
為了變的有趣些,我們一開始就讓這些線程把自己掛起來,然后隨機(jī)地啟動其中一個(gè)線程。
class Chaser
attr_reader :count
def initialize(name)
@name = name
@count = 0
end
def chase(other)
while @count < 5
while @count-other.count > 1
Thread.pass
end
@count +=1
print "#@name: #{count}\n"
end
end
end
c1 = Chaser.new("A")
c2 = Chaser.new("B")
threads = [
Thread.new { Thread.stop; c1.chase(c2) },
Thread.new { Thread.stop; c2.chase(c1) },
]
start_index = rand(2)
threads[start_index].run
threads[1-start_index].run
threads.each { |t| t.join }
但是在實(shí)際的代碼中使用這些原語(primitive)實(shí)現(xiàn)同步并不是一件容易的事情——競態(tài)條件總是等著趁機(jī)咬你一口。同時(shí)處理共享數(shù)據(jù)時(shí),競態(tài)條件總是會帶來漫長且令人沮喪的調(diào)試階段。實(shí)際上,前面例子包含了這樣的錯誤:有可能在一個(gè)線程中,count被增加了,但是在count被輸出之前,第二個(gè)線程得到調(diào)度并且輸出了count。這個(gè)輸出結(jié)果將會是亂序的。
值得慶幸的是,線程有輔助的設(shè)施——互斥的概念,使用互斥,可以實(shí)現(xiàn)許多安全的同步機(jī)制。
11.3 互斥(Mutual Exclusion)
這是最底層的阻止其他線程運(yùn)行的方法,它使用了全局的線程關(guān)鍵(thread-critical)條件,當(dāng)條件設(shè)置為true(使用Thread.critical=方法)時(shí),調(diào)度器將不會調(diào)度現(xiàn)有的線程取運(yùn)行,但是這不會阻止創(chuàng)建和運(yùn)行新線程。
某些線程操作(如停止或殺死線程,在當(dāng)前線程中睡眠和引發(fā)異常)會導(dǎo)致及時(shí)線程處于一個(gè)關(guān)鍵區(qū)域內(nèi)也會被調(diào)度出去。
直接使用Thread.critical=當(dāng)然是可能的,但是它不是很方便,實(shí)際上,我們強(qiáng)烈建議不要使用它,除非你是一個(gè)多線程變成(和喜歡長時(shí)間調(diào)試)的黑帶高手(black belt)。值得慶幸的是,Ruby有很多變通方法,馬上會看到其中一種,Monitor庫,你也許想看一下Sync庫,Mutex庫,和現(xiàn)在的thread庫總的Queue類。
11.3.1 監(jiān)視器(Monitor)
盡管這些線程原語提供了基本的同步機(jī)制,但是熟練使用它們需要很多技巧,這些年來,各種人提出各種高級別的替代方法,尤其在面向?qū)ο笙到y(tǒng)中,其中比較成功的是一個(gè)方法是監(jiān)視器(Monitor)的概念。
監(jiān)視器用同步函數(shù)對包含一些資源的對象進(jìn)行封裝,為了看看在實(shí)際如何用他們??疾煲粋€(gè)被兩個(gè)線程訪問的簡單計(jì)數(shù)器。