GOLANG中的并发

栏目: Go · 发布时间: 7年前

内容简介: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中的实现。

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

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Haskell School of Music

The Haskell School of Music

Paul Hudak、Donya Quick / Cambridge University Press / 2018-10-4 / GBP 42.99

This book teaches functional programming through creative applications in music and sound synthesis. Readers will learn the Haskell programming language and explore numerous ways to create music and d......一起来看看 《The Haskell School of Music》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换