• go语言并发


    1. Go Mutex

    Go语言中goroutines共享内存。这对性能有好处,但是从多个goroutine修改相同的内存是不安全的, 会导致数据争用和崩溃。这是Go的座右铭:不要通过共享内存进行通信;而是通过通信共享内存。
    确保goroutine独占访问数据的另一种方法是使用互斥体。

    1.1 sync.Mutex

    var cache map[int]int
    var mu sync.Mutex
    
    func expensiveOperation(n int) int {
        // in real code this operation would be very expensive
        return n * n
    }
    
    func getCached(n int) int {
        mu.Lock()
        v, isCached := cache[n]
        mu.Unlock()
        if isCached {
            return v
        }
    
        v = expensiveOperation(n)
    
        mu.Lock()
        cache[n] = v
        mu.Unlock()
        return v
    }
    
    func accessCache() {
        total := 0
        for i := 0; i < 5; i++ {
            n := getCached(i)
            total += n
        }
        fmt.Printf("total: %d\n", total)
    }
    
    cache = make(map[int]int)
    go accessCache()
    accessCache()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    sync.Mutex的初始值是有效的mutex,所以不必初始化。为了提高性能,我们希望最小化持有锁的时间。与许多其他语言不同,Go互斥锁是非递归的。如果相同的goroutine尝试两次对互斥量进行Lock(),则第二个Lock()将永远阻塞。

    1.2 sync.RWMutex

    在sync.Mutex Lock()中始终采用排他锁。
    在重读场景中,如果我们允许多个读者但只允许一个写者,则可以提高性能。sync.RWMutex具有两种锁定功能:用于读取的锁定和用于写入的锁定。它遵循以下规则:

    • 写锁采用排他锁
    • 读锁将允许其他读但不允许写

    1.3 Mutex 陷阱

    不要拷贝互斥锁。sync.Mutex变量的副本以与原始互斥锁相同的状态开始,但不是同一个Mutex。
    拷贝互斥锁几乎总是错误的, 比如通过将其传递给另一个函数或将其嵌入结构中并复制该结构。如果要共享互斥变量,请将其作为指针* sync.Mutex传递。

    互斥体不是递归的(又名可重入)。在某些语言中,互斥锁是递归的,即同一线程可以多次锁定同一互斥锁。
    在Go中sync.Mutex是非递归的。在同一goroutine中两次调用Lock将导致死锁。

    1.4 检测争夺

    如果不使用sync.Mutex来确保goroutine之间的数据独占访问,或者忘记锁定程序的某些部分,则会引起数据争夺。数据争用可能导致内存损坏或崩溃。使用Go可以很容易地通过附加检查来对代码进行检测。
    使用-race进行go build或go run。 例如:go run -race data_race.go

    2. Goroutine

    go语言本身内置了调度和上下文的切换,开发者只需要关注自己的函数,并且交给goroutine就可以了

    2.1 waitGroup

    多个Goroutine

    var wg sync.WaitGroup
    
    func hello(i int) {
        defer wg.Done() // 结束后 goroutine的注册数量就会-1
        fmt.Println("Hello Goroutine!", i)
    }
    
    func main() {
        for i := 0; i < 2; i++ {
            wg.Add(1) // 注册goroutine
            go hello(i) // 启动goroutine
        }
        wg.Wait() // 等待所有的goroutine完成
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    多个goroutine并发执行,并不能保证顺序,同时main函数结束,协程也就会结束,所以想保障正确的逻辑,就需要通过waitGroup来完成。

    • wg。Add(): main协程通过此方法完成wg的注册,并将数量+1
    • wg。Done():work协程通过此方法完成任务,并将数量-1
    • wg。Wait():main协程会一直阻塞,直到所有的work协程完成

    2.2 Channels

    java和go中sync包通过共享内存实现通信,go channel提倡通过通信共享内存,这就是典型的CSP思想的实践。
    程序在给channel分配内存时,可以指定channel的容量大小。可以根据是否具有容量将channel分为无缓冲和缓冲两类。

    // 同步
    c1 := make(chan int)
    
    // 异步
    c2 := make(chan int,2)
    # 代码
    func fibonacci(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
        }()
        fibonacci(c, quit)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    2.3 池化

    Goroutine是很轻量级的协程,但是频繁的创建协程也需要很大的开销,主要表现在创建(内存),调度(调度器),删除(GC)。
    虽然Goroutine是用户态上的操作,但是最终都需要交给系统线程,而系统线程也有承载压力,所以我们需要协程池来降低这部分的压力。
    协程池通过维护一组协程(也就是Goroutine),来处理并发任务。协程池可以有效地控制协程的数量,避免过多的协程导致系统资源的浪费,从而提高程序的性能和稳定性。
    实现协程池一般需要以下几个步骤:

    • 创建协程池对象。协程池对象中需要包含协程池大小、任务队列、信号量等信息。
    • 初始化协程池。在初始化协程池时,需要创建一定数量的协程(Goroutine)并加入到协程池中。
    • 向协程池中提交任务。提交任务时,将任务加入到任务队列中,并通过信号量激活一个空闲的协程来执行任务。
    • 执行任务。协程从任务队列中取出任务并执行,当任务队列为空时,协程将进入等待状态。
    • 停止协程池。停止协程池时,需要将所有任务执行完毕,并关闭所有协程。

    2.4 GMP模型
    在这里插入图片描述

    结构图如下:
    在这里插入图片描述
    go func()执行过程
    在这里插入图片描述

  • 相关阅读:
    Java IO之管道简介说明
    一文搞懂V8引擎的垃圾回收机制
    Vue3实现获取验证码按钮倒计时效果
    《RCLane:Relay Chain Prediction for Lane Detection》论文笔记
    控价为什么一定要先监测价格
    《程序人生》
    基于 FPGA 实现数字时钟详细原理讲解及验证结果
    「Vue系列」欢迎传送到“Teleport”星球
    SSM框架Demo: 简朴博客系统
    八数码问题
  • 原文地址:https://blog.csdn.net/chengfangdong/article/details/133967051