内容简介:golang的一大特点是对并发的支持较好,golang的并发是通过goroutine来实现的。顾名思义,goroutine就是golang实现的协程。我们说并发,可以是线程的并发,也可以是协程的并发,协程相对于线程的优点是协程比线程更轻量级,因此并发度可以更高。拿goroutine来说,一个go进程包含数千个goroutine同时在跑。golang中,每个goroutine是独立的,多个goroutine为了共同完成一个任务,需要有一定的通信机制。
golang的一大特点是对并发的支持较好,golang的并发是通过goroutine来实现的。顾名思义,goroutine就是golang实现的协程。
我们说并发,可以是线程的并发,也可以是协程的并发,协程相对于线程的优点是协程比线程更轻量级,因此并发度可以更高。拿goroutine来说,一个 go 进程包含数千个goroutine同时在跑。
channel
golang中,每个goroutine是独立的,多个goroutine为了共同完成一个任务,需要有一定的通信机制。
比如说任务T由两个协程A、B共同完成,且A、B之间存在依赖关系,协程B依赖于协程A的执行结果,也就是只有等协程A执行完之后,协程B才能开始执行。
golang中协程之间的通信是通过channel来完成的。
可以把channel理解成一个管道(pipe),数据从管道的一端流进,从另一端流出。channel的语义是,当我们从管道中读数据时,读操作会一直block直到有数据流入管道;同样的,当我们写数据到管道中时,写操作一直block直到管道中的数据被读走。
unbuffered channel
unbuffered channel的语义是:当我们从管道中读数据时,读操作会一直block直到有数据流入管道;同样的,当我们写数据到管道中时,写操作一直block直到管道中的数据被读走。
下面的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") } 复制代码
buffered channel
使用make创建channel的时候,除了类型之外,还可以指定另外一个参数capacity,capacity指定了channel的buffer长度,这种channel称之为buffered channel,capacity默认为0。
类似地,buffered channel的语义也很好理解:当buffer满了,继续写就会被block;当buffer空了,继续读就会被block。
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
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) } 复制代码
WaitGroup
除了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
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中的实现。
- critical section(临界区)、(race condition)竞争条件
- 同步原语、互斥变量、条件变量、信号量、锁
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Java并发系列—并发编程基础
- [Java并发-17-并发设计模式] Immutability模式:如何利用不变性解决并发问题?
- JAVA并发编程之并发模拟工具
- Java并发系列—并发编程的挑战
- Core Java 并发:理解并发概念
- [Java并发-11] 并发容器的使用
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。