go-并发

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情

Go程

Go程(goroutine)是由Go运行时管理的轻量级线程。

go f(x,y,z)
复制代码

会启动一个新的Go程并执行

f(x,y,z)
复制代码

f,x,y和z的求值发生在当前的Go程中,而f的执行发生在新的Go程中。

例子:

package main

import (
   "fmt"
   "time"
)

func say(s string) {
   for i := 0; i < 5; i++ {
      time.Sleep(100 * time.Millisecond)
      fmt.Println(s)
   }
}

func main() {
   go say("world")
   say("hello")
}
复制代码

信道

信道是带有类型的管道,你可以通过它用信道操作符<-来发送或者接收值。

ch <- v //将v发送至信道 ch
v := <-ch //从ch接收值并赋予 v
复制代码

“箭头”就是数据流的方向。

和映射与切片一样,信道在使用前必须创建:

ch :=make(chan int)
复制代码

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得Go程可以在没有显示的锁或竞态量的情况下进行同步。

例子:

package main

import "fmt"

func sum(s []int, c chan int) {
   sum := 0
   for _, v := range s {
      sum += v
   }
   //将sum发送至管道c
   c <- sum
}

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)
   // 一旦两个Go程完成了它们的计算,它就能算出最终的结果
   x, y := <-c, <-c
   fmt.Println(x, y, x+y)
}
复制代码

带缓冲的信道

信道是可以缓冲的。将缓冲长度作为第二个参数提供给make来初始化一个带缓冲的信道:

ch := make(chan int, 100)
复制代码

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

例子:

package main

import "fmt"

func main() {
   ch := make(chan int, 2)
   ch <- 1
   ch <- 2
   fmt.Println(<-ch)
   fmt.Println(<-ch)
}
复制代码

有了缓冲区就可以在同一个协程写读。

range和close

发送者可通过close关闭一个信道来表示没有需要发送的值了。接受者可以通过为接收表达式分配第二个参数来测试是否关闭;若没有值可以接收且信道被关闭,那么在执行完

v,ok := <- ch
复制代码

之后ok会被设置为false。

循环for i := range c会不断从信道接收值,直到它被关闭。

只有发送者才能关闭信道,而接受者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)

信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再需要发送的值时才有必要关闭,例如终止一个 range 循环。

例子:

package main

import "fmt"

func fibonacci(n int, c chan int) {
   x, y := 0, 1
   for i := 0; i < n; i++ {
      c <- x
      x, y = y, x+y
   }
   close(c)
}

func main() {
   c := make(chan int, 10)
   go fibonacci(cap(c), c)
   for i := range c {
      fmt.Println(i)
   }
}
复制代码

select 语句

select 语句使用一个Go程可以等待多个通信操作。

select会阻塞到某个分支可以继续执行为止,这时救护执行该分支。当多个分支都准备好时随机选择一个执行。

package main

import "fmt"

func fibonacci1(c, quit chan int) {
   x, y := 0, 1
   for {
      select {
      case c <- x:
         x, y = y, x+y
      case <-quit:
         fmt.Println("quit")
         return
      }
   }
}

func main() {
   c := make(chan int)
   quit := make(chan int)
   go func() {
      for i := 0; i < 10; i++ {
         fmt.Println(<-c)
      }
      quit <- 0
   }()
   fibonacci1(c, quit)
}
复制代码

默认选择

当select中的其他分支都没准备好时,default分支就会执行。

为了在尝试发送或者接收时不发送阻塞,可以使用default分支:

select {
case i := <-c:
    // 使用 i
default:
    // 从 c 中接收会阻塞时执行
}
复制代码

例子:

package main

import (
   "fmt"
   "time"
)

func main() {
   tick := time.Tick(100 * time.Millisecond)
   boom := time.After(500 * time.Millisecond)
   for {
      select {
      case <-tick:
         fmt.Println("tick.")
      case <-boom:
         fmt.Println("BOOM!")
      default:
         fmt.Println("  .")
         time.Sleep(50 * time.Millisecond)
      }
   }
}
复制代码

sync.Mutex

我们已经看到信道非常合适在各个Go程间进行通信。

但是如果我们并不需要通信呢?比如说,若我们只想保证每次只有一个Go程能够访问一个共享的变量,从而避免冲突?

这里涉及的概念叫做 互斥(mutualexclusion) ,我们通常使用 互斥锁(Mutex) 这一数据结构来提供这种机制。

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

  • Lock
  • Unlock

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。参见 Inc 方法。

我们也可以用 defer 语句来保证互斥锁一定会被解锁。参见 Value 方法。

例子:

package main

import (
   "fmt"
   "sync"
   "time"
)

type SafeCounter struct {
   v   map[string]int
   mux sync.Mutex
}

func (c *SafeCounter) Inc(key string) {
   c.mux.Lock()
   c.v[key]++
   c.mux.Unlock()
}

func (c *SafeCounter) Value(key string) int {
   c.mux.Lock()
   defer c.mux.Unlock()

   return c.v[key]
}

func main() {
   c := SafeCounter{v: make(map[string]int)}
   for i := 0; i < 1000; i++ {
      go c.Inc("someKey")
   }
   time.Sleep(time.Second)
   fmt.Println(c.Value("someKey"))
}
复制代码

猜你喜欢

转载自juejin.im/post/7128017689473187876
今日推荐