golang生产者消费者模型的几种并发实现

133 阅读6分钟

1.使用有缓冲通道 ch:=make(chan interface{},size)

package main

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

var (
   ProducerCnt     = 1  //生产者个数
   ConsumerCnt     = 20 //消费者goroutine个数
   ProcessDuration = 10 //任务处理耗时,单位ms
   QueueCapacity   = 10 //队列长度

   Timeout = 10 // 程序退出时间, 单位s
)

/*
1p1c, 队列长度:10,  任务耗时:10ms, 持续10s     905
1p2c, 队列长度:10,  任务耗时:10ms, 持续10s     1792
1p3c, 队列长度:10,  任务耗时:10ms, 持续10s     2715
1p3c, 队列长度:20,  任务耗时:10ms, 持续10s     2714 (修改队列长度无明显变化,因为这里保证了生产者实时填充任务)
1p20c, 队列长度:10,  任务耗时:10ms, 持续10s    17608 (基本上保证了,消费者协程增多,处理速度线性增长)

*/

type Queue struct {
   ch       chan interface{}
   capacity int
   wg       *sync.WaitGroup

   terminate chan struct{}
}

type Task struct {
   ID  int
   Msg string
}

func NewQueue(ctx context.Context, capacity int) *Queue {
   q := &Queue{
      ch:        make(chan interface{}, capacity),
      terminate: make(chan struct{}),
      capacity:  capacity,
      wg:        &sync.WaitGroup{},
   }
   return q
}

func Produce(queue *Queue, producerID int) {
   defer func() {
      fmt.Println("生产者停止退出,id::", producerID)
      queue.wg.Done()
   }()
loop:
   for i := 0; ; i++ {
      select {
      case _, ok := <-queue.terminate:
         if !ok {
            // 接收到生产者停止消息后退出for 循环
            close(queue.ch) //生产者退出后,关闭ch,从而触发后续到消费者协程退出
            break loop
         }

      default:
         task := Task{
            ID:  producerID,
            Msg: fmt.Sprintf("task %d_%d", producerID, i),
         }
         //fmt.Printf("before生产: %v\n", task)

         queue.ch <- task
         fmt.Printf("生产: %v\n", task)
      }

   }
}

func Consume(queue *Queue, consumerID int) {
   defer func() {
      fmt.Println("comsumer exit:", consumerID)
      queue.wg.Done()
   }()
   for {
      val, ok := <-queue.ch
      if !ok {
         fmt.Printf("消费者退出: %v\n", consumerID)
         return
      }
      time.Sleep(time.Millisecond * time.Duration(ProcessDuration))
      fmt.Printf("----------------id:%d, 消费: %v  \n", consumerID, val)
   }

}

func main() {
   // 创建一个可取消的上下文
   ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Timeout)*time.Second)
   defer cancel()

   queue := NewQueue(ctx, QueueCapacity)

   // 生产者
   for i := 1; i <= ProducerCnt; i++ {
      queue.wg.Add(1)
      go Produce(queue, i)
   }

   // 消费者
   for i := 1; i <= ConsumerCnt; i++ {
      queue.wg.Add(1)
      go Consume(queue, i)
   }

   <-ctx.Done()
   println("截止时间到")
   close(queue.terminate) //发送消息,通知生产者停止

   queue.wg.Wait()

   fmt.Println("程序退出:", ctx.Err())
}

2.使用条件变量 sync.Cond

package main

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

var (
   ProducerCnt     = 1  //生产者个数
   ConsumerCnt     = 10 //消费者goroutine个数
   ProcessDuration = 10 //任务处理耗时,单位ms
   QueueCapacity   = 10 //队列长度

   Timeout = 2 // 程序退出时间, 单位s
)

type Queue struct {
   data         []interface{}
   capacity     int
   lock         sync.Mutex
   notEmptyCond *sync.Cond
   notFullCond  *sync.Cond
}

type Task struct {
   ID  int
   Msg string
}

func NewQueue(capacity int) *Queue {
   q := &Queue{
      data:     make([]interface{}, 0, capacity),
      capacity: capacity,
   }
   // 条件变量需要依赖互斥锁或者读写锁
   q.notEmptyCond = sync.NewCond(&q.lock)
   q.notFullCond = sync.NewCond(&q.lock)
   return q
}

func (q *Queue) Push(val interface{}) {
   q.lock.Lock()
   defer q.lock.Unlock()

   // 注意,这里在触发条件变量检查前,要先获取到互斥锁 
   // 队列满时等待
   for len(q.data) == q.capacity {
      q.notFullCond.Wait()  //wait挂起的同时,会在内部逻辑中释放锁
   }

   q.data = append(q.data, val)
   q.notEmptyCond.Signal()  //每次加入数据后,发送非空signal,从而唤醒挂起等待notEmptyCond的消费者

}

func (q *Queue) Pop() (interface{}, error) {
   q.lock.Lock()
   defer q.lock.Unlock()

   // 已经获取到锁 
   
   // 队列空时等待, 
   for len(q.data) == 0 {
      q.notEmptyCond.Wait()   //消费者挂起同时,释放锁
   }

   val := q.data[0]
   q.data = q.data[1:]
   q.notFullCond.Signal()  // 消费完毕后,发送未满signal, 唤醒挂起在notFullCond.wait的生产者
   return val, nil
}

func Produce(queue *Queue, producerID int) {
   for i := 0; ; i++ {
      task := Task{
         ID:  producerID,
         Msg: fmt.Sprintf("task %d_%d", producerID, i),
      }
      queue.Push(task)
      fmt.Printf("生产: %v\n", task)
   }
}

func Consume(queue *Queue, consumerID int) {
   for {
      val, err := queue.Pop()
      if err != nil {
         fmt.Printf("消费者退出: %v\n", err)
         return
      }
      time.Sleep(time.Millisecond * time.Duration(ProcessDuration))
      fmt.Printf("----------------id:%d, 消费: %v  \n", consumerID, val)
   }

}

/*
p生产者 c消费者 

1p1c, 队列长度:10,  任务耗时:10ms, 持续10s     900
1p2c, 队列长度:10,  任务耗时:10ms, 持续10s     1800
1p3c, 队列长度:10,  任务耗时:10ms, 持续10s     2700
1p3c, 队列长度:20,  任务耗时:10ms, 持续10s     2700 (修改队列长度无明显变化,因为这里保证了生产者实时填充任务)
1p20c, 队列长度:10,  任务耗时:10ms, 持续10s    18000 (基本上保证了,消费者协程增多,处理速度线性增长)


基于条件变量实现的生产者消费者模型,相比直接通过channel实现的一个优势是,在处理消息是,可以做额外的处理,比如根据优先级选择出队列任务等。
如果是使用channel,只能顺序从通道内获取任务进行处理
*/

func main() {
   queue := NewQueue(QueueCapacity)
   // 创建一个可取消的上下文
   ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Timeout)*time.Second)
   defer cancel()

   // ProducerCnt决定生产者协程个数
   for i := 1; i <= ProducerCnt; i++ {
      go Produce(queue, i)
   }

   // ConsumerCnt决定消费者协程个数
   for i := 1; i <= ConsumerCnt; i++ {
      go Consume(queue, i)
   }

   // 等待上下文超时或取消
   <-ctx.Done()
   queue.notEmptyCond.Broadcast()
   fmt.Println("程序退出:", ctx.Err())
}

3.使用信号量 semephore

package main

import (
   "context"
   "fmt"
   "golang.org/x/sync/semaphore"
   "sync"
   "time"
)

var (
   ProducerCnt           = 1  //生产者个数
   ConsumerCnt           = 2  //消费者goroutine个数
   ProcessDuration       = 10 //任务处理耗时,单位ms
   QueueCapacity   int64 = 10 //队列长度

   Timeout = 10 // 程序退出时间, 单位s
)

/*
p生产者 c消费者 

1p1c, 队列长度:10,  任务耗时:10ms, 持续10s     900
1p2c, 队列长度:10,  任务耗时:10ms, 持续10s     1800
1p3c, 队列长度:10,  任务耗时:10ms, 持续10s     2700
1p3c, 队列长度:20,  任务耗时:10ms, 持续10s     2700 (修改队列长度无明显变化,因为这里保证了生产者实时填充任务)
1p20c, 队列长度:10,  任务耗时:10ms, 持续10s    18000 (基本上保证了,消费者协程增多,处理速度线性增长)


*/

type Buffer struct {
   data     []interface{}
   mutex    sync.Mutex
   empty    *semaphore.Weighted // 空槽位数量
   full     *semaphore.Weighted // 满槽位数量
   capacity int64
}

func NewBuffer(size int64) *Buffer {
   b := &Buffer{
      data:     make([]interface{}, 0, size),
      empty:    semaphore.NewWeighted(size), // 最大信号量size,初始有size个空槽位
      full:     semaphore.NewWeighted(size), // 最大信号量size,初始有0个满槽位
      capacity: size,
   }
   //这里先把有数据槽位信号量置0, 保证初始化时消费者无可消费信号
   b.full.TryAcquire(size)

   return b
}

func (b *Buffer) Push(ctx context.Context, item interface{}) error {
   // 获取空槽位
   if err := b.empty.Acquire(ctx, 1); err != nil {
      return err
   }
   //fmt.Printf("b.empty:%v", b.empty)

   b.mutex.Lock()
   b.data = append(b.data, item)
   b.mutex.Unlock()

   // 释放一个满槽位
   b.full.Release(1)
   return nil
}

func (b *Buffer) Pop(ctx context.Context) (interface{}, error) {
   // 获取满槽位
   if err := b.full.Acquire(ctx, 1); err != nil {
      return nil, err
   }

   b.mutex.Lock()
   item := b.data[0]
   b.data = b.data[1:]
   b.mutex.Unlock()

   // 释放一个空槽位
   b.empty.Release(1)
   return item, nil
}

type Task struct {
   ID  int
   Msg string
}

func Produce(ctx context.Context, queue *Buffer, producerID int) {
   for i := 0; ; i++ {
      task := Task{
         ID:  producerID,
         Msg: fmt.Sprintf("task %d_%d", producerID, i),
      }
      queue.Push(ctx, task)
      fmt.Printf("生产: %v\n", task)
   }
}

func Consume(ctx context.Context, queue *Buffer, consumerID int) {
   for {
      val, err := queue.Pop(ctx)
      if err != nil {
         fmt.Printf("消费者退出: %v\n", err)
         return
      }
      time.Sleep(time.Millisecond * time.Duration(ProcessDuration))
      fmt.Printf("----------------id:%d, 消费: %v  \n", consumerID, val)
   }

}

func main() {
   buffer := NewBuffer(QueueCapacity)
   //for i:=0; i < QueueCapacity; i++; {
   //}
   ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Timeout)*time.Second)
   defer cancel()

   //buffer.full.Acquire(ctx, QueueCapacity)

   // 生产者
   for i := 1; i <= ProducerCnt; i++ {
      go Produce(ctx, buffer, i)
   }

   for i := 1; i <= ConsumerCnt; i++ {
      go Consume(ctx, buffer, i)
   }

   <-ctx.Done()
   fmt.Println("程序退出:", ctx.Err())
}