6-1. 并发与通道
并发基础
1.并发,并行
并发和并行都可以处理“多任务”,二者的主要区别在于是否是“同时进行”多个的任务。
-
并发:交替做不同事情的能力,不同的代码块交替执行
-
并行:同时做不同事情的能力,不同的代码块同时执行
1 2 3
#帮助理解 并发:老师甲先给学生A去讲思路,A听懂了自己书写过程并且检查,而甲老师在这期间直接去给B讲思路,讲完思路再去给C讲思路,让B自己整理步骤。这样老师就没有空着,一直在做事情,很快就完成了三个任务。与顺序执行不同的是,顺序执行,老师讲完思路之后学生在写步骤,这在这期间,老师是完全空着的,没做事的,所以效率低下。 并行:直接让三个老师甲、乙、丙三个老师“同时”给三个学生辅导作业,也完成的很快。
2.同步,异步
同步和异步关注的是--消息通信机制--同步与异步是针对应用程序与内核的交互而言的
- 同步:就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
- 调用者主动等待这个调用的结果。
- 异步:在异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者.
- 调用者不会等待这个调用的结果。
|
|
3.阻塞,非阻塞
同步异步是个操作方式,阻塞非阻塞是线程的一种状态。涉及到CPU线程调度
- 阻塞:调用结果返回前,线程挂起.
- 非阻塞:调用不会阻塞线程,而且立即返回.
|
|
4.进程、线程、协程
- 进程,计算机中资源分配的最小单元。 高计算的时候使用多进程
- 线程,计算机中被cpu调度的最小单元。
- 协程,又称为“微线程”,与进程、线程不同,进程线程是计算机中真实存在,协程是程序员级别人为创造出来的,本质上通过一个线程实现并发的操作。 io多的时候使用线程
|
|
CSP 并发模型
- CSP(Communication Sequential Process 通讯顺序进程 ) 模型
- CSP 并发模型不关注发送消息的实体,而只关注发送消息时使用的通信管道; 换句话说就是:CSP模型提倡通过通信来实现内存共享, 而不是通过内存共享而实现通信
- golang并发基于CSP并发模型, channal 类型的引入就是CSP模型的体现
goroutine 并发
-
golang并发优势:
- golang在代码底层实现了并发, 开发者不用再担心并发的底层逻辑和内存管理, 只需要担心业务逻辑即可
- golang通过goroutine 实现协程并发编程, 在底层实现了内存共享, 比线程更加易用高效.
-
每一个并发的执行单元都是一个goroutine; 通过使用go关键字实现并发; 一旦使用go关键字, 就不能使用函数的返回值来和主进程进行数据交换, 只能使用channel进行数据交换
-
当一个程序启动时, 其主函数就在一个单独的goroutine 中运行,当main函数后面没有代码逻辑时main函数就会停止, 而所有goroutine在main函数结束时会一并结束! 终止goroutine的最好方法是在goroutine内部结束goroutine
1. runtime包
- runtime包是一个小型的任务调度器, 可以高效的将CPU资源分配给每一个任务
1.1 Gosched
-
runtime.GOsched() 方法会将当前任务单元放弃处理器, 让其他Go协程运行; 等到其他goroutine结束后就会重启改任务单元;
-
一版goroutine出现以下几种情况, goroutine就会发生调度
-
syscall
-
C函数调用(本质和sysycall类似)
-
主动调用runtime.Gosched() 方法
-
某个goroutine调用时间超过100ms, 并且这个goroutine调用了非内联函数
内联函数是指当编译器发现某段代码在调用一个内联函数时, 他不是去调用函数而是将该函数的代码整段插入到当前位置, 省去了调用过程, 加快程序运行速度.
-
2.2 Goexit
- runtime.Goexit() 终止调用他的携程, 但是不会影响其他协程, 并在终止之前会调用defer的函数
- main函数调用此方法main函数结束,程序崩溃
3.3 GOMAXPROCS
-
runtime.GOMAXPROCS(n int)函数 可以设置程序在运行中所使用的CPU函数
-
go语言程序默认会使用最大CPU数进行计算:
- runtime.GOMAXPROCS(n int)设置可同时执行的最大CPU个数,并返回先前的设置. 若n<1, 不会改变当前设置, 本地机器的CPU个数可以通过NumCPU查询
2. Sync.WaitGroup
-
等待goroutine结束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
package main import ( "sync" ) type httpPkg struct{} func (httpPkg) Get(url string) {} var http httpPkg func main() { var wg sync.WaitGroup var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } for _, url := range urls { // waitgroup计数. wg.Add(1) // Launch a goroutine to fetch the URL. go func(url string) { // 函数完事之后告诉wg结束 defer wg.Done() // Fetch the URL. http.Get(url) }(url) } // 等待所有的goroutine结束. wg.Wait() }
channel 通道
特点: 空读写阻塞,写关闭异常,读关闭空零
- channel是一种特殊的类型, 与map类似; channel可以使用make创建的底层数据结构的引用
- 用于多个goroutine的通信, 内部实现了同步, 保证数据安全
1. channel类型
-
创建channal类型
-
1 2 3 4
var 通道变量 chan 通道类型 make(chan Type) //等价于 make(chan Type ,0) 无缓冲阻塞通道 make(chan Type, capacity) // 有缓冲通道
-
channal的零值是nil
-
当capacity 的值为0时, channal 是无缓冲阻塞读写, 当capacity 的值大于0时 ,channal是有缓冲物阻塞读写
-
channal使用
<-
来接收和发送数据k v channal <- value 发送value值到channal <-channal 接收并将其丢弃 x:= <- channal 从channal中接收数据赋值给x x, ok:= <- channal 同上,并检查通道是否关闭, 将此状态赋值给ok, 开启是true, 关闭是false
-
-
默认情况下, channal是阻塞的, 除非接收端和发送端同时准备好才能完成发送和接收操作
2. 缓冲机制
-
通道可以分为有缓冲通道和无缓冲通道;
-
无缓冲通道在接收之前没有能力保存任何值的通道
-
无缓冲阻塞, 要求接收方和发送发同时准备好 , 才能完成接收和发送的动作,否则会导致先接收或者发送的goroutine 阻塞等待
-
接收和发送是同步的, 谁也离不开谁
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
package main import "fmt" func main() { var cha = make(chan int) go func() { for i := 0; i < 3; i++ { cha <- i } }() for { fmt.Println(<-cha) // 阻塞 } }
-
-
有缓冲通道在接收之前就能存储一个或多个值得通道
- 有缓冲通道不会强制要求goroutine之间必须同时完成接收和发送;
- 只有在通道中没有要接收的值得时候接收端才会阻塞; 只有在通道中没有可用缓冲区容纳发送的值时, 发送端就会阻塞
3. close和range
-
当发送者知道没有更多的数据发送时, 让接收者知道没有更多的数据可以接收,可以让接收者停止不必要的等待; 可以通过close和range实现
-
close关闭通道时注意:
- channal不能和文件一样去经常关闭,除非你确定没有数据需要传输; 或者想显示的关闭range()之类的才回去关闭channal;
- 关闭channal之后, 无法再次向channal发送数据
- 关闭channal之后,可以继续从channal接收数据
- 对于nil的channal, 无论接收和发送都会被阻塞
-
range
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
package main import "fmt" func main() { var cha = make(chan int) go func() { for i := 0; i < 3; i++ { cha <- i } close(cha) }() for data:= range cha{ fmt.Println(data) } }
4. 单向channal
-
channal默认是双向的
-
定义单向channal
1 2 3
var cha1 chan int //双向通道 var cha1 chan<- int // 单向接收通道 var <-chan int // 单向发送通道
-
可以将双向channal隐式转换为单向通道, 但是不能将单向channal转为双向channal
-
单向channal的应用: 定时器
1 2 3 4 5 6 7 8 9 10 11 12 13 14
package main import ( "fmt" "time" ) func main() { ticker := time.NewTicker(time.Second) for { <- ticker.C fmt.Println("loop") } }
5. select
-
Select 关键字监听channal的数据流动,select 的用法和switch非常相似
-
select有较多的限制;其中最大的限制就是每一个case语句中都要是一个I/O操作
1 2 3 4 5 6 7 8
select{ case cha<-: // do case <-cha: // do default: // do }
-
select 阻塞; 满足条件时会从满足条件中的可执行语句中随机选择一个执行,没有执行default;
-
为了避免长时间阻塞; 使用time.After()执行超时操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
package main import ( "fmt" "time" ) func main() { ch := make(chan int) done := make(chan bool) go func() { for{ select { case val:= <-ch: fmt.Println(val) case <- time.After(time.Second*3): fmt.Println("已超时!") done <- true } } }() for i :=0; i<10 ;i++{ ch <-i } <-done fmt.Println("程序终止") } /** 0 1 2 3 4 5 6 7 8 9 已超时! 程序终止 */

