[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)关系示意图
![](https://mrliuqh.github.io/directionsImg/go/go%E4%B8%BB%E7%BA%BF%E7%A8%8B%E5%92%8C%E5%8D%8F%E7%A8%8B.png)
## 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的通信图
![](https://mrliuqh.github.io/directionsImg/go/goroutine%E4%B8%8Echannel%E7%9A%84%E9%80%9A%E4%BF%A1.png)
## 5.2 通道的特性
- 通道是一种特殊的类型
- 任何时候,同时只能有一个goroutine访问通道进行发送和获取数据
- channle本质就是一个数据结构-队列
![](https://mrliuqh.github.io/directionsImg/go/gochannel.png)
- 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
}
}
```