go并发编程

简介: go的GMP并发模型,让go天然支持高并发,先了解一下GMP模型吧GMP G协程,M工作线程、P处理器,M必须持有P才可以执行GP维护着一个协程G队列,P依次将G调度到M中运行if M0中G0发生系统调用,M0将释放P,冗余的M1获取P,继续执行P队列中剩余的G。(只要P不空闲就充分利用了CPU)G0系统调用结束后,如果有空闲的P,则获取P继续执行G0,否则将G0放入全局队列,M0进入缓存池睡眠。(全局队列中的G主要来自从系统调用中恢复的G)下面介绍一下编程常用的同步(synchronize)原语互斥锁 mutex rwmutex,要了解自旋和饥饿模式自旋最多4次,cpu核

go的GMP并发模型,让go天然支持高并发,先了解一下GMP模型吧

GMP G协程,M工作线程、P处理器,M必须持有P才可以执行G

P维护着一个协程G队列,P依次将G调度到M中运行

if M0中G0发生系统调用,M0将释放P,冗余的M1获取P,继续执行P队列中剩余的G。(只要P不空闲就充分利用了CPU)

G0系统调用结束后,如果有空闲的P,则获取P继续执行G0,否则将G0放入全局队列,M0进入缓存池睡眠。(全局队列中的G主要来自从系统调用中恢复的G)

下面介绍一下编程常用的同步(synchronize)原语

互斥锁 mutex rwmutex,要了解自旋和饥饿模式

自旋最多4次,cpu核数要大于1,Processor大于1

饥饿模式阻塞超过1ms,饥饿模式下不会启动自旋过程

channel go的精华所在,注意不能关闭已经关闭的channel,不能向已关闭的channel写数据,会panic

Once#

多用于实现单例模式

饿汉模式,一般是直接创建一个包级变量直接使用即可,注意既然是单例模式,就不能让他人随意创建,类型要是私有的,使用接口暴露方法,让外部获得私有变量

懒汉模式,在第一次使用时创建,这里需要注意并发安全,可以使用sync.Once来保证并发安全

type Singleton interface {
  Work() string
}
type singleton2 struct{}
func (s *singleton2) Work() string {
  return "singleton2 is working"
}
func newSingleton2() *singleton2 {
  return &singleton2{}
}
var (
  instance *singleton2
  once     sync.Once
)
// GetSingleton2 用于获取单例模式对象
func GetSingleton2() Singleton {
  once.Do(func() {
    instance = newSingleton2()
  })
  return instance
}

Pool#

sync.Pool 是 Go 语言标准库中的一个并发安全的对象池,用于缓存和重用临时对象,以提高性能。通常用于减少内存分配和垃圾回收的开销。PS:一般将变量Put回Pool中,需要将变量重置

var studentPool = sync.Pool{
  New: func() any {
    return new(Student)
  },
}
func BenchmarkUnmarshalWithPool(b *testing.B) {
  for n := 0; n < b.N; n++ {
    stu := studentPool.Get().(*Student)
    json.Unmarshal(buf, stu)
    studentPool.Put(stu)
  }
}

Cond#

Cond是条件变量,可以让一组goroutine等待某个条件的发生。当条件发生时,调用Broadcast或者Signal来通知所有等待的goroutine继续执行。

需要注意的是 sync.Cond 都要在构造的时候绑定一个 sync.Mutex。Wait() 和 Signal() 函数必须在锁保护下的临界区中执行。Wait()一般放在for循环中,因为可能会出现虚假唤醒

var mu = &sync.Mutex{}
var done = false
func read(name string, c *sync.Cond) {
  c.L.Lock()
  for !done {
    c.Wait()
  }
  log.Println(name, "reading")
  c.L.Unlock()
}
func write(name string, c *sync.Cond) {
  time.Sleep(1 * time.Second)
  c.L.Lock()
  done = true
  c.L.Unlock()
  log.Println(name, "writting finish")
  c.Broadcast()
}
func main() {
  cond := sync.NewCond(mu)
  go read("reader1", cond)
  go read("reader2", cond)
  go read("reader3", cond)
  write("writer", cond)
  time.Sleep(time.Second * 3)
}

WaitGroup#

注意Wait后不能再Add,否则会panic

// WaitGroup 完美写法
func main() {
  taskNum := 3
  ch := make(chan any)
  go func() {
    group := &sync.WaitGroup{}
    for i := 1; i <= taskNum; i++ {
      group.Add(1)
      go func(i int) {
        defer group.Done()
        ch <- i
      }(i)
    }
    // 确保所有取数据的协程都完成了工作,才关闭 ch
    group.Wait()
    close(ch)
  }()
  for i := range ch {
    log.Println("goroutine ", i)
  }
  log.Println("finish")
}

扩展:errgroup,可以捕获goroutine中的panic,只要有一个goroutine出错,就会取消所有的goroutine

atomic#

原子操作,用于解决并发问题,比如计数器,锁等

func main() {
  // 定义一个共享的计数器,使用 int64 类型
  var counter int64
  // 使用 WaitGroup 来等待所有 goroutine 完成
  var wg sync.WaitGroup
  // 启动多个 goroutine 来增加计数器的值
  for i := 0; i < 5; i++ {
    wg.Add(1)
    go func() {
      // 在每个 goroutine 中原子地增加计数器的值
      for j := 0; j < 100000; j++ {
        atomic.AddInt64(&counter, 1)
      }
      wg.Done()
    }()
  }
  // 等待所有 goroutine 完成
  wg.Wait()
  // 输出最终的计数器值
  fmt.Println("Final Counter Value:", atomic.LoadInt64(&counter))
}

context#

WithCancel#

context.WithCancel() 创建可取消的 Context 对象,即可以主动通知子协程退出。

func reqTask(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
          fmt.Println("stop", name)
return
default:
          fmt.Println(name, "send request")
          time.Sleep(1 * time.Second)
       }
    }
}
func main() {
    ctx, cancel := context.WithCancel(context.Background())
go reqTask(ctx, "worker1")
    time.Sleep(3 * time.Second)
    cancel()
    time.Sleep(3 * time.Second)
}

WithValue#

如果需要往子协程中传递参数,可以使用 context.WithValue()

type Options struct{ Interval time.Duration }
func reqTask(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
          fmt.Println("stop", name)
return
default:
          fmt.Println(name, "send request")
          op := ctx.Value("options").(*Options)
          time.Sleep(op.Interval * time.Second)
       }
    }
}
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    vCtx := context.WithValue(ctx, "options", &Options{1})
go reqTask(vCtx, "worker1")
go reqTask(vCtx, "worker2")
    time.Sleep(3 * time.Second)
    cancel()
    time.Sleep(3 * time.Second)
}

WithTimeout#

如果需要控制子协程的执行时间,可以使用 context.WithTimeout 创建具有超时通知机制的 Context 对象。

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)

WithDeadlineWithTimeout 类似,不同的是 WithDeadline 可以指定一个具体的时间点。

ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Second))

semaphore#

用于限制对一组共享资源的访问。也可以手动用channel实现

// 创建一个具有初始计数为 2 的信号量
sem := semaphore.NewWeighted(2)
// 启动多个 goroutine 模拟并发访问
for i := 1; i <= 5; i++ {
  go func(id int) {
    fmt.Printf("Goroutine %d is trying to acquire the semaphore\n", id)
    // 尝试获取信号量,如果已经达到限制,则阻塞
    err := sem.Acquire(context.Background(), 1)
    if err != nil {
      fmt.Printf("Error acquiring semaphore in goroutine %d: %v\n", id, err)
      return
    }
    fmt.Printf("Goroutine %d has acquired the semaphore\n", id)
    time.Sleep(2 * time.Second)
    // 释放信号量
    sem.Release(1)
    fmt.Printf("Goroutine %d has released the semaphore\n", id)
  }(i)
}
// 等待所有 goroutine 完成
time.Sleep(10 * time.Second)

singleflight#

golang.org/x/sync/singleflight 防止缓存击穿

var sf singleflight.Group
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
  wg.Add(1)
  go func(id int) {
    defer wg.Done()
    // 以 "key" 为参数调用 Do 方法
    val, err, _ := sf.Do("key", func() (interface{}, error) {
      fmt.Printf("Goroutine %d is performing the operation\n", id)
      time.Sleep(2 * time.Second)
      return fmt.Sprintf("Result from goroutine %d", id), nil
    })
    if err != nil {
      fmt.Printf("Goroutine %d encountered an error: %v\n", id, err)
    } else {
      fmt.Printf("Goroutine %d got result: %v\n", id, val)
    }
  }(i)
}
wg.Wait()
相关文章
|
24天前
|
安全 Go
掌握Go语言:Go语言通道,并发编程的利器与应用实例(20)
掌握Go语言:Go语言通道,并发编程的利器与应用实例(20)
|
2月前
|
并行计算 算法 安全
通过三个例子,学习 Go 语言并发编程的利器 - goroutine
通过三个例子,学习 Go 语言并发编程的利器 - goroutine
42 0
|
3月前
|
Go
通道多路复用:Go语言并发编程的黄金法则
通道多路复用:Go语言并发编程的黄金法则
36 0
|
3月前
|
安全 Go
Go新手步步为赢:并发编程通信指南
Go新手步步为赢:并发编程通信指南
25 0
|
1天前
|
程序员 Go
Golang深入浅出之-Select语句在Go并发编程中的应用
【4月更文挑战第23天】Go语言中的`select`语句是并发编程的关键,用于协调多个通道的读写。它会阻塞直到某个通道操作可行,执行对应的代码块。常见问题包括忘记初始化通道、死锁和忽视`default`分支。要解决这些问题,需确保通道初始化、避免死锁并添加`default`分支以处理无数据可用的情况。理解并妥善处理这些问题能帮助编写更高效、健壮的并发程序。结合使用`context.Context`和定时器等工具,可提升`select`的灵活性和可控性。
10 2
|
2天前
|
安全 Go 开发者
Golang深入浅出之-Go语言并发编程面试:Goroutine简介与创建
【4月更文挑战第22天】Go语言的Goroutine是其并发模型的核心,是一种轻量级线程,能低成本创建和销毁,支持并发和并行执行。创建Goroutine使用`go`关键字,如`go sayHello(&quot;Alice&quot;)`。常见问题包括忘记使用`go`关键字、不正确处理通道同步和关闭、以及Goroutine泄漏。解决方法包括确保使用`go`启动函数、在发送完数据后关闭通道、设置Goroutine退出条件。理解并掌握这些能帮助开发者编写高效、安全的并发程序。
10 1
|
2天前
|
人工智能 Go 调度
掌握Go并发:Go语言并发编程深度解析
掌握Go并发:Go语言并发编程深度解析
|
1月前
|
缓存 资源调度 Go
内存模型与调度器在Go语言并发编程中的应用与优化
【2月更文挑战第16天】Go语言以其独特的并发编程模型而备受瞩目,其中内存模型和调度器是支撑其高效并发执行的关键机制。本文将探讨内存模型与调度器在Go语言并发编程中的应用场景,并介绍一些优化策略,帮助开发者更好地利用这些机制,提升程序的性能和稳定性。
|
2月前
|
存储 缓存 网络协议
Go语言并发编程实战:构建高性能Web服务器
【2月更文挑战第6天】本文将通过构建一个高性能的Web服务器实战案例,深入探讨如何在Go语言中运用并发编程技术。我们将利用goroutine和channel实现高效的请求处理、资源管理和并发控制,以提升Web服务器的性能和稳定性。通过这一实战,你将更好地理解和掌握Go语言在并发编程方面的优势和应用。
|
2月前
|
消息中间件 Go
Go语言高级并发编程技术:深入探索同步原语与复杂并发模式
【2月更文挑战第6天】在Go语言中,除了基础的goroutine和channel之外,还提供了丰富的同步原语和高级并发模式。本文将深入探讨Go语言中的sync包、atomic包以及更复杂的并发模式,如管道、消息队列和生产者消费者模型。通过理解这些高级并发编程技术,我们可以更好地构建高性能、可扩展的并发系统。