sync包

sync包

一、sync还是channel

​ Go语言提倡“不要通过共享内存来通信,而应该通过通信来共享内存”。正如在前文阐述的那样,建议大家优先使用CSP并发模型进行并发程序设计。但是在下面一些场景下,我们依然需要sync包提供的低级同步原语。

(1)需要高性能的临界区同步机制场景。
	在Go中,channel属于高级同步原语,其实现是建构在低级同步原语之上的。因此,channel自身的性能与低级同步原语相比要略微逊色。
(2)不想转移结构体对象所有权,但又要保证结构体内部状态数据的同步访问的场景。
	基于channel的并发设计的一个特点是,在goroutine间通过channel转移数据对象的所有权。只有拥有数据对象所有权(从channel接收到该数据)的goroutine才可以对该数据对象进行状态变更。如果你的设计中没有转移结构体对象所有权,但又要保证结构体内部状态数据能在多个goroutine之间同步访问,那么你可以使用sync包提供的低级同步原语来实现,比如最常用的sync.Mutex。

(3)需要通知具体协程的用channel

二、使用sync包的注意事项------使用sync包中类型时,推荐通过闭包方式或传递类型实例(或包裹该类型的类型实例)的地址或指针的方式进行,这是使用sync包最值得注意的事项。

// $GOROOT/src/sync/mutex.go
type Mutex struct {
        state int32
        sema  uint32
}
// Values containing the types defined in this package should not be copied.
// 不应复制那些包含了此包中类型的值
// $GOROOT/src/sync/mutex.go
// A Mutex must not be copied after first use. (禁止复制首次使用后的Mutex)
// $GOROOT/src/sync/rwmutex.go
// A RWMutex must not be copied after first use.(禁止复制首次使用后的RWMutex)
// $GOROOT/src/sync/cond.go
// A Cond must not be copied after first use.(禁止复制首次使用后的Cond)
...
// chapter6/sources/go-sync-package-2.go
type foo struct {
    n int
    sync.Mutex
}
func main() {
    f := foo{n: 17}
        go func(f foo) {
        for {
            log.Println("g2: try to lock foo...")
            f.Lock()
            log.Println("g2: lock foo ok")
            time.Sleep(3 * time.Second)
            f.Unlock()
            log.Println("g2: unlock foo ok")
        }
    }(f)
    f.Lock()
    log.Println("g1: lock foo ok")
    // 在Mutex首次使用后复制其值
    go func(f foo) {
        for {
            log.Println("g3: try to lock foo...")
            f.Lock()
            log.Println("g3: lock foo ok")
            time.Sleep(5 * time.Second)
            f.Unlock()
            log.Println("g3: unlock foo ok")
        }
    }(f)
    time.Sleep(1000 * time.Second)
    f.Unlock()
    log.Println("g1: unlock foo ok")
}
$go run go-sync-package-2.go 
2020/02/08 21:16:46 g1: lock foo ok
2020/02/08 21:16:46 g2: try to lock foo...
2020/02/08 21:16:46 g2: lock foo ok
2020/02/08 21:16:46 g3: try to lock foo...
2020/02/08 21:16:49 g2: unlock foo ok
2020/02/08 21:16:49 g2: try to lock foo...
2020/02/08 21:16:49 g2: lock foo ok
2020/02/08 21:16:52 g2: unlock foo ok
2020/02/08 21:16:52 g2: try to lock foo...
2020/02/08 21:16:52 g2: lock foo ok
...
//g3阻塞

三、互斥锁还是读写锁

在并发量较小的情况下,互斥锁性能更好。
读写锁适合应用在具有一定并发量且读多写少的场合。

四、条件变量-Cond -- 主协程通知子协程执行

type Cond struct {
	L Locker
}
// 创建一个带锁的条件变量,Locker 通常是一个 *Mutex 或 *RWMutex
func NewCond(l Locker) *Cond
// 唤醒所有因等待条件变量 c 阻塞的 goroutine
func (c *Cond) Broadcast()
// 唤醒一个因等待条件变量 c 阻塞的 goroutine
func (c *Cond) Signal()
// 等待 c.L 解锁并挂起 goroutine,在稍后恢复执行后,Wait 返回前锁定 c.L,只有当被 Broadcast 和 Signal 唤醒,Wait 才能返回
func (c *Cond) Wait()
//!!!!!!在调用 Signal,Broadcast 之前,应确保目标 Go 程进入 Wait 阻塞状态!!!!!!
package main
import (
    "fmt"
    "sync"
    "time"
)
var locker = new(sync.Mutex)
var cond = sync.NewCond(locker)
func main() {
    for i := 0; i < 10; i++ {
        go func(x int) {
            cond.L.Lock()         //获取锁
            defer cond.L.Unlock() //释放锁
            cond.Wait()           //等待通知,阻塞当前goroutine
            fmt.Println(x)
        }(i)
    }   
    time.Sleep(time.Second * 1)	// 睡眠1秒,使所有goroutine进入 Wait 阻塞状态
    fmt.Println("Signal...")
    cond.Signal()               // 1秒后下发一个通知给已经获取锁的goroutine
    time.Sleep(time.Second * 1)
    fmt.Println("Signal...")
    cond.Signal()               // 1秒后下发下一个通知给已经获取锁的goroutine
    time.Sleep(time.Second * 1)
    cond.Broadcast()            // 1秒后下发广播给所有等待的goroutine
    fmt.Println("Broadcast...")
    time.Sleep(time.Second * 1)	// 睡眠1秒,等待所有goroutine执行完毕
}

五、sync.WaitGroup -- 主协程等待子协程先执行

func TestWaitgroup(t *testing.T) {
   var wg sync.WaitGroup
   // 计数器 +2
   wg.Add(2)
   go func() {
      dosomething()
      // 计数器 -1
      wg.Done()
   }()
   go func() {
      dosomething()
      // 计数器 -1
      wg.Done()
   }()
   // 阻塞。计数器为 0 的时候,Wait 返回
   wg.Wait()
}

六、sync.Once--实现单例模式

once.Do会等待f执行完毕后才返回,这期间其他执行once.Do函数的goroutine(如上面运行结果中的goroutine 2~5)将会阻塞等待;Do函数返回后,后续的goroutine再执行Do函数将不再执行f并立即返回(如上面运行结果中的goroutine 0);即便在函数f中出现panic,sync.Once原语也会认为once.Do执行完毕,后续对once.Do的调用将不再执行f。

七、使用sync.Pool减轻垃圾回收压力-----数据对象缓存池

//它是goroutine并发安全的,可以被多个goroutine同时使用;放入该缓存池中的数据对象的生命是暂时的,随时都可能被垃圾回收掉;缓存池中的数据对象是可以重复利用的,这样可以在一定程度上降低数据对象重新分配的频度,减轻GC的压力;sync.Pool为每个P(goroutine调度模型中的P)单独建立一个local缓存池,进一步降低高并发下对锁的争抢。

//sync.Pool的一个典型应用就是建立像bytes.Buffer这样类型的临时缓存对象池
var bufPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}
// chapter6/sources/go-sync-package-7_test.go 
var bufPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}
func writeBufFromPool(data string) {
    b := bufPool.Get().(*bytes.Buffer)
    b.Reset()
    b.WriteString(data)
    bufPool.Put(b)
}
func writeBufFromNew(data string) *bytes.Buffer {
    b := new(bytes.Buffer)
    b.WriteString(data)
    return b
}

//(1)限制要放回缓存池中的数据对象大小
//(2)建立多级缓存池

八、sync.map

//sync.map并发安全
var m sync.Map
// 1. 写
m.Store("k1", 1)
m.Store("k2", 2)

// 2. 读
i, _ := m.Load("k1")
fmt.Println(i.(int))

// 3. 遍历
m.Range(func(key, value interface{}) bool {
    k := key.(string)
    v := value.(int)
    fmt.Println(k, v)
    return true
})

// 4. 删除
m.Delete("k1")
i, ok := m.Load("k1")
fmt.Println(i, ok)

// 5. 读取,不存在则写入
m.LoadOrStore("k3", 3)
i, _ = m.Load("k3")
fmt.Println(i)
posted @ 2023-11-10 20:47  longan55  阅读(29)  评论(0)    收藏  举报