Go 并发编程 Goroutines,Channels,sync 包

发布于 2022-01-15 13:12:21 字数 3261 浏览 1032 评论 0

Goroutines

在 Go 语言中,每一个并发的执行单元叫作一个 goroutine。使用 go 关键字即可以创建一个 goroutine,使得我们能够并发执行一些任务:

go func() () {
  // ...
}()

goroutine是并发执行的,不会阻塞下面的操作。但如果我们使用了多个goroutine,并且想要等待这些goroutine全都运行完毕再执行下一步的操作,这时可以使用sync.WaitGroup

wg := sync.WaitGroup{}

wg.Add(1)
go func() () {
  defer wg.Done()
  // ...
}()
wg.Add(1)
go func() () {
  defer wg.Done()
  // ...
}()
...

wg.Wait() // 等待上面的goroutine都执行完毕

Channels

一个 channel 是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。一个channel有发送和接受两个主要操作

ch := make(chan int) // ch has type 'chan int',是一个不带缓存的channel

ch <- x  // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch   // a receive statement; result is discarded

for x := range ch {
  fmt.Println(x) // channel支持range操作,当channel关闭且没有值可接收时for循环退出
}

close(ch) // 关闭操作
x, ok := <-ch // 对于已经关闭的channel,ok为false

Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。

channel分为带缓存的和不带缓存的两种。一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作;反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。

通过ch := make(chan string, 3)的方式可以创建一个带缓存的channel,如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。内置的cap()可以获取channel的容量,而len()可以获取channel中有效元素的个数。

可以通过带缓存的管道来实现最大并发数控制:

var limit = make(chan int, 3)

func main() {
  for _, w := range work {
    go func() {
      limit <- 1
      w()
      <-limit
    }()
  }
  select{}
}

select语句会选择case中能够执行的语句去执行,有点类似switch,如果多个case同时就绪时,select会随机地选择一个执行

for {
  select {
  case <-ch1:
    // ...
  case x := <-ch2:
    // ...use x...
  case ch3 <- y:
    // ...
  case <-time.After(time.Second):
    // ... timeout
  default:
    // ...
    return
  }
}

需要注意的是,select中如果用了break,跳出的只是select,而不是外面的for循环

sync.Mutex

var mu sync.Mutex

func DoSomething() {
  mu.Lock() // 如果已经上锁,则这一步会阻塞直到锁被释放
  defer mu.Unlock()
  // ...
}

除此之外,还有sync.RWMutexsync.Once也提供了很不错的特性。

sync.RWMutex提供了读写锁,一般有以下几种情况:

  • 在没有写锁的情况下,读锁是无阻塞的
  • 写锁之间互斥,存在写锁,则其他写锁阻塞
  • 写锁和读锁之间互斥,存在写锁则读锁阻塞,存在读锁则写锁阻塞

使用sync.RWMutex加读写锁的方法:

  • Lock 加写锁;Unlock 释放写锁
  • RLock 加读锁;RUnlock 释放读锁

sync.Once一般用于初始化变量,好处是可以在代码任意位置用到的时候再初始化,而不一定要放在init中,并且并发场景下是线程安全的,基于sync.Once重新实现单例模式:

var (
  instance *singleton
  once   sync.Once
)

func Instance() *singleton {
  once.Do(func() {
    instance = &singleton{}
  })
  return instance
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84960 人气
更多

推荐作者

linfzu01

文章 0 评论 0

可遇━不可求

文章 0 评论 0

枕梦

文章 0 评论 0

qq_3LFa8Q

文章 0 评论 0

JP

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文