Golang之协程同步

 

协程同步


Mutex

Go语言包中的sync包提供了两种锁类型:sync.Mutex和sync.RWMutex,前者是互斥锁,后者是读写锁。

使用锁的经典模式:

var lck sync.Mutex
func foo() {
    lck.Lock() 
    defer lck.Unlock()
    // ...
}

lck.Lock()会阻塞直到获取锁,然后利用defer语句在函数返回时自动释放锁。

 

 

Once

对于从全局角度只需要运行一次的代码,比如全局初始化操作,Go语言提供了一个once类型来保证全局的唯一性操作,如下:

var flag int32
var once sync.Once

func initialize() {
    flag = 3
    fmt.Println(flag)
}

func setup() {
    once.Do(initialize)
}

func main() {

    setup()    
    setup()    
}

flag只别打印 了一次。换句话说,如果once.Do(f)被多次调用,只有第一次调用会执行f,即使f每次调用Do 提供的f值不同。需要给每个要执行仅一次的函数都建立一个Once类型的实例。

 

 

WaitGroup

WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。 

import (
    "fmt"
    "sync"
)

type WaitGroupWrapper struct {
    sync.WaitGroup
}

func (w *WaitGroupWrapper) Wrap(cb func()) {
    w.Add(1)
    go func() {
        cb()
        w.Done()
    }()
}

var ch chan int = make(chan int, 10)

func Enqueue() {
    for n := 0; n < 10; n++ {
        ch <- (100 + n)
        fmt.Printf("Enqueue %d\n", 100+n)
    }
    close(ch)
}

func Dequeue() {
    var a int = 0
    for {

        a = <-ch
        fmt.Printf("Dequeue %d\n", a)

        if a <= 0 {
            break
        }
    }
}

func main() {
    var w WaitGroupWrapper

    w.Wrap(Dequeue)
    w.Wrap(Enqueue)
    w.Wait()
    w.Done()
}

 

 

Pool

type Pool struct {
    // 可选参数New指定一个函数在Get方法可能返回nil时来生成一个值
    // 该参数不能在调用Get方法时被修改
    New func() interface{}
    // 包含隐藏或非导出字段
}

func (p *Pool) Get() interface{}
func (p *Pool) Put(x interface{})

 

Pool 是一个可以分别存取的临时对象的集合;可以安全的被多个线程同时使用。

Pool 中保存的任何item都可能随时不做通告的释放掉。如果Pool持有该对象的唯一引用,这个item就可能被回收。

 

看个例子:

import (
    "fmt"
    "sync"
    "time"
)

var bytePool = sync.Pool{
    New: func() interface{} {
        b := make([]byte, 2048)
        return &b
    },
}

func main() {
    const N = 1000000

    a := time.Now().UnixNano()
    for i := 0; i < N; i++ {
        _ = make([]byte, 2048)
    }
    b := time.Now().UnixNano()
    fmt.Println("without pool ", (b-a)/1000000, "ms")

    for j := 0; j < N; j++ {
        obj := bytePool.Get().(*[]byte)
        bytePool.Put(obj)
    }
    c := time.Now().UnixNano()
    fmt.Println("with    pool ", (c-b)/1000000, "ms")
}

 

注意:Get方法并不会对获取到的对象值做任何的保证,因为放入本地池中的值有可能会在任何时候被删除,但是不通知调用者。放入共享池中的值有可能被其他的goroutine偷走。 所以对象池比较适合用来存储一些临时切状态无关的数据,但是不适合用来存储数据库连接的实例,因为存入对象池重的值有可能会在垃圾回收时被删除掉,这违反了数据库连接池建立的初衷。

根据上面的说法,Golang的对象池严格意义上来说是一个临时的对象池,适用于储存一些会在goroutine间分享的临时对象。主要作用是减少GC,提高性能。在Golang中最常见的使用场景是fmt包中的输出缓冲区。

 

 

 

context

context.Context 接口定义如下:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

其中,

Done() 方法返回一个 Channel,这个 Channel 会在当前工作完成或者被取消之后关闭,多次调用 Done 方法会返回同一个 Channel;

Err() 方法仅在 Done()方法返回的channel 被关闭时才有非空值:

  • 如果context.Context 被取消,返回Canceled 错误;
  • 如果context.Context 超时,返回 DeadlineExceeded 错误;

 

如下图所示,我们可能会创建多个 Goroutine 来处理一次请求,而 context.Context 的作用就是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期。

每一个 context.Context 都会从最顶层的 Goroutine 一层一层传递到最下层。

context.Context 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层,就可以在下层及时停掉无用的工作以减少额外资源的消耗。

如下例子,我们创建了一个过期时间为 1s 的上下文,并向上下文传入 handle 函数,该方法会使用 500ms 的时间处理传入的『请求』:

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

func handle(ctx context.Context, w *sync.WaitGroup, duration time.Duration) {
    select {
    case <-ctx.Done():
        fmt.Println("handle", ctx.Err())
    case <-time.After(duration):
        fmt.Println("process request with", duration)
    }
    w.Done()
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    var w sync.WaitGroup
    w.Add(1)
    handle(ctx, &w, 500*time.Millisecond)

    select {
    case <-ctx.Done():
        fmt.Println("main", ctx.Err())
    }

    w.Wait()
}

因为过期时间大于处理时间,所以我们有足够的时间处理该『请求』,运行上述代码会打印出如下所示的内容:

process request with 500ms
main context deadline exceeded

handle 函数没有进入超时的 select 分支,但是 main 函数的 select 却会等待 context.Context 的超时并打印出 main context deadline exceeded

如果我们将处理『请求』时间增加至 1500ms,整个程序都会因为上下文的过期而被中止,:

handle context deadline exceeded
main context deadline exceeded

这个例子演示了多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就立刻停止当前正在执行的工作。

 

创建 Context

创建上下文的各种方法如下:

func Background() Context
func TODO() Context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val
interface{}) Context

context.TODO 应该只在不确定应该使用哪种上下文时使用;

在多数情况下,如果当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始(parent)的上下文向下传递。

 

Context 的调用应该是链式的,通过context.WithXXX派生出新的 Context,当父 Context 被取消时,其派生的所有 Context 都将取消

通过context.WithXXX都将返回新的 Context 和 CancelFunc,调用 CancelFunc 将取消子代,移除父代对子代的引用,并且停止所有定时器。未能调用 CancelFunc 将泄漏子代,直到父代被取消或定时器触发。 

 

WithCancel

使用例子如下,避免goroutine泄露。

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

func main() {
    gen := func(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    fmt.Println("goroutine done")
                    return
                case dst <- n:
                    n++
                }
            }
        }()

        fmt.Println("gen done")
        return dst
    }

    ctx, cancel := context.WithCancel(context.Background())

    for n := range gen(ctx) {
        fmt.Println("read ", n)
        if n == 5 {
            break
        }
    }

    cancel() // cancel when we are finished consuming integers
    time.Sleep(time.Second * 1)
}

 

 

WithValue

使用例子

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

func main() {
    type favContextKey string

    f := func(ctx context.Context, k favContextKey) {
        if v := ctx.Value(k); v != nil {
            fmt.Println(k, ", found value:", v)
            return
        }
        fmt.Println(k, ", key not found")
    }

    k := favContextKey("language")
    ctx := context.WithValue(context.Background(), k, "Go")

    f(ctx, k)
    f(ctx, favContextKey("color"))
}

 

posted @ 2020-10-13 17:07  如果的事  阅读(782)  评论(0编辑  收藏  举报