目录
一、仅执行一次
1、单例模式。
确保在多线程的情况下,某段代码只执行一次,且线程安全。
sync.Once能确保里面的 Do() 方法在多线程的情况下只会被执行一次。
package single_ton
import (
"fmt"
"sync"
"testing"
"unsafe"
)
type Singleton struct {
}
var singleInstance *Singleton
var once sync.Once
//获取一个单例对象
func GetSingletonObj() *Singleton {
once.Do(func() {
fmt.Println("Create a singleton Obj")
singleInstance = new(Singleton)
})
return singleInstance
}
//启动多个协程,测试我们单例对象是否只创建了一次
func TestGetSingletonObj(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
obj := GetSingletonObj()
fmt.Printf("%x\n", unsafe.Pointer(obj))
wg.Done()
}()
}
wg.Wait()
}
/*
=== RUN TestGetSingletonObj
Create a singleton Obj
124cfb8
124cfb8
124cfb8
124cfb8
124cfb8
--- PASS: TestGetSingletonObj (0.00s)
PASS
*/
我们可以看到上面的调试结果,启动多个协程,只打印了一次 "Create a singleton Obj",而且多个协程返回对象的地址是完全一样的,说明单例模式正常。
二、仅需任意任务完成
1、场景
当我们需要执行许多并发任务,但是只要任意一个任务执行完毕,就可以将结果返回给用户,例如我们想百度和google发起请求,任意一个请求返回结果即可。
2、代码示例
package util_anyone_reply
import (
"fmt"
"runtime"
"testing"
"time"
)
//从网站上执行搜索功能
func searchFromWebSite(webSite string) string {
time.Sleep(10 * time.Millisecond)
return fmt.Sprintf("search from %s", webSite)
}
//收到第一个结果后立刻返回
func FirstResponse() string {
var arr = [2]string{"baidu", "google"}
//这里用 buffer channel 很重要,否则可能导致剩下的协程会被阻塞在那里,
//当阻塞的协程达到一定量后,最终可能导致服务器资源耗尽而出现重大故障
ch := make(chan string, len(arr))
for _, val := range arr {
go func(v string) {
//拿到所有结果放入 channel
ch <- searchFromWebSite(v)
}(val)
}
//这里没有使用 WaitGroup,因为我们的需求是当 channel 收到第一个消息后就立刻返回
return <-ch
}
func TestFirstResponse(t *testing.T) {
t.Log("Before:", runtime.NumGoroutine())
t.Log(FirstResponse())
t.Log("After:", runtime.NumGoroutine())
}
/*
=== RUN TestFirstResponse
first_response_test.go:35: Before: 2
first_response_test.go:36: search from baidu
first_response_test.go:37: After: 3
--- PASS: TestFirstResponse (0.01s)
PASS
*/
三、所有任务完成
1、场景
有时候我们所有任务都完成才进入下一个环节,s我们下单成功后,只有积分和优惠券都赠送了才显示所有优惠赠送成功。
2、代码示例
我们这里采用了两种方案,一种是 CSP 的方案,另一种是 WaitGroup 的方案。
a、CSP方案
package until_all_done
import (
"fmt"
"runtime"
"sync"
"testing"
"time"
)
//送豪礼方法
func sendGift(gift string) string {
time.Sleep(10 * time.Millisecond)
return fmt.Sprintf("送%s", gift)
}
//使用 CSP 拿到所有的结果才返回
func CspAllResponse() []string {
var arr = [2]string{"优惠券", "积分"}
//这里用 buffer channel 很重要,否则可能导致剩下的协程会被阻塞在那里,
//当阻塞的协程达到一定量后,最终可能导致服务器资源耗尽而出现重大故障
ch := make(chan string, len(arr))
for _, val := range arr {
go func(v string) {
//拿到所有结果放入 channel
ch <- sendGift(v)
}(val)
}
var finalRes = make([]string, len(arr), len(arr))
//等到所有的的协程都执行完毕,把结果一起返回
for i :=0; i < len(arr); i++ {
finalRes[i] = <-ch
}
return finalRes
}
func TestAllResponse(t *testing.T) {
t.Log("Before:", runtime.NumGoroutine())
t.Log(CspAllResponse())
t.Log("After:", runtime.NumGoroutine())
}
/*
=== RUN TestFirstResponse
until_all_done_test.go:61: Before: 2
until_all_done_test.go:62: [送优惠券 送积分]
until_all_done_test.go:64: After: 2
--- PASS: TestFirstResponse (0.02s)
PASS
*/
b、WaitGroup方案
package until_all_done
import (
"fmt"
"runtime"
"sync"
"testing"
"time"
)
//送豪礼方法
func sendGift(gift string) string {
time.Sleep(10 * time.Millisecond)
return fmt.Sprintf("送%s", gift)
}
//使用 WaitGroup 拿到所有的结果才返回
func WaitGroupAllResponse() []string {
var arr = [2]string{"优惠券", "积分"}
var finalRes = make([]string, 0, len(arr))
var wg sync.WaitGroup
for _, val := range arr {
wg.Add(1)
go func(v string) {
//拿到所有结果放入 channel
ret := sendGift(v)
finalRes = append(finalRes, ret)
wg.Done()
}(val)
}
wg.Wait()
return finalRes
}
func TestAllResponse(t *testing.T) {
t.Log("Before:", runtime.NumGoroutine())
t.Log(WaitGroupAllResponse())
t.Log("After:", runtime.NumGoroutine())
}
/*
=== RUN TestFirstResponse
until_all_done_test.go:61: Before: 2
until_all_done_test.go:63: [送优惠券 送积分]
until_all_done_test.go:64: After: 2
--- PASS: TestFirstResponse (0.02s)
PASS
*/
四、对象池
1、场景
在我们日常的开发中,经常会有像数据库连接,网络连接等,我们经常需要把它们池话,以免对象被重复创建。在 Go 语言中我们使用 buffered channel 实现对象池。我们可以通过设定 buffer 的大小来设定池的大小,我们可以从这个 buffer 池中拿到一个对象,用完了又还回来。
2、代码示例
package obj_pool
import (
"errors"
"fmt"
"testing"
"time"
)
//可重用对象
type Reusable struct {
}
//对象池
type ObjPool struct {
//用于缓存可重用对象
bufChan chan *Reusable
}
//创建一个包含多个可重用对象的对象池
func NewObjPool(numObjPool int) *ObjPool {
//声明对象池
objPool := ObjPool{}
//初始化 objPool.bufChan 为一个 channel
objPool.bufChan = make(chan *Reusable, numObjPool)
//往 objPool 对象池里面放多个可重用对象
for i:= 0;i < numObjPool ; i++ {
objPool.bufChan <- &Reusable{}
}
return &objPool
}
//从对象池拿到一个对象
func (objPool *ObjPool) GetObj(timeout time.Duration) (*Reusable, error) {
select {
case ret := <- objPool.bufChan:
return ret, nil
case <-time.After(timeout): //超时控制
return nil, errors.New("time out")
}
}
//将可重用对象还回对象池
func (objPool *ObjPool) ReleaseObj(ReusableObj *Reusable) error {
select {
case objPool.bufChan <- ReusableObj:
return nil
default:
return errors.New("overflow")
}
}
//从对象池里面拿出对象,用完了又放回去
func TestObjPool(t *testing.T) {
pool := NewObjPool(3)
for i := 0; i < 3; i++ {
if obj, err := pool.GetObj(time.Second * 1); err != nil {
t.Error(err)
} else {
fmt.Printf("%T\n", obj)
if err := pool.ReleaseObj(obj); err != nil {
t.Error(err)
}
}
}
t.Log("Done")
}
五、sync.Pool 对象缓存
其实 sync.Pool 并不是对象池的类,而是个对象缓存,叫sync.Cache 更贴切,不要被它的名字所误导了。
1、私有对象和共享池
sync.Pool 有两个重要的概念,私有对象和共享池。
- 私有对象是协程安全的,写入的时候不需要锁。
- 而共享池是协程不安全的,写入的时候需要锁。
2、sync.Pool 获取对象的顺序
- 先尝试从私有对象获取;
- 如果私有对象不存在,则尝试从当前 Processor 的共享池获取;
- 如果当前 Processor 的共享池也是空的,那么就尝试从其他 Processor 的共享池获取;
- 如果所有协程的共享池都是空的,最后就用用户指定的 New 函数产生一个新的对象返回。
3、Sync.Pool 对象的返回
- 如果私有对象不存在,则保存为私有对象;
- 如果私有对象已经存在,则放入当前 Processor 子池的共享池中。
4、使用 sync.Pool
//使用 New 关键字创建新对象
pool := &sync.Pool{
New: func() interface{} {
return 0
},
}
//从 pool 中获取一个对象,因为返回的是 interface,所有要自己做断言
array := pool.Get().(int)
//往 pool 中放入一个对象
pool.Put(10)
5、sync.Pool对象的生命周期
- 每次GC都会清除 sync.pool 缓存对象
- 对象的缓存有效期为下一次 GC之前
由于GC是系统调度的,我们没办法控制,而GC又会自动清除 sync.pool 对象,所以如果我们想长时间控制一个连接的生命周期那就不行。
a、sync.Pool的基本用法
package sync_pool
import (
"fmt"
"sync"
"testing"
)
//调试 sync.Pool 对象
func TestSyncPool(t *testing.T) {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Create a new object")
return 0
},
}
//第一次从池中获取对象,我们知道它一定是空的,所有肯定会调用 New 方法去创建一个新对象
v := pool.Get().(int)
fmt.Println(v) //0
//放一个不存在的对象,它会优先放入私有对象
pool.Put(10)
//此时私有对象已经存在了,所有会优先拿到私有对象的值
v1 := pool.Get().(int)
fmt.Println(v1) //10
//模拟系统调用GC, GC会清除 sync.pool中缓存的对象
//runtime.GC()
}
b、sync.Pool 在多协程中的应用
package sync_pool
import (
"fmt"
"sync"
"testing"
)
//调试 sync.Pool 在多个协程中的应用场景
func TestSyncPoolInMultiGoroutine(t *testing.T) {
pool := sync.Pool{
New: func() interface{} {
return 0
},
}
pool.Put(11)
pool.Put(12)
var wg sync.WaitGroup
for i := 0; i < 5 ; i++ {
wg.Add(1)
go func() {
v, _ := pool.Get().(int)
fmt.Println(v)
wg.Done()
}()
}
wg.Wait()
}
/*
=== RUN TestSyncPoolInMultiGroutine
11
12
0
0
0
--- PASS: TestSyncPoolInMultiGroutine (0.00s)
PASS
*/
6、sync.pool总结
- 适合于通过复用,降低复杂对象的创建和 GC 代价;
- 协程安全,会有锁的开销;
- 生命周期受 GC 影响,不适合于做连接池等需要自己管理生命周期的资源的池化。
六、总结
- sync.Once能确保里面的 Do() 方法用来生成单例。
- 通过 buffer channel 拿到第一个结果后立即返回实现仅需任意任务完成即可的功能。
- 通过 buffer channel 拿到所有结果后才返回实现需要所有任务都完成的功能;
- 通过 WaitGroup 也可以实现需要所有任务都完成的功能。
- 通过 buffered channel 实现对象池,用来池化我们的数据库连接等。
- sync.pool 适合于通过复用,降低复杂对象的创建和 GC 代价,但是不适用于做需要自己管理生命周期的连接池,如数据库连接池等。
注:这篇博文是我学习中的总结,如有转载请注明出处:
https://blog.csdn.net/DaiChuanrong/article/details/118558641
上一篇:Go-Context与任务取消
下一篇:Go-单元测试