Go并发编程与并发安全

一、前言

1、goroutine的并发:一个go程序进程中,在同一时间跑多个goroutine,那么这些goroutine是并发的。虽然现在计算机CPU一般有多个计算核心,但是核心数也是较少的,而一个计算机运行的各种程序其所需要的内核线程数量往往远大于其核心数,所以只能在不同的CPU时间分片执行不同线程,实现一个伪并行状态。

2、goroutine并发安全问题:

(1)协程的执行顺序不确定导致的问题。多个协程执行同一个方法,且多个线程存在共享变量,由于这些协程执行顺序是不确定的,由这些协程同步修改使用一些变量时,产生了不确定的结果

func main() {
    arr := make([]int, 10000)
    go func() {
        for i := 0; i < len(arr); i++ {
            arr[i] = -1
        }
    }()
    go func() {
        for i := 0; i < len(arr); i++ {
            arr[i] = 1
        }
    }()
    time.Sleep(time.Second * 1)
    fmt.Println(arr) //arr有些值被置为-1,有些值置改为1
}

(2)CPU的内存模型(多级缓存和内存)导致的问题。多个协程执行同一个方法,且多个线程存在共享变量。一个协程修改完变量,变量最新的值会放到cpu缓存中,没有马上同步到内存并更新其他核心的缓存数据,导致其他协程感知不到这个修改操作,仍然读旧的脏数据,导致最终的结果不符合预期(通道通信和互斥锁这种同步操作会让cpu把累计的写操作从缓存刷回到内存)

func main() {
    groupW := sync.WaitGroup{}
    groupW.Add(2)
    num := 0
    //这里以num++为例子
    //PS 就算是num++本身也不是线程安全的,这个操作不是原子性的,他有三步操作,将num值读取到寄存器,寄存器值加一,将这个值写回到内存/缓存
    go func() {
        for i := 0; i < 10000; i++ {
            num++
        }
        groupW.Done()
    }()
    go func() {
        for i := 0; i < 10000; i++ {
            num++
        }
        groupW.Done()
    }()
    groupW.Wait()
    //预期为20000  实际输出值小于20000  第一次13782 第二次20000
    fmt.Println(num)
}

3、并发安全的类型:如果某种类型所有的方法操作都是并发安全时,则可称它为并发安全的类型

 

二、协程共享(共同读/写)变量

1、包变量,多个协程之间可以共享

2、父协程在创建一个或多个子协程之前,在父协程中定义的变量或父协程可以访问的变量,可以被所有的子协程以及子协程的子协程共享

 

三、实现线程安全

1、使用通道实现线程安全

用通道来传递变量,通道数据满了时阻塞 通道写,通道没有数据时阻塞读

func main() {
    //一个int类型通道
    numCn := make(chan int, 1)
    defer close(numCn)
    numCn <- 0
    groupW := sync.WaitGroup{}
    groupW.Add(2)

    go func() {
        for i := 0; i < 10000; i++ {
            val := <-numCn
            numCn <- val + 1
        }
        groupW.Done()
    }()
    go func() {
        for i := 0; i < 10000; i++ {
            val := <-numCn
            numCn <- val + 1
        }
        groupW.Done()
    }()
    groupW.Wait()
    //预期为20000  实际输出值20000
    fmt.Println(<-numCn)
}
View Code

 2、使用锁实现线程安全

   2.1、互斥锁(sync.Mutex)

      2.1.1、加锁与解锁

   获取锁方法:func (m *Mutex) Lock()   

   (获取锁后,阻塞其他goroutine的Lock()操作,将其添加到获取锁的等待队列中(阻塞等待唤醒或自旋几次尝试抢锁))

   释放锁方法():func (m *Mutex) Unlock()

   (释放锁后,其他goroutine可以获取锁)

      2.1.2、互斥锁的使用

   在Lock()和Unlock()之间的代码,同一时间只能有一个goroutine执行,所有可以用Lock()和UnLock()来创建协程安全的临界区域

var numInsLock = sync.Mutex{}
var num = 0

func increaseNum() {
    numInsLock.Lock()
    num++
    numInsLock.Unlock()
}

func main() {
    groupW := sync.WaitGroup{}
    groupW.Add(2)

    go func() {
        for i := 0; i < 10000; i++ {
            increaseNum()
        }
        groupW.Done()
    }()
    go func() {
        for i := 0; i < 10000; i++ {
            increaseNum()
        }
        groupW.Done()
    }()
    groupW.Wait()
    //预期为20000  实际输出值20000
    fmt.Println(num)
}
View Code

  一般Lock()和UnLock() 配合defer使用,保证最后都能解锁,也避免Lock后调用多次UnLock导致Panic

func increaseNum() {
    numInsLock.Lock()
    defer numInsLock.Unlock()

    num++
}

  2.2、读写互斥锁(sync.RWMutex)

   2.2.1、加锁与解锁

  获取读锁:func (rw *RWMutex) RLock()     

      (获取读锁成功后,阻塞其他线程获取写锁的操作,不阻塞获取读锁的操作 [实际就是把读操作计数器加1])

           

      释放读锁:func (rw *RWMutex) RUnlock()

      (将读操作技术器减去1,读操作技术器为0时,获取写锁被阻塞的goroutine可以获取写锁)

             

      获取写锁:func (rw *RWMutex) Lock()

      (读操作技术器值为0,获取写锁成功后,阻塞其他线程获取写锁和获取读锁的操作)

         

      释放写锁:func (rw *RWMutex) Unlock()

     (释放写锁,唤醒获取读锁时被阻塞的goroutine,如果没有获取读锁的goroutine,则唤醒一个获取写锁的goroutine)

              

    2.2.2、读写互斥锁的应用

读写锁互斥锁sync.RWMutex相对于互斥锁sync.Mutex,在协程写竞争较少,读竞争较大,也就是写少读多的场景,可以通过减少读之间的协程竞争来提高并发的效率。但是sync.RWMutex其相对于

sync.Mutex也有其他额外的开销,比如还要记录一个获取读锁gouroutine等操作。所以在没有大量读竞争的场景,sync.Mutex效率往往更高。

2.3、sync.atomic包(提供原子性的载入、储存、交换、比较并交换、增减方法)

/*原子储存和读取操作*/
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
/*原子增减操作*/
func AddInt32(addr *int32, delta int32) (new int32)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
/*原子交换操作*/
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
/*原子比较并交换的操作,CAS算法*/
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
View Code

2.4、sync.Once

在做一些初始化操作时,由于初始化有多个步骤,比如在一个goroutine在初始化一个结构体的多个成员时,另一个goroutine直接用这个未完全初始化但不为nil的结构体来作为参数,容易导致空指针异常等意外的错误

这种情况下,可以通过加锁来初始化这个结构体,让这个结构体要么为nil要么初始化完成。但是加锁可能存在多个goroutine对这个结构体初始化的情况,导致这个结构体被初始化多次,sync.Once封装了保证让一个方法调用只调用一次且线程安全的方法,sync.Once也可以用于单例模式。

sync.Once结构体

type Once struct {
    m    Mutex
    done uint32
}

sync.Once实现一次线程安全的函数调用的方法

func (o *Once) Do(f func()) {
    //原子加载o.done的数据,o.done的零值为0,如果o.done为1说明Do方法已经被调用了一次,被调用则直接返回
    if atomic.LoadUint32(&o.done) == 1 {
        return
    }
    // 加锁最后解锁
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        //原子设置o.done的值为1,调用参数中传递的f()函数
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}

 应用示例:

import (
    "fmt"
    "sync"
)

type AnimalStruct struct {
    name   *string
    eyeNum *int32
    legNum *int32
}

func main() {
    var animalFlog AnimalStruct
    name := "青蛙"
    eyeNum := int32(4)
    legNum := int32(6)
    //初始化方法
    initFunc := func(name string, eyeNum int32, legNum int32) AnimalStruct {
        animal := AnimalStruct{name: new(string), eyeNum: new(int32), legNum: new(int32)}
        animal.name = &name
        animal.eyeNum = &eyeNum
        animal.legNum = &legNum
        return animal
    }(name, eyeNum, legNum)
    initOnceFunc := func() {
        animalFlog = initFunc
        fmt.Println("执行初始化完成")
        fmt.Printf("一只%v有%v只眼睛%v条腿", *animalFlog.name, *animalFlog.eyeNum, *animalFlog.legNum)
    }

    wg := sync.WaitGroup{}
    wg.Add(10)
    //多个线程只初始化一次
    onceOpreate := sync.Once{}
    for i := 0; i < 10; i++ {
        go func() {
            onceOpreate.Do(initOnceFunc)
            wg.Done()
        }()
    }
    wg.Wait()
}
View Code

 

四、线程间通信与控制

1、chan与select多路复用

select语法格式:

select { //select case判断是随机的,不是按照从上往下判断一个一个case判断
    case <-chan_name: //如果能从指定通道读取数据,执行这个case的代码
        // ...
    case val := <-chan_name: //如果能从指定通道读取数据并赋值,执行这个case的代码
        // ...use x...
    case chan_name <- y: //如果能向指定通道发送数据,执行这个case的代码
        // ...
    default: //如果都以上case都不满足,执行default下的代码
        // ...
    }

 应用示例:

func main() {
    chanInt := make(chan int, 1)
    num := 0
    go func() {
        for {
            select {
            case <-chanInt:
                println("从通道接收到值")
            case val := <-chanInt:
                println("从通道接收到值并赋值给变量val,val=%v", val)
            case chanInt <- num:
                num++
                println("发送num值到通道,num=%v", num)
            default:
                println("默认值")
            }
        }
    }()
    time.Sleep(time.Millisecond * 1)
}

 

2、等待组(sync.WaitGroup)

  2.1、sync.WaitGroup的三个方法

func (wg *WaitGroup) Add(delta int)
将wg内部的计数器counter增加delta(counter初始值为0,参数delta可以小于0)
func (wg *WaitGroup) Done()
将wg内部的计数器counter值减1,其实这个方法就是调用了wg.Add(-1)
// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {
    wg.Add(-1)
}
func (wg *WaitGroup) Wait()
将执行Wait()方法的goroutine阻塞(并用cas算法将在wg上阻塞的等待gourutine数量加一)直到wg的counter值为0时,唤醒这个goroutine
2.2、应用示例
当一个goroutine执行一些任务,需要其他goroutine先执行一些前置任务或子任务时,可以用sync.WaitGroup来控制这个并发执行流程
func main() {
    subMission := sync.WaitGroup{}
    //执行子任务
    missionNum := 10
    subMission.Add(10)
    for i := 1; i <= missionNum; i++ {
        go func(count int) {
            println("正在执行第%v次任务", count)
            subMission.Done()
        }(i)
    }
    subMission.Wait()
    println("所有子任务执行完毕,接下来做主任务处理......")
}

3、Context上下文

Context可以控制一组树状结构的goroutine,相比于WaitGroup,Context对于派生的goroutine有更强的控制能力。WaitGroup往往应用于确定数量的goroutine,而当goroutine数量未知或不可控时,就可以采用Context来控制整个并发流程。Context之间可以设置父子关系,当父级别Context关闭时,子Context会同时关闭;Context还可以设置延时关闭和超时关闭。

   3.1 context接口的4个方法

type Context interface {
    /*Deadline()返回一个deadline和标识是否已设置deadline的bool值,如果没有设置deadline,
    则ok == false,此 时deadline为一个初始值的time.Time值*/
    Deadline() (deadline time.Time, ok bool)

    /*Done()返回一个channel,需要在select-case语句中使用,如”case <-context.Done():”。
    当context关闭后,Done()返回一个被关闭的管道,关闭的管理仍然是可读的,据此goroutine可以收到关闭请求;
    当context还未关闭时,Done()返回nil。*/
    Done() <-chan struct{}

    //当context关闭后,Err()返回context的关闭原因
    Err() error
    
    //从context中 同过key值获取value值
    Value(key interface{}) interface{}
}
 3.2标准库中实现了Context接口的类型(emptyCtx、cancelCtx、timeCtx和valueCtx)
 
   3.2.1、emptyCtx
   emtpyCtx一般用来做根Context,标准库中定义了两个变量并初始化为了emptyCtx,分别是backgroud和todo用作Context根节点或用来避免nil值的Context节点
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done() <-chan struct{} {
    return nil
}

func (*emptyCtx) Err() error {
    return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
    return nil
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}
View Code 
var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)
 标准库中用来分别返回backgroud和todo的方法
func Background() Context {
    return background
}

func TODO() Context {
    return todo
}
 
    3.2.2、cancelCtx
type cancelCtx struct {
    Context                       // 父级context                                   

    mu       sync.Mutex           // protects following fields
    done     chan struct{}        // 一个通道,当cancelCtx被关闭后,可以从done通道中获取到nil值
    children map[canceler]struct{} //子context
    err      error                // 用来设置关闭cancelCtx的远影
}
    (1)创建cancelCtx的方法:
// 返回一个指定父级别Context的cancelCtx以及一个关闭这个cancelCtx的方法
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}
// 返回一个指定父级别Context的cancelCtx
func newCancelCtx(parent Context) cancelCtx {
   return cancelCtx{Context: parent}
} 

    (2) cancelCtx的核心方法:

func (c *cancelCtx) Done() <-chan struct{}         

返回一个通道,当调用cancel()方法后,可以从这个通道获取到nil值,用来告诉goroutine这个cancelCtx有没有被关闭

func (c *cancelCtx) cancel(removeFromParent bool, err error)

关闭这个cancelCtx和这个cancelCtx的子级Context,其实就是关闭这些cancelCtx的done通道。removeFromParent表示是否要把自己从父级context中删除,err用来传递关闭的原因

func (c *cancelCtx) Err() error

返回这个cancelCtx被关闭的原因

 

    3.2.3、timerCtx

    timerCtx其实就是对cancel做了进一步的封装,增加了一个定时器和截止时间
type timerCtx struct {
    cancelCtx
    timer *time.Timer  // 定时器
    deadline time.Time // 截止时间,到达这个时间,会把这个timerCtx自动关闭
}

     (1)创建timerCtx的方法:

   func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(false, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

      func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 

//返回一个带有超时时间的context,其实就是返回了一个截止时间为当前时间+超时时间的一个timeCtx
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

  (2)timeCtx的核心方法:

func (c *timerCtx) Deadline() (deadline time.Time, ok bool)

返回关闭这个timerCtx的截止时间,true代表这个context是带有截止时间的

func (c *timerCtx) cancel(removeFromParent bool, err error)

手动关闭这个timerCtx,removeFromParent表示是否要把自己从父级context中删除,err用来传递关闭的原因

 

   3.2.4、valueCtx
   带有键值对数据的Context
type valueCtx struct {
    Context
    key, val interface{}
}

   (1)创建方法

  func WithValue(parent Context, key, val interface{}) Context
func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

   (2)核心方法

func (c *valueCtx) Value(key interface{}) interface{}
通过key值从这个valueCtx获取对应的value值,如果这个valueCtx中没有这个key值,那么从这个value的父级Context中寻找key对应的value值
func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

     3.3、Context使用示例

      这里用context.Background()返回一个background(emptyCtx类型)作为Context根节点。以ctx为父Context新建一个cancelCtx类型的Context cancelCtx1,以cancelCtx1为父Context分别新建一个cancelCtx类型的Context cancelCtx2和两个timerCtx类型的Context timerCtx1(截止时间当前时间后2秒)和timerCtx2(设置超时时间为3秒),再以cancelCtx2为父Context建立一个valueCtx类型的Context  valueCtx1(键"name",值"kiki")

      定时1s后调用cancel方法关闭 timerCtx2(超时时间设置为3秒),定时3s后调用cancel方法关闭Context timerCtx1(截止时间设置为2秒后),定时5s后调用cancel方法关闭cancelCtx1

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

func main() {
    //以一个emptyCtx类型的Context background 为根上下文
    go ContextDemoFunc(context.Background())
    time.Sleep(time.Second * 7)
}

func ContextDemoFunc(ctx context.Context) {
    //以ctx为父Context,新建一个cancelCtx类型的Context cancelCtx1,
    cancelCtx1, cancelCancelCtx1Func := context.WithCancel(ctx)
    //以cancelCtx1为父Context,新建一个cancelCtx类型的Context cancelCtx2
    cancelCtx2, _ := context.WithCancel(cancelCtx1)
    //以cancelCtx1为父Context,新建一个timerCtx类型的Context timerCtx1,截止时间为当前时间后两秒
    timerCtx1, cancelTimerCtx1Func := context.WithDeadline(cancelCtx1, time.Now().Add(time.Second*2))
    //以cancelCtx1为父Context,新建一个timerCtx类型的Context timerCtx2,超时时间为三秒
    timerCtx2, cancelTimerCtx2Func := context.WithTimeout(cancelCtx1, time.Second*3)
    //以cancelCtx2为父Context,新建一个valueCtx类型的Context valueCtx1,键为"name",值为"kiki"
    valueCtx1 := context.WithValue(cancelCtx2, "name", "kiki")

    //1/3/5s后,分别调用方法关闭 timerCtx2/timerCtx1/cancelCtx1
    time.AfterFunc(time.Second*1, cancelTimerCtx2Func)
    time.AfterFunc(time.Second*3, cancelTimerCtx1Func)
    time.AfterFunc(time.Second*5, cancelCancelCtx1Func)

    pre := time.Now()

    go func() {
        select {
        case <-cancelCtx1.Done():
            fmt.Printf("%v后,检测到cencelCtx1关闭,接下来做点什么\n", time.Now().Sub(pre).Round(time.Second))
        }
    }()
    go func() {
        select {
        case <-cancelCtx2.Done():
            fmt.Printf("%v后,检测到cencelCtx2关闭,接下来做点什么\n", time.Now().Sub(pre).Round(time.Second))
        }
    }()
    go func() {
        select {
        case <-timerCtx1.Done():
            fmt.Printf("%v后,检测到timerCtx1关闭,接下来做点什么\n", time.Now().Sub(pre).Round(time.Second))
        }
    }()
    go func() {
        select {
        case <-timerCtx2.Done():
            fmt.Printf("%v后,检测到timerCtx2关闭,接下来做点什么\n", time.Now().Sub(pre).Round(time.Second))
        }
    }()
    go func() {
        fmt.Printf("在valueCtx1中键name的值为%v\n", valueCtx1.Value("name"))
        select {
        case <-valueCtx1.Done():
            fmt.Printf("%v后,检测到valueCtx1关闭,接下来做点什么\n", time.Now().Sub(pre).Round(time.Second))
        }
    }()
}

       输出结果

在valueCtx1中键name的值为kiki
1s后,检测到timerCtx2关闭,接下来做点什么
2s后,检测到timerCtx1关闭,接下来做点什么
5s后,检测到cencelCtx2关闭,接下来做点什么
5s后,检测到cencelCtx1关闭,接下来做点什么
5s后,检测到valueCtx1关闭,接下来做点什么

      可以发现timerCtx2在超时时间2s内就被关闭(定时器1后调用了关闭的方法);timerCtx1在2s后到达截止时间自动关闭;5s后cancelCtx1关闭,同时关闭了它的子Context cancelCtx2,valueCtx1随cancelCtx2关闭而关闭

posted @ 2022-08-22 13:17  初袋霸王龙  阅读(554)  评论(0编辑  收藏  举报