golang的一大特色是对并发的支持较好,golang的并发是经过goroutine来实现的。顾名思义,goroutine就是golang实现的协程。git
咱们说并发,能够是线程的并发,也能够是协程的并发,协程相对于线程的优势是协程比线程更轻量级,所以并发度能够更高。拿goroutine来讲,一个go进程包含数千个goroutine同时在跑。github
golang中,每一个goroutine是独立的,多个goroutine为了共同完成一个任务,须要有必定的通讯机制。golang
好比说任务T由两个协程A、B共同完成,且A、B之间存在依赖关系,协程B依赖于协程A的执行结果,也就是只有等协程A执行完以后,协程B才能开始执行。编程
golang中协程之间的通讯是经过channel来完成的。并发
能够把channel理解成一个管道(pipe),数据从管道的一端流进,从另外一端流出。channel的语义是,当咱们从管道中读数据时,读操做会一直block直到有数据流入管道;一样的,当咱们写数据到管道中时,写操做一直block直到管道中的数据被读走。ui
unbuffered channel的语义是:当咱们从管道中读数据时,读操做会一直block直到有数据流入管道;一样的,当咱们写数据到管道中时,写操做一直block直到管道中的数据被读走。spa
下面的case我想在程序退出以前在屏幕上输出hello world,为了实现这点,我使用了done这个类型为chan bool的channel变量。线程
package main
import (
"fmt"
"time"
)
// 接收 bool 的 cahnnel
func hello(done chan bool) {
fmt.Println("hello world")
time.Sleep(4 * time.Second)
done <- true
}
func main() {
//用 make 创建一個不為 nil 的 channel
done := make(chan bool)
fmt.Println("Main going to call hello go goroutine")
go hello(done)
<- done // 管道读操做一直block,直到 hello goroutine执行并往管道中写数据,注释掉此行,main goroutine会一直执行到结束,hello goroutine不会被调度
fmt.Println("Main received data")
}
复制代码
使用make建立channel的时候,除了类型以外,还能够指定另一个参数capacity,capacity指定了channel的buffer长度,这种channel称之为buffered channel,capacity默认为0。code
相似地,buffered channel的语义也很好理解:当buffer满了,继续写就会被block;当buffer空了,继续读就会被block。server
package main
import (
"fmt"
"time"
)
func write(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
fmt.Println("successfully wrote", i, "to ch")
}
close(ch)
}
func main() {
ch := make(chan int, 2)
go write(ch)
time.Sleep(2 * time.Second)
for v := range ch {
fmt.Println("read value", v,"from ch")
time.Sleep(2 * time.Second)
}
}
/* successfully wrote 0 to ch successfully wrote 1 to ch read value 0 from ch successfully wrote 2 to ch read value 1 from ch successfully wrote 3 to ch read value 2 from ch successfully wrote 4 to ch read value 3 from ch read value 4 from ch */
复制代码
mutex其实是一种锁机制,确保在任一时间点,只有一个goroutine可以进入到临界区(critical section),进而防止竞争条件(race condition)的发生。
下面的case中,启动1000个goroutine来让x自增1000次,每次运行的结果可能都不必定,x会小于等于1000。 这是由于x的自增操做不是原子的,某一时刻,goroutine1读到x的值为10,+1以后为11,可是尚未写入主存,此时发生协程切换,goroutine2开始运行,goroutine2从主存读到x依然为10,+1以后为11,接下来,goroutine1和goroutine2把个字结果写回主存(无论前后顺序),x的值更新为11,出现了两次自增操做只实现了+1的效果。(local运行)
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
x = x + 1
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, &m) // 這裡必定要用 address
}
w.Wait()
fmt.Println("final value of x", x)
}
复制代码
要解决这个问题很简单,每次执行自增操做以前先加锁,执行完以后再释放锁,以此来保证自增操做的原子性。下面的case无论运行多少次,每次运行x的值都会是1000,也就是说程序的运行结果是肯定的。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x + 1
m.Unlock()
wg.Done()
}
func main() {
var w sync.WaitGroup
var m sync.Mutex
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, &m) // 這裡必定要用 address
}
w.Wait()
fmt.Println("final value of x", x)
}
复制代码
特别的,咱们还可使用channel来实现mutex的功能。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m chan int) {
m <- 1
x = x + 1
<- m
wg.Done()
}
func main() {
var w sync.WaitGroup
m := make(chan int, 1)
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, m) // 這裡必定要用 address
}
w.Wait()
fmt.Println("final value of x", x)
}
复制代码
除了channel和mutex以外,golang还提供了WaitGroup和Select来实现并发。
WaitGroup本质上是一个counter,只有counter=1的时候才会继续下一步。通常咱们使用WaitGroup来实现语义:当一组goroutine都执行完成的时候,才继续下一步。
在下面的case中,执行一个goroutine以前counter先加1,goroutine执行完退出以前,counter减1,这就保证了只有在全部的goroutine都完成以后,才会继续执行main goroutine。
package main
import (
"fmt"
"sync"
"time"
)
func process(i int, wg *sync.WaitGroup) {
fmt.Println("started Goroutine ", i)
time.Sleep(2 * time.Second)
fmt.Printf("Goroutine %d ended\n", i)
wg.Done() // -1
}
func main() {
no := 3
var wg sync.WaitGroup
for i := 0; i < no; i++ {
wg.Add(1) // + 1
go process(i, &wg) // wg 必定要用 pointer,不然每一个 goroutine 都会有各自的 WaitGroup
}
wg.Wait() // =0 才继续下一步
fmt.Println("All go routines finished executing")
}
复制代码
select的语法跟switch的语法很是相似,用来实现以下语义:当一组协程中的全部协程都处于block时则block,当这组协程中有一个协程ready时,选择这个协程执行,当一组协程里面有多个协程ready时,随机选一个执行。
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
time.Sleep(6 * time.Second)
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
// 等待到其中一个 channel 回來,就执行,若是都有就会随机
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
复制代码
本篇介绍了golang中关于并发编程的几个关键概念。golang的好处是从golang出发能够很清楚搞清楚并发中的不少关键概念。
下篇介绍并发编程中的关键概念,它们之间的联系,以及这些关键概念在golang中的实现。