美文网首页编程杂谈
Goroutine学习笔记(二)

Goroutine学习笔记(二)

作者: eye_water_ | 来源:发表于2020-11-18 19:21 被阅读0次
    Goroutine与锁

    在进行并发编程时,很多时候都需要涉及到变量的共享,下面这段代码创建了2个Goroutine来访问变量a并对a进行自加操作,a预期结果应为200000

    goroutine-without-lock.go

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    func main() {
        a := 0
        var n sync.WaitGroup
        for i := 0; i < 2; i++ {
            n.Add(1)
            go func() {
                defer n.Done()
                for j := 0; j < 100000; j++ {
                    a++
                }
            }()
        } //创建2个Goroutine
        n.Wait()
        fmt.Printf("a = %d\n", a)
    }
    

    大多数情况下并不能得到正确的结果

    假设此时a的值为10000,那么在Goroutine [1]读取a的值后,并且未在内存中写入a加1之后的值的这段时间,如果Goroutine [2]此时访问a的值,它得到的值为10000,而不是10001,这样就造成了Goroutine [2]并没有读取到Goroutine [1]更新后的数值,因此会出现少加的情况

    解决该问题的办法就是当Goroutine涉及到有关变量a的执行语句时,要确保读取和写入操作完成后其它Goroutine才能访问变量a

    goroutine-with-lock.go

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    func main() {
        a := 0
        var mu sync.Mutex
        var n sync.WaitGroup
        for i := 0; i < 2; i++ {
            n.Add(1)
            go func() {
                defer n.Done()
                for j := 0; j < 100000; j++ {
                    mu.Lock()
                    a++
                    mu.Unlock()
                }
            }()
        }
        n.Wait()
        fmt.Printf("a = %d\n", a)
    }
    

    每当Goroutine执行到涉及变量a的语句时,先申请锁,更新完a的值后在释放锁,当一个Goroutine持有锁时,其它的Goroutine都会等待锁释放后再执行申请锁的操作,这样就保证了每次只有一个Goroutine执行变量a的读取和写入操作

    加锁的技巧

    为了避免程序运行时不必要的等待,在加锁时需要注意仅当Goroutine需要更改共享变量的值时再获取锁,更改完共享变量的值立刻释放锁

    一个不恰当的例子

    lock-whole-goroutine-execution_time.go

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    const (
        UNASSIGN  = 0 //未分配
        COMPLETED = 1 //执行完毕
    )
    
    func main() {
        task := make([]int, 10)
        var mu sync.Mutex
        var n sync.WaitGroup
        for index := range task {
            task[index] = UNASSIGN
        }
        for index := range task {
            n.Add(1)
            go func(index int) {
                defer n.Done()
                mu.Lock()
                time.Sleep(1 * time.Second)
                task[index] = COMPLETED
                mu.Unlock() //在整个Goroutine加锁
            }(index)
        }
        n.Wait()
        fmt.Printf("All task done!\n")
    }
    

    在这个例子中,由于在整个Goroutine执行语句进行加锁,导致整个程序执行了10s,结果和串行执行所需时间一样

    如果在Goroutine中等待任务完成后(time.Sleep(1 * time.Second)在此处相当于执行任务,通常情况下任务可以为I/O读写,爬虫请求等等),再申请锁会极大的加快程序执行效率,将上面的代码time.Sleep(1 * time.Second)mu.Lock()互换位置再执行,整个程序仅需1s就可以执行完毕

    上述代码其实不适用sync.WaitGroup也可以实现等待操作,主要思想就是通过循环遍历检查所有的任务是否执行完毕,如果所有任务执行完毕退出循环

    lock-without-waitgroup.go

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    const (
        UNASSIGN  = 0 //未分配
        COMPLETED = 1 //执行完毕
    )
    
    func main() {
        task := make([]int, 10)
        var mu sync.Mutex
        for index := range task {
            task[index] = UNASSIGN
        }
        for index := range task {
            go func(index int) {
                time.Sleep(1 * time.Second)
                mu.Lock()
                task[index] = COMPLETED
                mu.Unlock()
            }(index)
        }
    
        for {
            taskDone := true
            for index := range task {
                taskDone = taskDone && (task[index] == COMPLETED)
            } //当所有任务执行完毕时,taskDone为true
            if taskDone {
                break
            } //当所有任务执行完毕时,退出循环
        }
        fmt.Printf("All task done!\n")
    }
    

    上面的代码还可以优化一下,由于使用的是for {}语句,会一直占用CPU,为了避免其一直占用CPU,在for循环内部可以添加time.Sleep(100 * time.Millisecond)

    相关文章

      网友评论

        本文标题:Goroutine学习笔记(二)

        本文链接:https://www.haomeiwen.com/subject/fjxhiktx.html