GO 常见的并发操作和模式

423 阅读6分钟

概述

Go语言最吸引人的地方是它内建的并发支持,在日常的开发也经常需要用都并发。并发的经常操作有线程等待、加锁、线程间通信、线程数控制和超时控制等。

并发常见的操作

线程等待

WaitGroup 是 sync 包下的内容,用于控制协程间的同步。WaitGroup 使用场景同名字的含义一样,当我们需要等待一组协程都执行完成以后,才能做后续的处理时,就可以考虑使用。

func worker(wg *sync.WaitGroup) {
   // 线程执行完成之后,需要调用 Done 方法
   defer wg.Done()
   fmt.Println("time:", time.Now().Unix())
}

func main() {
   var wg sync.WaitGroup
   for i := 0; i < 10; i++ {
      // 新建线程时,对应的计时器加1
      wg.Add(1)
      go worker(&wg)
   }
   // 等待所有的线程执行完毕
   wg.Wait()
}

加锁

在并发编程中,对共享资源的正确访问需要精确的控制,在目前的绝大多数语言中,都是通过加锁等线程同步方案来解决这一困难问题,通过调用sync.MutexLockUnlock方法进行加锁和解锁。

var total struct {
   sync.Mutex
   value int
}

func worker(wg *sync.WaitGroup) {
   defer wg.Done()

   for i := 0; i < 100; i++ {
      total.Lock()
      total.value += 1
      total.Unlock()
   }
}

func main() {
   var wg sync.WaitGroup
   wg.Add(2)
   go worker(&wg)
   go worker(&wg)
   wg.Wait()
   fmt.Println("total:", total.value)
}

原子操作

我们通过sync.Mutex加锁和解锁来保证该语句在同一时刻只被一个线程访问。对于多线程模型的程序而言,进出临界区前后进行加锁和解锁都是必须的。如果没有锁的保护,由于多线程之间的竞争而可能会导致结果不正确。用互斥锁来保护一个数值型的共享资源,麻烦且效率低下。标准库的sync/atomic包对原子操作提供了丰富的支持。

type Config struct {
   Env string
}

func updateConfig(wg *sync.WaitGroup, config *atomic.Value) {
   defer wg.Done()
   config.Store(Config{Env: "test"})
}

func getConfig(wg *sync.WaitGroup, config *atomic.Value) {
   defer wg.Done()
   c := config.Load()
   if c == nil {
      fmt.Println("config is null.")
      return
   }
   c = c.(Config)
   fmt.Println("get config:", c)
}

func main() {
   var wg sync.WaitGroup
   var config atomic.Value
   wg.Add(4)
   go updateConfig(&wg, &config)
   go updateConfig(&wg, &config)
   go getConfig(&wg, &config)
   go getConfig(&wg, &config)
   wg.Wait()
}

线程间通信

线程间通过channel进行通信,channel包含无缓存和有缓存两种类别。无缓存和有缓存的channel的他们的区别是:无缓存channel就像是快邮递员必须要等收件员准备好才会把信放进信箱。 如果收信人不在,或没有准备好,则会一直等到收信人准备好才把信放进信箱。 而有缓存channel则是,邮递员把信放进信箱就走了,至于收信人什么时候过来取,他不会关心。

func worker(ch chan bool) {
   fmt.Println("hi, Go")
   // 向管道发送信息
   ch <- true
}

func main() {
   ch := make(chan bool)
   go worker(ch)
   // 接收管道信息
   <-ch
   fmt.Println("finish.")
}

线程数控制

可以根据控制channel的缓存大小来控制并发执行的Goroutine的最大数目

  • 定义带有缓存的 channel
  • 在启动新的线程前,发送 channel 消息
  • 在启动新的线程后,接收 channel 消息
func worker(wg *sync.WaitGroup) {
   defer wg.Done()
   fmt.Println("curr:", time.Now())
}

func main() {
   var wg sync.WaitGroup
   // 定义带有缓存的 channel
   limit := make(chan int, 3)

   for i := 0; i < 100; i++ {
      // 启动线程前,发送 channel 消息
      limit <- 1
      wg.Add(1)
      go worker(&wg)
      <-limit
      // 接收 channel 消息
   }

   wg.Wait()
   fmt.Println("finish.")
}

安全退出

第一种:通过close来关闭cancel管道向多个Goroutine广播退出的指令

func worker(wg *sync.WaitGroup, ch chan bool) {
   defer wg.Done()
   for {
      select {
      default:
         fmt.Println("working...")
      case <-ch:
         fmt.Println("stop!!!")
         return
      }
   }

}

func main() {
   ch := make(chan bool)
   var wg sync.WaitGroup

   for i:= 0; i < 10; i++ {
      wg.Add(1)
      go worker(&wg, ch)
   }

   time.Sleep(time.Second)
   close(ch)
   wg.Wait()
}

备注:当每个Goroutine收到退出指令退出时一般会进行一定的清理工作,但是退出的清理工作并不能保证被完成,可以结合sync.WaitGroup进行等待所有线程都执行完毕后再退出。

第二种:通过 context 的 cannel() 来通知后台线程退出

func worker(ctx context.Context, wg *sync.WaitGroup) error {
   defer wg.Done()

   for {
      select {
      default:
         fmt.Println("working...")
      case <-ctx.Done():
         fmt.Println("cancel!!!")
         return ctx.Err()
      }
   }
}

func main() {
   ctx, cancel := context.WithCancel(context.Background())
   var wg sync.WaitGroup
   for i := 0; i < 10; i++ {
      wg.Add(1)
      go worker(ctx, &wg)
   }

   time.Sleep(time.Second)
   cancel()

   wg.Wait()
}

超时控制

在Go1.7发布时,标准库增加了一个context包,用来简化对于处理单个请求的多个Goroutine之间与请求域的数据、超时和退出等操作,则可以用 context 来进行超时的控制。

package main

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

func worker(ctx context.Context, wg *sync.WaitGroup) error {
   defer wg.Done()

   for {
      select {
      default:
         fmt.Println("working...")
      case <-ctx.Done():
         return ctx.Err()
      }
   }
}

func main() {
   ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
   var wg sync.WaitGroup
   for i := 0; i < 10; i++ {
      wg.Add(1)
      go worker(ctx, &wg)
   }

   time.Sleep(time.Second)
   cancel()

   wg.Wait()
}

并发常见的模型

生产者消费者模式

并发编程中最常见的例子就是生产者消费者模式,该模式主要通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。简单地说,就是生产者生产一些数据,然后放到成果队列中,同时消费者从成果队列中来取这些数据。这样就让生产消费变成了异步的两个过程。当成果队列中没有数据时,消费者就进入饥饿的等待中;而当成果队列中数据已满时,生产者则面临因产品挤压导致CPU被剥夺的下岗问题。

// Producer 生产者
func Producer(factor int, out chan<- int) {
   for i := 0; ; i++ {
      out <- i * factor
   }
}

// Consumer 消费者
func Consumer(in <-chan int) {
   for v := range in {
      fmt.Println(v)
   }
}

func main() {
   ch := make(chan int, 64)

   go Producer(3, ch)
   go Producer(5, ch)
   go Consumer(ch)

   time.Sleep(5 * time.Second)
}

发布订阅模式

发布订阅(publish-and-subscribe)模型通常被简写为pub/sub模型。在这个模型中,消息生产者成为发布者(publisher),而消息消费者则成为订阅者(subscriber),生产者和消费者是M:N的关系。发布者通常不会知道、也不关心哪一个订阅者正在接收主题消息。订阅者和发布者可以在运行时动态添加,是一种松散的耦合关系,这使得系统的复杂性可以随时间的推移而增长。

package main

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

type (
   subscriber chan interface{}
   topicFunc  func(v interface{}) bool
)

// Publisher 发布者对象
type Publisher struct {
   m           sync.Mutex               // 读写锁
   buffer      int                      // 订阅队列的缓存大小
   timeout     time.Duration            // 发布超时时间
   subscribers map[subscriber]topicFunc // 订阅者信息
}

// NewPublisher 新建发布者对象
func NewPublisher(publisherTimeout time.Duration, buffer int) *Publisher {
   return &Publisher{
      timeout:     publisherTimeout,
      buffer:      buffer,
      subscribers: make(map[subscriber]topicFunc),
   }
}

// Subscribe 订阅所有事件
func (p *Publisher) Subscribe() chan interface{} {
   return p.SubscribeTopic(nil)
}

// SubscribeTopic 订阅某个 topic
func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} {
   ch := make(chan interface{}, p.buffer)
   p.m.Lock()
   p.subscribers[ch] = topic
   p.m.Unlock()
   return ch
}

// Evict 退出订阅
func (p *Publisher) Evict(sub chan interface{}) {
   p.m.Lock()
   defer p.m.Unlock()

   delete(p.subscribers, sub)
   close(sub)
}

// Publish 发布一个主题
func (p *Publisher) Publish(v interface{}) {
   p.m.Lock()
   defer p.m.Unlock()

   var wg sync.WaitGroup
   for sub, topic := range p.subscribers {
      wg.Add(1)
      go p.sendTopic(sub, topic, v, &wg)
   }
   wg.Wait()
}

func (p *Publisher) sendTopic(sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup) {
   defer wg.Done()
   if topic != nil && !topic(v) {
      return
   }

   select {
   case sub <- v:
   case <-time.After(p.timeout):
   }
}

// Close 关闭所有发布者对象
func (p *Publisher) Close() {
   p.m.Lock()
   defer p.m.Unlock()

   for sub := range p.subscribers {
      delete(p.subscribers, sub)
      close(sub)
   }
}

func main() {
   p := NewPublisher(time.Duration(10*time.Second), 10)
   defer p.Close()

   all := p.Subscribe()
   golang := p.SubscribeTopic(func(v interface{}) bool {
      if s, ok := v.(string); ok {
         return strings.Contains(s, "golang")
      }
      return false
   })

   p.Publish("hello, world")
   p.Publish("hello, golang")

   go func() {
      for msg := range all {
         fmt.Println("all:", msg)
      }
   }()
   go func() {
      for msg := range golang {
         fmt.Println("golang:", msg)
      }
   }()

   time.Sleep(3 * time.Second)
}