Go-并发任务

目录

一、仅执行一次

1、单例模式。

二、仅需任意任务完成

1、场景

2、代码示例

三、所有任务完成

1、场景

2、代码示例

a、CSP方案

b、WaitGroup方案

四、对象池

1、场景

2、代码示例

五、sync.Pool 对象缓存

1、私有对象和共享池

2、sync.Pool 获取对象的顺序

3、Sync.Pool 对象的返回

4、使用 sync.Pool

5、sync.Pool对象的生命周期

a、sync.Pool的基本用法

b、sync.Pool 在多协程中的应用

6、sync.pool总结

六、总结


一、仅执行一次

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 获取对象的顺序

  1. 先尝试从私有对象获取;
  2. 如果私有对象不存在,则尝试从当前 Processor 的共享池获取;
  3. 如果当前 Processor 的共享池也是空的,那么就尝试从其他 Processor 的共享池获取;
  4. 如果所有协程的共享池都是空的,最后就用用户指定的 New 函数产生一个新的对象返回。

3、Sync.Pool 对象的返回

  1. 如果私有对象不存在,则保存为私有对象;
  2. 如果私有对象已经存在,则放入当前 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总结

  1. 适合于通过复用,降低复杂对象的创建和 GC 代价;
  2. 协程安全,会有锁的开销;
  3. 生命周期受 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-单元测试

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值