由于并发程序要考虑不少的细节,以保证对共享变量的正确访问,使得并发编程在不少状况下变得很复杂。
可是Go语言在开发并发时,是比较简洁的。它经过channel来传递数据。数据竞争这个问题在golang的设计上就进行了规避了。它提倡用通讯的方式实现共享,而不要以共享方式来通讯
Go语言用2种手段来实现并发程序,goroutine和channel,其支持顺序通讯进程(communicating sequential processes),简称为CSP。CSP是一种现代的并发编程模型,在这种编程模型中,值会在不一样的运行实例(goroutine)中传递。golang
在Go语言中,每个并发的执行单元就叫作goroutine。
每一个goroutine都对应一个很是简单的模型:它是一个并发的执行函数,而且在多个并发的goroutine间,资源是共享的。goroutine很是轻量,建立的开销不多。编程
goroutine的用法:
直接在函数前加上一个关键字:go。
go func() {}bash
例子:数据结构
package main import ( "fmt" "time" ) func main() { fmt.Println("In main") go longSleep() go shortSleep() fmt.Println("sleep ") time.Sleep(10 * 1e9)//ns,符号 1e9 表示 1 乘 10 的 9 次方,e=指数 fmt.Println("the end of main") } func longSleep() { fmt.Println("longSleep begin") time.Sleep(5 * 1e9) fmt.Println("longSleep end") } func shortSleep() { fmt.Println("shortSleep begin") time.Sleep(2 * 1e9) fmt.Println("shortSleep end") }
运行结果:并发
In main sleep longSleep begin shortSleep begin shortSleep end longSleep end the end of main
main() ,longSleep() 和 shortSleep() 这3个函数都是独立的处理单元按顺序启动,而后开始并行运行。为了模拟
运算时间的损耗,咱们使用了sleep()函数,这个函数能够按照指定时间来暂停函数或协程执行。app
若是咱们不在main()函数中sleep()较长的时间,那么main() 函数结束时,其余协程运行的程序也会结束。main()程序退出,它不会等待任何其余非main协程的结束。
协程是独立的处理单元,一旦陆续启动一些协程,就没法肯定他们是何时正在开始运行的。函数
上面咱们讲到,协程都是独立运行的,他们之间没有通讯。
协程可使用共享变量来通讯,可是不建议这么作。在Go中有一种特殊的类型channle通道,能够经过它来进行goroutine之间的通讯,能够避免共享内存的坑。channel的通讯保证了同步性。
数据经过通道,同一时间只有一个协程能够访问数据,因此不会出现数据竞争,设计时就是这样的。ui
channel也是经过make进行分配的,其返回的是指向底层相关数据结构的引用。设计
var chan1 chan string chan1 = make(chan string) //or chan1 := make(chan string) //int intchan := make(chan int) //函数也能够 funcchan := chan func()
var chan2 chan string chan2 := make(chan string) chan3 := make(chan string, 0)
//在make第二个参数加上数字,就变成一个带缓冲的channel, //也是一个双向channel,既能够读也能够写 chan3 := make(chan string, 4)
//只发送的channel,在类型后面加上一个箭头 <-,只能向channel写数据 var chan4 chan <-int chan4 := make(chan <-int)
//只接收的channel,箭头放在chan前面,只能从channel读取数据 var chan4 <-chan int chan4 := make(<-chan int) //初始化
基础特性code
操做 | 值为 nil 的 channel | 被关闭的 channel | 正常的 channel |
---|---|---|---|
close | panic | panic | 成功关闭 |
c<- | 永远阻塞 | panic | 阻塞或成功发送 |
<-c | 永远阻塞 | 永远不阻塞 | 阻塞或成功接收 |
happens-before 特性
channel无缓冲区,发送方和接收方须要一一配对,否则发送方会一直阻塞,直到数据被接收方取出。
其实无缓冲区channel不论是存消息仍是取消息,都会挂起当前goroutine,除非另一端已经准备好。
无缓冲区的channel永远不会存数据,只负责数据的流通。
注意:
同步的channel不能只在一个协程中发送和接收,由于会被永远阻塞,数据不能到接收方那里。
package main import "fmt" func main() { chan1 := make(chan int) go func() { for d := range chan1 { fmt.Println(d) } }() chan1 <- 1 //发送要放在接收协程跑起来后面,由于发送后会阻塞等待接收 chan1 <- 2 chan1 <- 3 close(chan1) }
import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // send sum to c } func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c // receive from c fmt.Println(x, y, x+y) }
有缓冲区
channel建立一个缓冲区,若是缓冲区已满,发送方的主进程或者协程会被阻塞,发送方只能在接收方取走数据后才能从阻塞状态恢复;若是未满就不会阻塞;若是为空,接收方的协程会被阻塞。
上面的这种特性,好比能够控制主进程的退出,由于有时咱们碰到主协程退出了,其余的子协程尚未运行完成。
package main import ( "fmt" ) //------------- var ichan = make(chan int, 3) var str string func f() { str = "hello world" ichan <- 0 } func main() { go f() <-ichan //这里有值,下面才会运行 fmt.Println(str) }
package main import ( "fmt" ) func main() { chan1 := make(chan int, 3) quit := make(chan bool) //阻塞主进程,防止未处理完的子协程 go func() { for d := range chan1 { //若是data的缓冲区为空,这个协程会一直阻塞,除非被channel被close fmt.Println(d) } quit <- true }() chan1 <- 1 chan1 <- 2 chan1 <- 3 chan1 <- 4 chan1 <- 5 close(chan1) //用完须要关闭,不然goroutine会被死锁,由于上面用range,它是不等到信道关闭是不会结束读取的 <-quit //解除阻塞 }
上面有的例子是一个一个的取数据,其实golang还提供了for range 来读取channel中的数据。
package main import ( "fmt" "time" ) func main() { go func() { time.Sleep(1 * time.Hour) }() c := make(chan int) go func() { for i := 0; i < 10; i = i + 1 { c <- i } close(c)//若是把close(c)注释掉,程序会一直阻塞在for …… range那一行 }() for i := range c { fmt.Println(i) } fmt.Println("end!") } //range c 产生的迭代值为channel中发送的值,它会一直迭代直到channel被关闭。 //注意:上面的例子中若是把close(c)注释掉,程序会一直阻塞在for …… range那一行
select监测各个channel的数据。
若是有多个channel接收数据,select会随机选择一个case来处理。
你还能够给select加上一个default语句,若是没有case须要处理,那么就会选择default语句。
多个case状况下,若是没有default也没有case须要处理的,那么select会阻塞,只到某个case须要处理。
注意:nil channel 的操做会一直被阻塞,若是没有default的话,select会一直被阻塞。
package main import ( "fmt" ) func foo(i int) chan int { c := make(chan int) go func() { c <- i }() return c } func main() { c1, c2, c3 := foo(1), foo(2), foo(3) ichan := make(chan int) //开一个goroutine监听各个channel数据输出并收集数据到channel go func() { for {//for语句循环处理select, 若是只有一个select,那么它只会选一个case处理就结束了 select { //监听c1,c2,c3流出,并所有流入到ichan case v1 := <-c1: ichan <- v1 case v2 := <-c2: ichan <- v2 case v3 := <-c3: ichan <- v3 } } }() //阻塞主协程,取出ichan的数据 for i := 0; i < 3; i++ { fmt.Println(<-ichan) // 从打印来看咱们的数据输出并非严格的1,2,3顺序 } fmt.Println("end!") }
输出结果:
2 1 3 end!
select还有一个应用超时处理的功能。上面说到若是没有case须要处理,那么select会一直阻塞,这时候咱们就能够在一个case下定义一个超时状况,其余case没有数据处理时,到时间点了这个超时case就会处理了,就不会一直阻塞。
咱们用time.After,它返回一个类型为 <-chan time 的单向channel,在指定时间发送一个当前时间给channel
package main import ( "fmt" "time" ) func main() { chan1 := make(chan string, 1) go func() { time.Sleep(time.Second * 3) chan1 <- "res1" }() select { case res := <-chan1: //3秒以后才会有数据进入槽chan1 fmt.Println(res) case <-time.After(time.Second * 1)://定义超时状况,1秒后超时.这个超时时间比上面的case短,因此先运行这个case fmt.Println("timeout 1") } }
输出:
timeout 1
上面的特性咱们列举了close channel的状况。
close()掉了,你继续往里面写数据,会出现panic。
可是,从这个关闭的channel能够读出已发送的数据,还能够不断的读取零值。
若是是经过range读取数据,channel关闭后for循环会跳出。
经过i, ok := <-c 能够查看channel的状态,判断是零值仍是正常读取的值。
c := make(chan int, 10) close(c) i, ok := <-c fmt.Printf("%d, %t", i, ok) //0, false