協程可以算是go語言的最大特點,也是go誕生的初衷
很多文章有這麼一句話叫做,一核有難多核圍觀,意思是針對多核CPU一個核非常忙,剩下的核確是閒置的,出現上面的原因是寫的程序是單核處理器,不是針對多核CPU的高併發程序,在這裏也記錄一些併發和並行的區別,借用看到的資料的解釋,比較形象,併發就是使用同一個鍋炒不同的菜,菜品在鍋中隨時切換,並行就是有多個鍋,每個鍋同時炒不通的菜,併發就是你拿一把刀切菜,一會切白菜,一會切蘿蔔,一會切茄子,你的這把刀就是CPU的一個核,然後併發的去切很多菜,並行就是你用多把刀同時切不同的菜
注意:併發不是並行,並行是讓不同的代碼片段同時在不同的物理處理器上執行,並行的關鍵是同時做很多事情,而併發是指同時管理很多事情,這些事情可能只做了一般就被暫停去做別的事情了在很多情況下,併發的效果比並行好,因為操作系統和硬件的總資源一般很少,但能支持系統同時做很多事情
協程實現和協程交互
func testfun() {
// do something
}
go testfun()
這種用法還是相當方便的,只需要加一個關鍵字就可以實現協程的功能,但是這種一般都是獨立的協程,如果需要協程之間相互通信,go語言也提供了多種方法,第一種就是跟C/C++一樣的加鎖
lock.Lock()
testname = "newname"
lock.Unlock()
但是go語言雖然支持這種鎖的方式進行線程之間的通信,但是go一般不用這種方式,go一般都用通道channel的形式來進行協程之間的數據交互,
創建和使用channel
channel是一個通道,隊列,那麼我們關係的應該是如果創建這個通道,將數據裝進去,從通道中提取數據,golang為他設計了一個操作符
left <- right, 當left為channel時,表示向通道中寫入數據,並且如果通道存在數據,寫入會被阻塞,所有可以簡歷一個大小為n的通道,當寫入的數量大於n時,通道才會阻塞,當right為通道時,表示從通道通提取數據
package main
import "fmt"
func main() {
simpleChan()
}
func simpleChan () {
// 申明chan類型的字符串變量
var ch chan string
ch = make(chan string)
// 向channel中發送string類型數據
go func(){
ch <- "ping"
}()
// 創建一個string類型的變量,用來保存從channel隊列提取的數據
var v string
v = <-ch
fmt.Println(v)
}
上面的例子中創建了一個通道ch,然後make了一塊內存,這個通道實現了一個類似隊列的功能,有數據寫入,裏面如果沒有被讀走,就排隊等候,上面代碼裏面的兩個操作語句就可以完成了數據入隊列(ch <- "ping"),數據出隊列(v = <-ch)的動作,這裏有個問題需要注意,channel的接收宇發送需要分別在兩個goroutine中,如果你是直接看英文的文檔,或者其他介紹的文章,可能沒有指出這個要求,他是垮協程的,如果 ch <- "ping"不用協程調用,會報錯
從上面的例子可以看到協程通過通道ch來實現數據傳遞,這個通道ch就類似枷鎖操作的變量,通道也是一種數據結構,跟使用枷鎖方式操作變量一樣,通道定義的時候也需要定下來通道的類型,定義好後不能修改
數據協程,通道channel使用實例
這個例子是我在搜索資料看到的,感覺還行,放在記錄一下,例子中主要展示的原意是有兩個幹活的worker, 然後有5個工作的job,需要這兩個人來完成5個工作,功能實現裏面還定義了五個工作完成的結構
package main
import (
"fmt"
"time"
)
func main() {
workpools()
}
func workpools() {
const numberOfJobs = 5
const numberOfWorkers - 2
jobs := make(chan int, numberOfJobs)
results := make(chan string, numberOfJobs)
// 向任務隊列寫入任務
for i := 1; i <= numberOfJobs; i++ {
jobs <- i
}
fmt.Println("佈置job後,關閉jobs channel")
chose(jobs)
// 控制並行度,每個worker函數都運行在單獨我goroutine中
for w := 1; w <= numberOfWorkers; w ++ {
go worker(w, jobs, result)
}
// 監聽results channel,只要有內容就會被取走
for i := 1; i <= numberOfJobs; i++ {
fmt.Printf("結果: %s\n", <-result)
}
}
// worker 邏輯: 一個不斷從jobs chan中取任務的循環
// 並將結果放在out channel中待取
func worker(id int, jobs <-chan int, out chan<- string) {
fmt.Printf("worker #%d 啓動\n", id)
for job := range jobs {
fmt.Printf("worker #%d 開始工作%d\n", id, job)
// sleep 模擬 [正在處理任務]
time.Sleep(time.Millisecond * 500)
fmt.Print("worker #%d 工作%d", id, job)
out <- fmt.Sprintf("worker #% 結束工作 %d\n", id, job)
}
fmt.Printf("worker #%d 退出\n", id)
}
在go語言中,協程(goroutine)是指在後台中運行的輕量級執行線程,go協程是go中實現併發的關鍵組成部分,go中提供了一個關鍵字go來創建go協程,當在函數或方法的調用之前添加這個go關鍵字,這樣就開啓了一個go的協程,該函數或方法就會在這個go的協程中運行
由於go協程相對於傳統的操作系統的中線程是非常輕量級的,由此對於一個典型的go應用來説,有數以千計的go協程併發運行的情形是非常常見的,併發可以顯著地提升應用的運行速度,並且可以幫助我們編寫關注點分離的代碼
什麼是go的協程
我們也許在理論上已經知曉go協程是如何工作的,但在代碼層面上,go協程是何須物也,其實,go協程看起來只是一個與其他眾go協程併發運行的一個簡單函數或者方法, 但是我們並不能想當然地從函數或者方法中定義來確定一個go的協程,go協程的確定還是要取決於我們如何去調用
go中提供了一個關鍵字go來讓我們創建一個go的協程,當我們在函數或方法的調用之前添加一個關鍵字go,這樣我們就開啓了一個go的協程,該函數或者方法就會在這個go協程中運行
進程
進程是應用程序的啓動實例,是系統進行資源分配和調度的基本單位,每個進程都有獨立的內存空間,不通進程通過進程間的通信方式來通信
線程
線程從屬於進程,是進程的一個實體,線程是CPU調度的基本單位,一個線程由線程ID,當前指令指針,寄存器集合和堆棧組成
小城不擁有自己的系統資源,他與同屬於同一進程的其他線程共享進程所擁有的全部資源,多個線程之間通過共享內存等線程間的通信方式來通信,線程擁有自己獨立的站和共享的堆
協程
協程可以理解為輕量級線程,一個線程可以擁有多個協程,與線程相比,協程不受操作系統調度,協程調度器按照調度策略把協程調度到線程中執行,協程調度器由應用程序runtime包提供,用户使用go關鍵字即可創建協程,這也就是go在語言層面直接支持協程的含義
協程的優勢
由於協程運行在用户態,能夠大大減少上下文切換帶來的開銷,並且協程調度器把可運行的協程逐個調度到線程中執行,同時及時把阻塞的協程調度出線程,從而有效的避免了線程的頻繁切換,達到了使用少量線程實現高併發的效果,但對於一個線程來説每一時刻只能運行一個協程
進程,線程 協程對吧
1.協程既不是進程也不是線程,一個進程可以包含多個線程,一個線程可以包含多個協程,進程,線程,協程不是同一個維度的
2.進程,線程的切換者都是操作系統,切換時機由操作系統的切換策略決定,而協程的切換者是用户,切換時間由用户自己的程序來決定
3.進程的切換內容包含頁全局目錄,內核棧,硬件上下文,切換內容保存在內存中
4.切菜的切換過程只存在於用户態,因此切換效率高
go協程是與其他函數或方法一起併發運行的函數或方法,go協程可以看做是輕量級的線程,與線程相比,穿點一個go的協程成本很小,因此在go應用中,常常會看到有數以千計的go協程併發運行
啓動一個go協程
調用函數或者方法時,如果在前面加上關鍵字go,就可以讓一個新的go協程併發地運行