Goroutine学习笔记(二)

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学习笔记(二))