[toc]
# 1. 進程和線程介紹
## 1.1 進程和線程的介紹
### 1.1.1 進程
進程是程序在操作系統中的一次執行過程,是系統進行資源分配和調度的基本單位,每個進程都有自己的獨立內存空間,不同進程通過進程間通信來通信。由于進程比較重量,占據獨立的內存,所以上下文進程間的切換開銷(棧、寄存器、虛擬內存、文件句柄等)比較大,但相對比較穩定安全
### 1.1.2 線程
線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。線程間通信主要通過共享內存,上下文切換很快,資源開銷較少,但相比進程不夠穩定容易丟失數據。
### 1.1.3 協程
協程是一種用戶態的輕量級線程,協程的調度完全由用戶控制。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操作棧則基本沒有內核切換的開銷,可以不加鎖的訪問全局變量,所以上下文的切換非常快。
### 1.1.4 進程和線程的關系
- 一個進程可以創建和銷毀多個線程,同一個進程中的多個線程可以并發執行
- 一個程序至少有一個進程,一個進程至少有一個線程
## 1.2 區別
### 1.2.1 進程多與線程比較
- <b>地址空間</b>:線程是進程內的一個執行單元,進程內至少有一個線程,它們共享進程的地址空間,而進程有自己獨立的地址空間
- <b>資源擁有</b>:進程是資源分配和擁有的單位,同一個進程內的線程共享進程的資源
- <b>基本單位</b>: 線程是CPU調度的基本單位,進程是系統進行資源分配和調度的基本單位
- <b>并發執行</b>: 二者均可并發執行
- <b>層級關系</b>:一個進程可以創建和銷毀多個線程,同一個進程中的多個線程可以并發執行
- <b>執行過程</b>:線程在執行過程中與進程還是有區別的。每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。`但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。`
<b> 為了加深理解,做個簡單的比喻:進程=火車,線程=車廂 </b>
1. 線程在進程下行進(單純的車廂無法運行)
2. 一個進程可以包含多個線程(一輛火車可以有多個車廂)
3. 不同進程間數據很難共享(一輛火車上的乘客很難換到另外一輛火車,比如站點換乘)
4. 同一進程下不同線程間數據容易共享(A車廂換到B車廂很容易)
5. 進程要比線程消耗更多的計算機資源(采用多列火車相比多個車廂更耗資源)
6. 進程間不會相互影響,一個線程掛掉將導致整個進程掛掉(一列火車不會影響到另外一列火車,但是如果一列火車上中間的一節車廂與前一節產生斷裂,將影響后面的所有車廂)
7. 進程可以拓展到多機,進程最適合多核(不同火車可以開在多個軌道上,同一火車的車廂不能在行進的不同的軌道上)
8. 進程使用的內存地址可以上鎖,即一個線程使用某些共享內存時,其他線程必須等它結束,才能使用這一塊內存。(比如火車上的洗手間)-"互斥鎖"
9. 進程使用的內存地址可以限定使用量(比如火車上的餐廳,最多只允許多少人進入,如果滿了需要在門口等,等有人出來了才能進去)-"信號量"
### 1.2.2 協程多與線程進行比較
- 一個線程可以多個協程,一個進程也可以單獨擁有多個協程,這樣python中則能使用多核CPU。
- 線程進程都是同步機制,而協程則是異步
- 協程能保留上一次調用時的狀態,每次過程重入時,就相當于進入上一次調用的狀態
### 1.2.3 其他介紹資料
- [進程與線程的一個簡單解釋(阮一峰)](http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html)
- [漫畫:什么是協程?](https://www.itcodemonkey.com/article/4620.html)
# 2. 并發和并行
## 2.1 并發
多線程程序在單核上運行,就是并發
### 2.1.1 并發的特點
- 多個任務作用在一個cpu
- 從微觀角度看,在一個時間點上,只有一個任務在執行
## 2.2 并行
多線程程序在多核上運行,就是并行
### 2.2.1 并行的特點
- 多個任務作用在多個cpu
- 從微觀角度看,在一個時間點上,有多個任務在執行
# 3. Go 協程和 Go 主線程
## 3.1 關系介紹
- 主線程是一個物理線程,直接作用在 cpu 上的。是重量級的,非常耗費 cpu 資源。
- 協程從主線程開啟的,是輕量級的線程,是邏輯態。對資源消耗相對小。
- Golang的協程機制是重要的特點,可以輕松的開啟上萬個協程。其它編程語言的并發機制是一
般基于線程的,開啟過多的線程,資源耗費大,這里就突顯 Golang 在并發上的優勢了
## 3.2 Go主進程和Go協程(goroutine)關系示意圖

## 3.3 Go協程的特點
- 有獨立的棧空間
- 共享程序堆空間
- 調度由用戶控制
- 協程是輕量級的線程
# 4. Go協程(Goroutine)的使用
Go程序中使用go關鍵字為一個函數創建一個goroutine。一個函數可以被創建多個goroutine,一個goroutine必定對應一個函數。
## 4.1 使用方式
### 4.1.1 普通函數創建goroutine
- 使用格式
```
go 函數名 ( 參數列表 )
```
- 函數名:要調用的函數名。
- 參數列表:調用函數需要傳入的參數。
><font color=red>使用go關鍵字創建goroutine時,被調用函數的返回值會被忽略。</font>如果需要在goroutine中返回數據,則需要通過通道(channel)把數據從goroutine中作為返回值傳出。
- 使用示例
```
package main
import (
"fmt"
"time"
"strconv"
)
func test() {
var i =1
for {
fmt.Println(""+strconv.Itoa(i))
time.Sleep(time.Second)
i++
}
}
func main() {
// 為一個普通函數創建goroutine
go test()
//接收用戶輸入,知道按Enter鍵時
var input string
//將用戶輸入內容寫入input變量中,并返回,整個程序終止
fmt.Scanln(&input)
}
```
輸出:
```
1
2
3
4
5
6
...
exit
```
> <font color=red>所有goroutine在mian()函數結束時會一同結束</font>
### 4.1.2 匿名函數創建goroutine
`go關鍵字后也可以為匿名函數或閉包啟動goroutine`
- 使用格式
`使用匿名函數或閉包創建goroutine時,除了將函數定義部分寫在go的后面之外,還需要加上匿名函數的調用參數`
```
go func( 參數列表 ){
函數體
}( 調用參數列表 )
```
- 參數列表:函數體內的參數變量列表。
- 函數體:匿名函數的代碼。
- 調用參數列表:啟動goroutine時,需要向匿名函數傳遞的調用參數
- 使用示例
```
package main
import (
"fmt"
"time"
)
func main() {
go func() {
var i int
for {
fmt.Println("計時器:", i)
time.Sleep(time.Second)
i++
}
}()
var input string
fmt.Scanln(&input)
}
```
## 4.2 并發運行性能調整
### 4.2.1 設置運行的 cpu 數
**為了充分了利用多 cpu 的優勢,在 Golang 程序中,可以通過runtime.GOMAXPROCS() 函數 設置運行的 cpu 數目**
- 使用格式
```
runtime.GOMAXPROCS( 邏輯CPU)
//邏輯CPU可以通過 runtime.NumCpu()函數獲取
```
這里的邏輯CPU數量可以有如下幾種數值:
- 邏輯CPU < 1:不修改任何數值
- 邏輯CPU = 1:單核心執行
- 邏輯CPU > 1:多核并發執行
> - Go 1.5版本之前,默認使用的是單核心執行。從Go 1.5版本開始,默認執行上面語句以便讓代碼并發執行,最大效率地利用CPU。
> - GOMAXPROCS同時也是一個環境變量,在應用程序啟動前設置環境變量也可以起到相同的作用。
# 5. 通道(Channel)
## 5.1 為什么需要 channel?
`單純地將函數并發執行是沒有意義的。函數與函數間需要交換數據才能體現并發執行函數的意義。雖然可以使用共享內存進行數據交換,但是共享內存在不同的goroutine中容易發生競態問題。為了保證數據交換的正確性,必須使用互斥量對內存進行加鎖,這種做法勢必造成性能問題。Go語言提倡使用通信的方法代替共享內存,這里通信的方法就是使用通道`
### 5.1.1 goroutine與channel的通信圖

## 5.2 通道的特性
- 通道是一種特殊的類型
- 任何時候,同時只能有一個goroutine訪問通道進行發送和獲取數據
- channle本質就是一個數據結構-隊列

- goroutine間通過通道就可以通信
- 通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規則,保證收發數據的順序
- channel 是線程安全,多個協程操作同一個channel時,不會發生資源競爭問題(競態)
## 5.3 創建通道
### 5.3.1 創建無緩沖通道
`通道是引用類型,需要使用make進行創建`
```
通道實例 := make(chan數據類型)
```
- 數據類型:通道內傳輸的元素類型。
- 通道實例:通過make創建的通道句柄。
- 使用示例
```
// 創建一個整型類型的通道
intch := make(chan int)
// 創建一個空接口類型的通道,可以存放任意格式
interfacech := make(chan interface{})
// 創建Mystruct指針類型的通道,可以存放*Equip
type Mystruct struct{
/* 一些字段 */
...
}
structch := make(chan *Mystruct)
```
### 5.3.2 創建有緩沖通道
`在無緩沖通道的基礎上,為通道增加一個有限大小的存儲空間形成帶緩沖通道。帶緩沖通道在發送時無需等待接收方接收即可完成發送過程,并且不會發生阻塞,只有當存儲空間滿時才會發生阻塞。同理,如果緩沖通道中有數據,接收時將不會發生阻塞,直到通道中沒有數據可讀時,通道將會再度阻塞。`
- 聲明格式
```
通道實例 := make(chan通道類型, 緩沖大小)
```
- 通道類型:和無緩沖通道用法一致,影響通道發送和接收的數據類型。
- 緩沖大小:決定通道最多可以保存的元素數量。
- 通道實例:被創建出的通道實例。
- 使用示例
```
func main() {
//創建可以存放 3 map 類型通道
intCh := make(chan int, 3)
//數據發送到通道中
intCh <- 34
intCh <- 20
intCh <- 10
/*
注意: 當我們給管寫入數據時,不能超過其容量,
否則報錯:fatal error: all goroutines are asleep - deadlock!
*/
//intCh <- 1
}
```
### 5.3.3 帶緩沖通道阻塞條件
`帶緩沖通道在很多特性上和無緩沖通道是類似的。無緩沖通道可以看作是長度永遠為0的帶緩沖通道。因此根據這個特性,帶緩沖通道在下面列舉的情況下依然會發生阻塞`
- 帶緩沖通道被填滿時,嘗試再次發送數據時發生阻塞。
- 帶緩沖通道為空時,嘗試接收數據時發生阻塞。
### 5.3.4 為什么對通道要限制長度?
>我們知道通道(channel)是在兩個goroutine間通信的橋梁。使用goroutine的代碼必然有一方提供數據,一方消費數據。當提供數據一方的數據供給速度大于消費方的數據處理速度時,如果通道不限制長度,那么內存將不斷膨脹直到應用崩潰。因此,限制通道的長度有利于約束數據提供方的供給速度,供給數據量必須在消費方處理量+通道長度的范圍內,才能正常地處理數據。
### 5.3.5 單向通道聲明
`只能發送的通道類型為: chan <- x,只能接收的通道類型為: x <- chan`
```
ch := make(chan int)
// 聲明一個只能發送的通道類型,并賦值為ch
var ch Send Only chan<- int = ch
//聲明一個只能接收的通道類型,并賦值為ch
var ch Recv Only <-chan int = ch
```
## 5.4 發送數據
`通道創建后,就可以使用特殊的操作符“<-”,向通道進行發送或者從通道接收數據。`
### 5.4.1 使用方法
```
func main() {
//創建可以存放 3 map 類型通道
intCh := make(chan int, 3)
//數據發送到通道中
intCh <- 34
intCh <- 20
intCh <- 10
/*
注意: 當我們給管寫入數據時,不能超過其容量,
否則報錯:fatal error: all goroutines are asleep - deadlock!
*/
//intCh <- 1
}
```
## 5.5 接收數據
### 5.5.1 阻塞接收數據
`阻塞模式接收數據時,將接收變量作為“<-”操作符的左值,格式如下:`
```
data := <-ch
```
> 執行該語句時將會阻塞,直到接收到數據并賦值給data變量
### 5.5.2 非阻塞接收數據
`使用非阻塞方式從通道接收數據時,語句不會發生阻塞,格式如下:`
```
data, ok := <-ch
```
- data:表示接收到的數據。未接收到數據時,data為通道類型的零值。
- ok:表示是否接收到數據。
>非阻塞的通道接收方法可能造成高的CPU占用,因此使用非常少。如果需要實現接收超時檢測,可以配合select和計時器channel進行。
### 5.5.3 接收任意數據,忽略接收的數據
`阻塞接收數據后,忽略從通道返回的數據,格式如下:`
```
<-ch
```
> 執行該語句時將會發生阻塞,直到接收到數據,但接收到的數據會被忽略。<font color=ffb800>這個方式實際上只是通過通道在goroutine間阻塞收發實現并發同步。</font>
- 使用示例
```
func main() {
intCh := make(chan int)
start := time.Now()
testNum := 10
go func() {
fmt.Println("start goroutine....")
for i := 0; i < 3; i++ {
testNum += i
time.Sleep(time.Second)
}
//數據寫入通道
intCh <- testNum
fmt.Println("end goroutine....")
}()
//等待匿名函數執行完成
fmt.Println("wait goroutine...")
//執行該語句時將會發生阻塞,直到接收到數據,但接收到的數據會被忽略
<-intCh
diff := time.Now().Sub(start)
fmt.Printf("testNum: %d\n", testNum)
fmt.Println("耗時: ", diff)
}
```
## 5.6 channel 使用的注意事項
- channle的容量放滿后,就不能再放入了
- channel中只能存放指定的數據類型
- 空接口類型的通道能接收任意參數
```
interfaceCh := make(chan interface{}, 4)
//發送字符串
interfaceCh <- "hello"
//發送整型
interfaceCh <- 100
//發送切片
interfaceCh <- []int{1, 2, 3, 4}
/*
range函數遍歷每個從通道接收到的數據,因為queue再發送完3個數據之后就關閉了通道,所以這里我們range函數在接收到3個數據之后就結束了。
如果上面的queue通道不關閉,那么range函數就不會結束,從而在接收第4個數據的時候就阻塞了。
*/
close(interfaceCh)
for data := range interfaceCh {
fmt.Println(data)
}
```
- 發送將持續阻塞直到數據被接收
- 通過range函數遍歷通道接收數據時,要再發送完數據到通道后,用Close()函數顯示的關閉通道,否則range函數就不會結束
- 通道一次只能接收一個數據元素
## 5.7 使用select多路復用
`在使用通道時,想同時接收多個通道的數據是一件困難的事情。通道在接收數據時,如果沒有數據可以接收將會發生阻塞。`
雖然可以使用如下模式進行遍歷,但運行性能會非常差。
```
//運行性能會非常差
for{
// 嘗試接收ch1通道
data, ok := <-ch1
// 嘗試接收ch2通道
data, ok := <-ch2
// 接收后續通道
…
}
```
`Go語言中提供了select關鍵字,可以同時響應多個通道的操作。select的每個case都會對應一個通道的收發過程。當收發完成時,就會觸發case中響應的語句。多個操作在每次select中挑選一個進行響應。`
### 5.7.1 聲明格式
格式如下:
```
select{
case 操作1:
//響應操作1
case 操作2:
//響應操作2
…
default:
//沒有操作情況
}
```
- case <- ch: 代表接收任意數據
- case d:= <-ch: 接收變量
- case ch <- 120: 發送數據到通道
### 5.7.2 使用示例
```
//生成通道int ch
intCh := make(chan int, 10)
for i := 1; i < 10; i++ {
intCh <- i
}
//生成通道string ch
strCh := make(chan string, 10)
for i := 1; i < 10; i++ {
strCh <- "String:" + fmt.Sprintf("%d", i)
}
/*
傳統的方法在遍歷管道時,如果不關閉會阻塞,從而導致deadlock,在實際開發中,可能我們不好確定什么關閉該管道,
可以使用 select 方式可以解決
*/
//使用select
for {
select {
case v := <-intCh:
fmt.Println("intCH: ", v)
case v := <-strCh:
fmt.Println("strCh: ", v)
default:
fmt.Println("Noting!")
return
}
}
```