golang同步之sync包

本文介绍了Go语言中的并发机制及同步工具,包括互斥锁Mutex、条件变量Cond、等待组WaitGroup、单例模式Once和对象池Pool等。通过具体实例展示了如何使用这些工具确保并发程序的一致性和效率。

golang中实现并发非常简单,只需在需要并发的函数前面添加关键字"go",但是如何处理go并发机制中不同goroutine之间的同步与通信,golang 中提供了sync包来解决相关的问题,当然还有其他的方式比如channel,原子操作atomic等等,这里先介绍sync包的用法。

sync 包提供了互斥锁这类的基本的同步原语.除 Once 和 WaitGroup 之外的类型大多用于底层库的例程。更高级的同步操作通过信道与通信进行。

type Cond

    func NewCond(l Locker) *Cond

    func (c *Cond) Broadcast()
    func (c *Cond) Signal()
    func (c *Cond) Wait()
type Locker
type Mutex
    func (m *Mutex) Lock()
    func (m *Mutex) Unlock()
type Once
    func (o *Once) Do(f func())
type Pool
    func (p *Pool) Get() interface{}
    func (p *Pool) Put(x interface{})
type RWMutex
    func (rw *RWMutex) Lock() 写锁
    func (rw *RWMutex) RLock()                       读锁
    func (rw *RWMutex) RLocker() Locker
    func (rw *RWMutex) RUnlock()
    func (rw *RWMutex) Unlock()
type WaitGroup
    func (wg *WaitGroup) Add(delta int)
    func (wg *WaitGroup) Done()
    func (wg *WaitGroup) Wait()
    golang中的同步是通过sync.WaitGroup来实现的.WaitGroup的功能:它实现了一个类似队列的结构,可以一直向队列中添加任务,当任务完成后便从队列中删除,如果队列中的任务没有完全完成,可以通过Wait()函数来出发阻塞,防止程序继续进行,直到所有的队列任务都完成为止.
WaitGroup总共有三个方法:
Add(delta int), Done(), Wait()。Add:添加或者减少等待goroutine的数量
Done:相当于Add(-1)
Wait:执行阻塞,直到所有的WaitGroup数量变成0
package main

import (
	"fmt"
	"sync"
)

var waitgroup sync.WaitGroup

func function(i int) {
	fmt.Println(i)
	waitgroup.Done() //任务完成,将任务队列中的任务数量-1,其实.Done就是.Add(-1)
}

func main() {
	for i := 0; i < 10; i++ {
	    //每创建一个goroutine,就把任务队列中任务的数量+1
		waitgroup.Add(1) 
		go function(i)
	}
	//这里会发生阻塞,直到队列中所有的任务结束就会解除阻塞
	waitgroup.Wait() 
}
      程序中需要并发,需要创建多个goroutine,并且一定要等这些并发全部完成后才继续接下来的程序执行.WaitGroup的特点是Wait()可以用来阻塞直到队列中的所有任务都完成时才解除阻塞,而不需要sleep一个固定的时间来等待
    接下来看cond用法,很简单一个goroutine等待另外的goroutine发送通知唤醒。
package main

import (
	"fmt"
	"sync"
	"time"
)
var locker = new(sync.Mutex)
var cond = sync.NewCond(locker)
var waitgroup sync.WaitGroup

func test(x int) {
	cond.L.Lock()
	//等待通知,阻塞在此
	cond.Wait()
	fmt.Println(x)
	time.Sleep(time.Second * 1)
	defer func() {
		cond.L.Unlock()//释放锁
		waitgroup.Done()
	}()
}

func main() {
	for i := 0; i < 10; i++ {
		go test(i)
		waitgroup.Add(1);
	}
	fmt.Println("start all")
	time.Sleep(time.Second * 1)
	// 下发一个通知给已经获取锁的goroutine
	cond.Signal()
	time.Sleep(time.Second * 1)
	// 下发一个通知给已经获取锁的goroutine
	cond.Signal()
	time.Sleep(time.Second * 1)
	//下发广播给所有等待的goroutine
	fmt.Println("start Broadcast")
	cond.Broadcast()
	waitgroup.Wait()
}
    然后看Once,它可以保证代码段植段只被执行一次,可以用来实现单例。
   
package main

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

func main() {
	var once sync.Once
	onceBody := func() {
		time.Sleep(3e9)
		fmt.Println("Only once")
	}
	done := make(chan bool)
	for i := 0; i < 10; i++ {
		j := i
		go func(int) {
			once.Do(onceBody)
			fmt.Println(j)
			done <- true
		}(j)
	}
	<-done
	time.Sleep(3e9)
}
    用once可以保证上面的oncebody被执行一次,即使被多次调用,内部用一个atmoic int字段标示是否被执行过,和一个锁来实现,具体的可以看go的源码,syc目录下的once.go
    然后说道pool,说白了就是一个对象池,这个类设计的目的是用来保存和复用临时对象,以减少内存分配,降低CG压力。
    Get返回Pool中的任意一个对象。如果Pool为空,则调用New返回一个新创建的对象。如果没有设置New,则返回nil。还有一个重要的特性是,放进Pool中的对象,会在说不准什么时候被回收掉。所以如果事先Put进去100个对象,下次Get的时候发现Pool是空也是有可能的。不过这个特性的一个好处就在于不用担心Pool会一直增长,因为Go已经帮你在Pool中做了回收机制。这个清理过程是在每次垃圾回收之前做的。垃圾回收是固定两分钟触发一次,而且每次清理会将Pool中的所有对象都清理掉!
   
func main(){
	// 建立对象
	var pipe = &sync.Pool{New:func()interface{}{return "Hello,BeiJing"}}
	// 准备放入的字符串
	val := "Hello,World!"
	// 放入
	pipe.Put(val)
	// 取出
	log.Println(pipe.Get())
	// 再取就没有了,会自动调用NEW
	log.Println(pipe.Get())
} 
   最后 RWMutex读写锁,RWMutex有两种锁写锁和读锁,用法也有不同,首先读锁可以同时加多个,但是写锁就不行当你试图加第二个写锁时就回导致当前的goroutine或者线程阻塞,但是这里的读锁就不会,那他有什么作用呢。
   当有读锁,试图加写锁会阻塞,当有写锁,试图加读锁时会阻塞,当有读锁,试图加读锁时不会阻塞,这样有什么好处呢,当我们有一种数据读操作远远多于写操作时,当我们读时,如果加mutex或者写锁,会大大影响其他线程,因为我们大多数是读操作,因此如果我们加读锁,就不会影响其他线程的读操作,同时有线程写时也能保证数据的同步。最后一点很重要,不论是读锁还是写锁lock和unlock时一一对应的,unlock前一 定要有lock,就像c++的new和delete,一定要注意。
   下 面看两个例子:来源:点击打开链接
    随便读:注意此时此时不能写。
 
package main

import (
    "sync"
    "time"
)

var m *sync.RWMutex

func main() {
    m = new(sync.RWMutex)
    
    // 多个同时读
    go read(1)
    go read(2)

    time.Sleep(2*time.Second)
}

func read(i int) {
    println(i,"read start")

    m.RLock()
    println(i,"reading")
    time.Sleep(1*time.Second)
    m.RUnlock()    

    println(i,"read over")
}
   写的时候不可读也不可写
   
package main

import (
    "sync"
    "time"
)

var m *sync.RWMutex

func main() {
    m = new(sync.RWMutex)
    
    // 写的时候啥也不能干
    go write(1)
    go read(2)
    go write(3)

    time.Sleep(2*time.Second)
}

func read(i int) {
    println(i,"read start")

    m.RLock()
    println(i,"reading")
    time.Sleep(1*time.Second)
    m.RUnlock()    

    println(i,"read over")
}

func write(i int) {
    println(i,"write start")

    m.Lock()
    println(i,"writing")
    time.Sleep(1*time.Second)
    m.Unlock()

    println(i,"write over")
}
Golang的`sync`是Go语言中用于处理并发控制和同步的重要工具集,它提供了多种机制来确保多个goroutine之间的安全协作。以下是对`sync`中主要功能和使用方法的详细解析: ### 1. `sync.WaitGroup` `sync.WaitGroup`用于等待一组goroutine完成任务。其核心思想是通过计数器来追踪正在运行的goroutine数量。当计数器归零时,所有等待的goroutine可以继续执行。 - **基本用法**: - 使用`Add(n)`方法增加等待的goroutine数量。 - 每个goroutine执行完成后调用`Done()`方法,相当于计数器减一。 - 主goroutine通过调用`Wait()`方法阻塞,直到计数器归零。 ```go package main import ( "fmt" "sync" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() fmt.Printf("Worker %d starting\n", id) // 模拟工作 fmt.Printf("Worker %d done\n", id) } func main() { var wg sync.WaitGroup for i := 1; i <= 5; i++ { wg.Add(1) go worker(i, &wg) } wg.Wait() fmt.Println("All workers done") } ``` - **注意事项**: - `WaitGroup`不能被复制,通常应作为指针传递。 - `Add`方法应在启动goroutine之前调用,以避免竞争条件。 - 调用`Done()`时应使用`defer`确保即使发生panic也能正确减少计数器[^1]。 ### 2. `sync.Once` `sync.Once`用于确保某个函数在整个程序生命周期内只执行一次。它常用于单次初始化操作,例如加载配置文件或初始化数据库连接池。 - **基本用法**: - 使用`Once.Do(f)`方法,其中`f`是一个无参数的函数。 - 即使多个goroutine同时调用`Once.Do(f)`,函数`f`也只会被执行一次。 ```go package main import ( "fmt" "sync" ) var once sync.Once func initialize() { fmt.Println("Initializing...") } func main() { once.Do(initialize) once.Do(initialize) // 这次调用不会执行 } ``` - **注意事项**: - `Once`变量应作为全局变量或级变量使用,以确保其在整个程序生命周期内有效。 - `Once.Do(f)`中的`f`不应含长时间运行的操作,以免阻塞其他goroutine[^2]。 ### 3. `sync.Mutex` `sync.Mutex`是一种互斥锁,用于保护共享资源免受并发访问的影响。它有两种状态:已锁定和未锁定。 - **基本用法**: - 使用`Lock()`方法获取锁。 - 使用`Unlock()`方法释放锁。 - 在Go 1.18及以上版本中,`Mutex`还支持`TryLock()`方法,允许非阻塞地尝试获取锁。 ```go package main import ( "fmt" "sync" ) var ( counter = 0 mutex sync.Mutex ) func increment() { mutex.Lock() defer mutex.Unlock() counter++ fmt.Println("Counter:", counter) } func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() increment() }() } wg.Wait() } ``` - **注意事项**: - 必须在访问共享资源前调用`Lock()`,并在操作完成后调用`Unlock()`。 - 使用`defer`确保即使发生panic也能正确释放锁。 - 避免死锁:确保锁的获取和释放顺序一致,避免循环依赖[^3]。 ### 4. `sync.RWMutex` `sync.RWMutex`是读写锁的实现,允许多个读操作同时进行,但写操作必须独占锁。 - **基本用法**: - 使用`RLock()`和`RUnlock()`进行读操作加锁和解锁。 - 使用`Lock()`和`Unlock()`进行写操作加锁和解锁。 ```go package main import ( "fmt" "sync" ) var ( data = make(map[string]int) rwMutex sync.RWMutex ) func read(key string) { rwMutex.RLock() defer rwMutex.RUnlock() fmt.Println("Read:", data[key]) } func write(key string, value int) { rwMutex.Lock() defer rwMutex.Unlock() data[key] = value fmt.Println("Write:", key, "=", value) } func main() { write("a", 1) read("a") } ``` - **注意事项**: - 读锁可以被多个goroutine同时持有,但写锁只能被一个goroutine持有。 - 写锁优先级高于读锁,因此在高并发场景下,频繁的写操作可能导致读操作饥饿。 ### 5. `sync.Cond` `sync.Cond`用于实现条件变量,允许goroutine等待某个条件成立后再继续执行。 - **基本用法**: - 使用`NewCond()`创建条件变量。 - 使用`Wait()`方法等待条件成立。 - 使用`Signal()`或`Broadcast()`通知等待的goroutine。 ```go package main import ( "fmt" "sync" ) var ( cond = sync.NewCond(&sync.Mutex{}) ready = false ) func waitUntilReady() { cond.L.Lock() for !ready { cond.Wait() } fmt.Println("Ready!") cond.L.Unlock() } func setReady() { cond.L.Lock() ready = true cond.Broadcast() cond.L.Unlock() } func main() { go waitUntilReady() setReady() } ``` - **注意事项**: - 条件检查应在`Wait()`之前进行,以避免错过信号。 - 使用`Broadcast()`时应谨慎,因为它会唤醒所有等待的goroutine。 ### 6. `sync.Pool` `sync.Pool`用于缓存临时对象,减少内存分配和垃圾回收的压力。它适用于需要频繁创建和销毁的对象。 - **基本用法**: - 使用`Put()`方法将对象放入池中。 - 使用`Get()`方法从池中获取对象。 ```go package main import ( "fmt" "sync" ) var pool = sync.Pool{ New: func() interface{} { return "default" }, } func main() { pool.Put("hello") fmt.Println(pool.Get()) // 输出 "hello" fmt.Println(pool.Get()) // 池为空时输出 "default" } ``` - **注意事项**: - `sync.Pool`不保证对象一定存在,因此不能用于存储需要持久化的数据。 - `New`函数是可选的,如果未提供,则在池为空时返回`nil`。 ### 7. `sync.Map` `sync.Map`是一个线程安全的键值对映射,适用于并发读写的场景。 - **基本用法**: - 使用`Store()`方法存储键值对。 - 使用`Load()`方法获取键对应的值。 - 使用`Delete()`方法删除键值对。 ```go package main import ( "fmt" "sync" ) func main() { var m sync.Map m.Store("a", 1) m.Store("b", 2) fmt.Println(m.Load("a")) // 输出 1 m.Delete("a") fmt.Println(m.Load("a")) // 输出 nil } ``` - **注意事项**: - `sync.Map`的性能在高并发场景下优于普通`map`加上互斥锁。 - 它不支持直接遍历,如果需要遍历应使用`Range()`方法。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值