這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
我們為什麼需要並發程式?
- 資源使用率:從整個程式的執行角度來看,程式執行時可以看作是對輸入的資料進行計算處理然後輸出到特定的裝置中。如果這條流程線完全是串列執行的話,當其中的一個環節正在執行的時候其他環節就不能工作。這就意味著一旦輸入阻塞,即IO等待讀入資料那麼已讀入的資料也不能得到處理,已處理的資料也不能輸出,這就造成了CPU的閑置。而如果這三個步驟可以並發執行的話即使IO在等待輸入CPU仍然可以對已在記憶體中的資料做計算處理,結果也可以正常輸出。這就提高了CPU的利用率,不會因為輸入輸出的阻塞導致CPU的計算能力被浪費。
- 時間:很多任務彼此之間並沒有什麼關聯,當有充分的資源可以使用時,它們可以同時被執行,與串列地執行任務相比,這樣既可以充分利用現有資源,提高資源的利用率,同時又可以減少任務完成的總時間,可以節省出更多的時間處理接下來的任務。
- 公平性:對於同優先順序的任務來說,它們應該能夠受到電腦資源的同等待遇。如果是串列執行的話就意味著有先有後,這就造成了任務處理的不公平性,並發就可以很好地解決這個問題,它們或是同時在不同的CPU上執行,或是在單個CPU上交替執行,保證了任務應該享有的公平性。
- 簡便性:當有多種類型的任務執行時,為每種任務單獨編寫程式比編寫混雜在一起的所有任務的處理常式要簡單的多。試想當我們在處理多種事情時,把每種任務都分配給單獨的人員比起把每種任務都平均分配然後讓相應人員都處理所有種類的任務相比,效率肯定要高的多。一方面是因為一直做一件事會做的越來越熟,更重要的是可以專心做一件事而不用受到其他事情的幹擾,這一點想必已經工作的朋友一定深有體會。
所有的進階編程語言在單核心的機器上運行。Go是現代編程語言,它能夠是我們充分利用機器的所有核心。
- 什麼時候適合使用並發編程呢?
任何事情的優勢就決定了它在什麼情況下適用。正如上面”為什麼要使用並發編程”中所說,並發編程最大的優勢就是提高程式的運行速度和資源使用率,而如果串列執行的程式在這兩方面並不受到限制的話就沒有必要使用並發編程了,而如果在這兩方面遇到了瓶頸需要有所突破或者你就是個該死的極致效能追求者的話,就要考慮你的程式情境是不是符合下面這些並發編程的用武之地了:
- 任務會阻塞線程,導致之後的代碼不能執行:比如一邊從檔案中讀取,一邊進行大量計算的情況
- 任務執行時間過長,可以劃分為分工明確的子任務:比如分段下載
- 任務間斷性執行:比如上報crash,日誌列印
- 任務本身需要協作執行:比如生產者消費者問題
- ……
- 在實際的並發編程時都需要注意什麼呢?
上面所說的並發編程的風險大概就是並發編程的注意點,更具體一點,從並發的編程角度來看,我個人更願意將並發編程分為三部分:
- 多線程的並發執行:這是並發編程的核心,研究的是如何保證任務在不同線程中並發執行從而提高程式的運行速度
- 線程間的通訊:線程的執行雖然是並發的,但是他們所執行的任務並不一定是獨立的,它研究的就是如何?任務所線上程之間的高效、可靠的通訊
- 線程間對共用狀態的同步與互斥:線程之間會共用一些對象,我們稱之為狀態,當多線程同時讀寫某個共用狀態時可能會因不恰當的執行時序而造成程式邏輯的混亂,如何保證共用狀態的互斥(即保證任意時刻某個共用狀態只能由單個線程訪問)和同步(當前線程的值都是上一線程執行完後的最新的值)之後的文章也是圍繞這三方面來展開的,其中多線程的並發執行是基礎,畢竟如果沒有並發,也就談不上線程間通訊和共用狀態了。然而就多線程並發執行單方面僅僅是解決效能的問題,而如果沒有線程間通訊和對共用狀態的保護,恐怕連最最基本的正確性都不能保證了。因此,這三方面對於並發編程來說缺一不可,任何一項的短板都不能讓我們成功編寫優秀的並發程式。
引入依賴包
package main import ( "fmt" "io/ioutil" "os" "net/http" "time" )
12345678 |
package main import ( "fmt" "io/ioutil" "os" "net/http" "time" ) |
- “fmt” is for I/O
- “io/ioutil” is for reading the HTTP response body
- “os” is for accesing command line arguments
- “time” is for printing time data
- “net/http” is for making HTTP requests
我們在寫一個 叫做 MakeRequest 的 function 用來做http的並發請求
func MakeRequest(url string, ch chan<- string) {start := time.Now()resp, _ := http.Get(url)secs := time.Since(start).Seconds()body, _ := ioutil.ReadAll(resp.Body)ch <- fmt.Sprintf("%.2f elapsed with response length: %d %s", secs, len(body), url)}
1234567 |
func MakeRequest(url string, ch chan<- string) {start := time.Now()resp, _ := http.Get(url)secs := time.Since(start).Seconds()body, _ := ioutil.ReadAll(resp.Body)ch <- fmt.Sprintf("%.2f elapsed with response length: %d %s", secs, len(body), url)} |
makerequest 需要2個參數,一個string類型的url,一個channel 類型的ch,我們都知道在Go 語言中go rotines 是通過channel進行通訊的。我們這裡為每一個url請求開啟一個go routine,只要請求結束 go routine 將返回的資料寫入到channel中去。在main 方法中會吧 channel中儲存的資料列印出來。
Gofunc main() {start := time.Now()ch := make(chan string)for _, url := range os.Args[1:] {go MakeRequest(url, ch)}for range os.Args[1:] {fmt.Println(<-ch)}fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())}
1234567891011 |
func main() {start := time.Now()ch := make(chan string)for _, url := range os.Args[1:] {go MakeRequest(url, ch)}for range os.Args[1:] {fmt.Println(<-ch)}fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())} |
- Note down the start time
- Create a channel ch
- For each URL of command line arguments launch a go co-routine
- For each URL read the channel for result
- Calculate time difference between beginning and end
$ go build concurrent.go
1 |
$ go build concurrent.go |
我們在使用Python3來順序程式與go 的並發做一個比較
$ time python3 -c "import requests;print(len(requests.get('http://localhost:8000').text));print(len(requests.get('http://localhost:8000').text));print(len(requests.get('http://localhost:8000').text))"
1 |
$ time python3 -c "import requests;print(len(requests.get('http://localhost:8000').text));print(len(requests.get('http://localhost:8000').text));print(len(requests.get('http://localhost:8000').text))" |
$ time ./concurrent http://localhost:8000 http://localhost:8000 http://localhost:8000
1 |
$ time ./concurrent http://localhost:8000 http://localhost:8000 http://localhost:8000 |
Time |
Go 1.6 |
Python 3 |
real |
0.00s |
0.18s |
user |
0.00s |
0.02s |
cpu |
0.006s |
0.197s |
我們可以看到結果 Go 需要的時間比Python 少很多。
參考文獻:http://blog.narenarya.in/concurrent-http-in-go.html
Go語言的Http並發