一,并发编程
1.sync.WaitGroup可以用于等待一组goroutine完成。它提供了三个方法:
- Add(n int):表示等待n个goroutine完成。
- Done():表示一个goroutine已经完成。
- Wait():阻塞当前goroutine,直到所有Add的数量被Done调用相减为0时才会继续执行。
使用WaitGroup的步骤如下:
- 创建WaitGroup变量wg: wg := sync.WaitGroup{}
- 在每个需要等待的goroutine中调用Add方法增加计数器数量:wg.Add(1)
- 在每个goroutine完成时调用Done方法减少计数器数量:wg.Done()
- 最后,在主函数中调用Wait方法等待所有goroutine完成:wg.Wait()
这样就可以确保在所有goroutines执行完之前,程序不会退出。
2.sync.Cond可以根据条件等待goroutine完成。它提供了三个方法:
- Wait():阻塞当前goroutine,直到被Broadcast或Signal唤醒。
- Signal():唤醒一个等待的goroutine。
- Broadcast():唤醒所有等待的goroutine。
使用sync.Cond的步骤如下:
- 创建一个锁对象l和一个条件变量c: l := sync.Mutex{} c := sync.NewCond(&l)
- 在需要等待的goroutine中获取锁并调用Wait方法进行等待: l.Lock() defer l.Unlock() c.Wait()
- 在满足条件时,通过Signal或Broadcast方法来唤醒等待的goroutine。例如,在另一个goroutine中修改条件并调用Signal或Broadcast方法: l.Lock() // 修改条件 c.Signal() or c.Broadcast() l.Unlock()
- 当条件不再满足时,重复上述过程。
这样就可以实现根据特定条件等待多个goroutines完成。
3.sync.Mutex和sync.RWMutex都可以用来实现并发安全。
sync.Mutex是最常见的互斥锁,它只允许同时有一个goroutine进入临界区执行。当一个goroutine获取到Mutex的锁时,其他goroutine就无法再获取到这个锁,只能等待当前持有锁的goroutine释放锁。在使用Mutex时需要注意以下几点:
- 在临界区代码前调用Lock()方法获取锁,在临界区代码后调用Unlock()方法释放锁。
- 不要嵌套Lock()方法调用,这样容易导致死锁。
- 尽量避免在临界区内执行耗时操作或者阻塞操作。
sync.RWMutex是读写锁,它分为读模式和写模式两种,多个goroutine可以同时获取读锁进行读操作,但只有一个goroutine可以获取写锁进行写操作。在使用RWMutex时需要注意以下几点:
- 在读操作前调用RLock()方法获取读锁,在读操作后调用RUnlock()方法释放读锁;在写操作前调用Lock()方法获取写锁,在写操作后调用Unlock()方法释放写锁。
- 当存在大量的并发读取数据时应该优先考虑使用RWMutex而不是Mutex。
- 写操作会阻塞所有正在进行的读和写操作直到完成为止,因此尽量避免长时间占用写锁。
综上所述,Mutex适合于对于临界区的读写操作不频繁或者只有单个goroutine进行写操作的场景;而RWMutex则适用于大量并发读取数据、少量写入数据的场景。
4.sync.Map是Go语言标准库中提供的线程安全集合,它可以在多个goroutine之间安全地并发读写数据。
使用sync.Map的步骤如下:
- 创建一个新的sync.Map对象:m := sync.Map{}
- 使用Store方法存储键值对:m.Store(key, value)
- 使用Load方法获取指定键的值:value, ok := m.Load(key)
- 使用Delete方法删除指定键的值:m.Delete(key)
- 遍历Map时需要使用Range方法,该方法会自动遍历所有键值对并执行指定操作。例如,可以使用以下代码遍历Map中所有元素并打印它们的键和值:
需要注意的是,当调用Store、Delete等写操作时,必须保证同时只有一个goroutine执行这些操作。如果有多个goroutine同时进行写操作,则可能导致数据竞争问题。
总体来说,sync.Map是一种非常方便且高效的线程安全集合,特别适用于大规模数据处理和分布式系统中共享变量的场景。
5.sync.Pool是Go语言标准库中提供的一个对象池,它可以用来存储和重复利用一些临时对象。通过使用sync.Pool,我们可以避免创建大量的临时对象,从而减少GC的负担。
使用sync.Pool的步骤如下:
- 创建一个新的sync.Pool对象:p := sync.Pool{}
- 使用Put方法将对象放入池中:p.Put(obj)
- 使用Get方法从池中获取对象:obj := p.Get()
- 当不再需要这个对象时,调用Put方法将其放回到池中,以便后续重复利用。
需要注意的是,sync.Pool并不能保证每次都能获取到同一个对象。如果池中没有可用的对象,则会创建一个新的,并将其返回给调用者。因此,在使用Pool时,必须假设获取到的对象可能与之前获取到的不同。
此外,由于Pool会在某些条件下自动清空其中存储的所有临时对象(例如GC触发时),因此我们不能依赖于Pool来存储长期有效或具有状态的数据。
总体来说,sync.Pool是一种非常实用和高效的工具,特别适合处理那些频繁创建和销毁、无状态或很少状态变化的临时对象。sync。pool实现对象的重复利用。
6.sync.Once是Go语言标准库中提供的一个工具,它可以用来保证某个函数只被执行一次,常用于实现数据懒加载。
使用sync.Once的步骤如下:
- 定义一个全局变量或结构体,并在其中定义需要懒加载的数据和一个Once对象:var data []string var once sync.Once
- 在需要获取懒加载数据的地方,调用Do方法,并将其作为参数传入一个匿名函数,在匿名函数中进行数据初始化操作:func GetData() []string { once.Do(func() { data = loadDataFromDatabase() }) return data }
- 当第一次调用GetData时,once.Do会执行匿名函数并将data赋值为从数据库中读取到的数据。以后再调用GetData时,由于once已经被执行过了,所以直接返回之前保存的data。
需要注意的是,在使用sync.Once时必须保证传入Do方法的匿名函数是幂等性的(即无论执行多少次都得到相同结果),否则可能会导致程序出现不可预期的错误。
总体来说,sync.Once非常适合处理那些只需要初始化一次、但是又不能提前初始化或者初始化代价很大的数据。它可以保证线程安全和高效地完成数据懒加载。
7.在Go语言中,Context是一个非常有用的工具,它提供了一种机制来控制协程的执行。Context可以跨多个goroutine和函数传递,并且可以用于取消任务、超时等操作。
在使用goroutine的过程中,往往需要控制goroutine的退出。这时就可以利用Context来实现协程的优雅退出。
下面我们通过一个例子来解析如何使用context控制协程退出:
work()在main函数中我们首先创建了一个根Context对象background,并调用WithCancel方法返回一个新的带有cancel方法的子Context对象ctx。然后启动了一个新的goroutine,并将ctx作为参数传入work()函数中。
接着主线程sleep 5秒钟之后调用cancel方法取消ctx,此时会向所有基于该Context对象派生出的goroutine发送Done信号,work()函数会打印"work done!"并返回,结束协程的执行。
最后主线程再sleep 2秒钟等待工作协程退出。这样我们就利用Context对象优雅地控制了协程的退出。golang的context控制协程退出解析的更多细节。
8.在Go语言中,atomic包提供了一组原子操作函数,用于安全地读写共享变量。这些函数通过使用特殊的CPU指令来确保对变量的读写操作是原子的(不可分割的),从而避免了多个协程同时访问同一个变量时出现的数据竞争问题。
atomic包支持以下几种类型的原子操作:
- Add:以原子方式将给定值与指定地址中存储的值相加,并返回新值。
- CompareAndSwap:比较指定地址中存储的值和旧值,如果相等则将新值存储到该地址,并返回true;否则不做任何操作并返回false。
- Load:以原子方式获取指定地址中存储的值,并返回该值。
- Store:以原子方式将给定值存储到指定地址中。
下面是一个例子:
counteratomic.AddInt64atomic.LoadInt64atomic.SwapInt64需要注意的是,虽然原子操作可以确保对变量的读写操作是原子的,但它并不能解决所有并发问题。例如,在一个复杂的应用程序中,可能还需要考虑协程之间的同步和通信等问题。因此,在使用原子操作时,应该仔细考虑应用场景和具体需求,并结合其他技术手段来实现完整的并发控制方案。