GOLANG中的并发

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中的实现。

  1. critical section(临界区)、(race condition)竞争条件
  2. 同步原语、互斥变量、条件变量、信号量、锁

参考

猜你喜欢

转载自juejin.im/post/5cde4605f265da1bb276fd3f