go语言中常见的并发任务

仅执行一次

确保在多线程情况下有些任务仅执行一次,类似于单例模式。

如下是 Java 中的单例模式的示例代码。

public class Singleton {
    
    // volatile 修饰,保证多线程下的可见行
 	private static volatile Singleton INSTANCE=null;
    
    // 构造方法用private 修饰,防止外部实例化
 	private Singleton(){
        
    }
    
    // 只能通过 Singleton.getIntance() 的方式获取一个实例对象
 	public static Singleton getIntance(){
        // 双检锁
 		if(INSTANCE==null){
            synchronized (Singleton.class){
                if(INSTANCE==null){
                    INSTANCE = new Singleton();
                }
            }
 		}
 		return INSTANCE;
 	}
}

在 go 语言中可以使用 sync.Once 来创建代理对象。once.Do 指定的方法最多只会被执行一次。

package singleton

import (
	"fmt"
	"sync"
	"testing"
)

type Singleton struct {
	name string
}

var singleInstance *Singleton
var once sync.Once

func GetSingletonObj() *Singleton {
	// 这个方法只会执行一次,因此就不需要判断是否为空,不需要双检锁这样的机制
	once.Do(func() {
		fmt.Println("Create Singleton obj.")
		// new(Singleton) 返回的是一个地址,因此singleInstance需要声明为指针类型
		singleInstance = new(Singleton)
	})
	return singleInstance
}

func TestSingletonObj(t *testing.T) {
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			obj := GetSingletonObj()
			// %p用来输出指针的值、输出地址符, %x输出无符号以十六进制表示的整数
			t.Logf("obj 对象的地址为 %p", obj)
			//t.Logf("obj 对象的地址为 %x", unsafe.Pointer(obj))
			wg.Done()
		}()
	}
	wg.Wait()
}

输出结果如下。从输出中可以发现不同的协程中获得的 obj 对象的地址都是同一个,说明返回的是同一个对象。

=== RUN   TestSingletonObj
Create Singleton obj.
    singleton_test.go:33: obj 对象的地址为 0xc00008a000
    singleton_test.go:33: obj 对象的地址为 0xc00008a000
    singleton_test.go:33: obj 对象的地址为 0xc00008a000
    singleton_test.go:33: obj 对象的地址为 0xc00008a000
    singleton_test.go:33: obj 对象的地址为 0xc00008a000
    singleton_test.go:33: obj 对象的地址为 0xc00008a000
    singleton_test.go:33: obj 对象的地址为 0xc00008a000
    singleton_test.go:33: obj 对象的地址为 0xc00008a000
    singleton_test.go:33: obj 对象的地址为 0xc00008a000
    singleton_test.go:33: obj 对象的地址为 0xc00008a000
--- PASS: TestSingletonObj (0.00s)
PASS

仅需其中一个任务执行完毕

有多个任务,但是我们只需要其中一个任务返回数据即可。比如我们使用聚合搜索,有bing搜索引擎,百度搜素引擎,360搜索引擎,Google搜素引擎,我们查询时,只需要其中一个搜索引擎返回数据即可。
示例代码如下

func runTask(id int) string {
	time.Sleep(10 * time.Millisecond)
	// fmt.Sprintf 格式化返回一个字符串
	return fmt.Sprintf("The result is from %d", id)
}

func FirstResponse() string {
	numOfRunner := 10
	ch := make(chan string)
	for i := 0; i < numOfRunner; i++ {
		go func(i int) {
			ret := runTask(i)
			ch <- ret
		}(i)
	}
	// 一旦chan中有数据,return语句就不会阻塞在这里,就会立即返回
	// 也就是说,只要其中一个 Runner 返回了数据,整个程序就立马结束
	return <-ch
}

func TestFirstResponse(t *testing.T) {
	t.Log(FirstResponse())
}

输出。因为协程的调度机制,每次返回数据都不同,但是有且仅有一条。

=== RUN   TestFirstResponse
    first_response_test.go:29: The result is from 3
--- PASS: TestFirstResponse (0.01s)
PASS

继续看。通过 runtime.NumGoroutine() 可以得到系统正存在的协程数。

func TestFirstResponse(t *testing.T) {
	// runtime.NumGoroutine(),输出当前正存在的协程数
	t.Log("Before goroutine num:", runtime.NumGoroutine())
	t.Log(FirstResponse())
	// 让所有协程都能得到运行
	time.Sleep(time.Second * 1)
	t.Log("After goroutine num:", runtime.NumGoroutine())
}

输出。最开始时 go routine 为2,这是因为该程序是在测试中运行的,测试程序会在主协程中再启动一个子协程进行运行,所以一开始会存在2个协程。然后其中一个协程最先往chan中发送数据,并陷入阻塞等待,等待接收者接收数据,然后 t.Log(FirstResponse()) 所在的协程从chan中接收到数据,最先发送数据的那个协程从阻塞中退出,执行完毕。但是其它的协程依然在往 chan 中发送数据,但是已经没有接收方再接收数据了,所以它们将陷入等待,因此最后还存在 9 + 2(固有的)= 11个协程。这就造成了资源的浪费,会有大量协程阻塞得不到释放

=== RUN   TestFirstResponse
    first_response_test.go:31: Before goroutine num: 2
    first_response_test.go:32: The result is from 4
    first_response_test.go:34: After goroutine num: 11
--- PASS: TestFirstResponse (1.01s)
PASS

解决方法,将 channel换成带 buffer 的 channel,只需要在使用make创建channel时指定buffer大小即可。

func runTask(id int) string {
	time.Sleep(10 * time.Millisecond)
	// fmt.Sprintf 格式化返回一个字符串
	return fmt.Sprintf("The result is from %d", id)
}

func FirstResponse() string {
	numOfRunner := 10
	//带buffer的channel
	ch := make(chan string, numOfRunner)
	for i := 0; i < numOfRunner; i++ {
		go func(i int) {
			ret := runTask(i)
			ch <- ret
		}(i)
	}
	// 一旦chan中有数据,return语句就不会阻塞在这里,就会立即返回
	// 也就是说,只要其中一个 Runner 返回了数据,整个程序就立马结束
	return <-ch
}

func TestFirstResponse(t *testing.T) {
	// runtime.NumGoroutine(),输出当前正存在的协程数
	t.Log("Before goroutine num:", runtime.NumGoroutine())
	t.Log(FirstResponse())
	//sleep 让所有的协程都能运行完毕
	time.Sleep(time.Second * 1)
	t.Log("After goroutine num:", runtime.NumGoroutine())
}

输出如下。会发现运行完毕后,协程的数量变为了2,达到了预期。原因也很简单,带 buffer 的 channel 将发送方与接收方进行了解耦,发送方只需往 buffer 中发送数据,不需要阻塞等待接收方接收数据

=== RUN   TestFirstResponse
    first_response_test.go:32: Before goroutine num: 2
    first_response_test.go:33: The result is from 9
    first_response_test.go:36: After goroutine num: 2
--- PASS: TestFirstResponse (1.01s)
PASS

需要所有任务都完成

方法一:使用 WaitGroup 可以参考我的这篇文章: go语言学习之并发编程

方法二:

func runTask(id int) string {
	time.Sleep(10 * time.Millisecond)
	// fmt.Sprintf 格式化返回一个字符串
	return fmt.Sprintf("The result is from %d", id)
}

func AllResponse() string {
	numOfRunner := 10
	//带buffer的channel
	ch := make(chan string, numOfRunner)
	for i := 0; i < numOfRunner; i++ {
		go func(i int) {
			ret := runTask(i)
			ch <- ret
		}(i)
	}

	finalRet := "\n"
	// 依次去chan中取数据,直到取出所有的数据,然后再返回。相当于做了一次聚合
	for j := 0; j < numOfRunner; j++ {
		finalRet += <-ch + "\n"
	}
	return finalRet
}

func TestFirstResponse(t *testing.T) {
	// runtime.NumGoroutine(),输出当前正存在的协程数
	t.Log("Before goroutine num:", runtime.NumGoroutine())
	t.Log(AllResponse())
	//sleep 让所有的协程都能运行完毕
	time.Sleep(time.Second * 1)
	t.Log("After goroutine num:", runtime.NumGoroutine())
}

输出。从输出中可以看到所有任务都已经被执行了。

=== RUN   TestFirstResponse
    all_response_test.go:37: Before goroutine num: 2
    all_response_test.go:38: 
        The result is from 1
        The result is from 5
        The result is from 7
        The result is from 6
        The result is from 8
        The result is from 9
        The result is from 3
        The result is from 2
        The result is from 0
        The result is from 4
        
    all_response_test.go:41: After goroutine num: 2
--- PASS: TestFirstResponse (1.01s)
PASS

Process finished with exit code 0

使用Buffered Channel实现对象池

池化思想在开发中很常见。对于一比较珍贵的资源,为了避免重复创建或者是创建完毕回收不当造成浪费,我们选择使用一个“池”来容纳这些资源,需要使用的时候,从池中取出资源,使用完毕,又归还到“池”中。比如数据库连接池,Java中的线程池。

在这里插入图片描述
在 go 语言中,我们可以使用 Buffered Channel 来实现这个 “池“。对于一些不易创建且销毁也比较麻烦的资源,我们就可以使用这个 “池" 来容纳这些资源。但是,我们也需要考虑到,Buffered Channel 为了保证在多协程下的同步,同样引入了锁的机制来保证数据的一致性,使用锁,不可避免也会带来相应的开销。因此,在使用 Buffered Channel 时,我们需要考虑:是使用锁带来的消耗大,还是创建资源,销毁资源带来的消耗大。

type ReusableObj struct {

}

type ObjPool struct {
	// 用于缓存可重用对象
	bufChan chan *ReusableObj
}

func NewObjPool(numOfObj int) *ObjPool {
	objPool := ObjPool{}
	objPool.bufChan = make(chan *ReusableObj, numOfObj)
	for i := 0; i < numOfObj; i++ {
		objPool.bufChan <- &ReusableObj{}
	}
	return &objPool
}

// 获取对象
func (p *ObjPool) GetObj(timeout time.Duration) (*ReusableObj, error) {
	select {
	case ret := <-p.bufChan:
		return ret, nil
	case <-time.After(timeout):// 超时控制
		return nil, errors.New("time out")
	}
}

// 释放对象,做的工作是将使用完毕的对象再次放入对象池
func (p *ObjPool) ReleaseObj(obj *ReusableObj) error {
	select {
	case p.bufChan <- obj:
		return nil
	default:
		return errors.New("overflow")
	}
}

func TestObjPool(t *testing.T) {
	// 创建对象池,大小为10,初始会默认往 pool 中放10个对象
	pool := NewObjPool(10)
	for i := 0; i < 11; i++ {
		if v, err := pool.GetObj(time.Second * 1); err != nil {
			t.Error(err)
		} else {
			t.Logf("v的类型为 %T \n", v)
		}
	}
	t.Log("Done")
}

输出如下,由于我们只往 pool 中放了十个对象,但是我们 get 了 11 次,所以,第 11 次会由于 pool 中的对象已经全部被取走了,等待了一秒以后依然无法取得对象,进而会抛出 error。

=== RUN   TestObjPool
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:54: time out
    obj_pool_test.go:62: Done
--- FAIL: TestObjPool (1.00s)

FAIL

继续测试,如果我们往一个已经装满了对象的对象池中继续放元素,就会出现 overflow error。

func TestObjPool(t *testing.T) {
	// 创建对象池,大小为10,初始会默认往 pool 中放10个对象
	pool := NewObjPool(10)
	if err := pool.ReleaseObj(&ReusableObj{}); err != nil {
		t.Error(err)
	}
	t.Log("Done")
}

输出如下。

=== RUN   TestObjPool
    obj_pool_test.go:50: overflow
    obj_pool_test.go:62: Done
--- FAIL: TestObjPool (0.00s)

FAIL

如果我们把对象从对象池中获取以后,使用完毕便进行归还,那么不管重复取多少次,对象池中都还是有对象可以取。

func TestObjPool(t *testing.T) {
	// 创建对象池,大小为10,初始会默认往 pool 中放10个对象
	pool := NewObjPool(10)
	for i := 0; i < 11; i++ {
		if v, err := pool.GetObj(time.Second * 1); err != nil {
			t.Error(err)
		} else {
			t.Logf("v的类型为 %T \n", v)
			if err := pool.ReleaseObj(v); err != nil {
				t.Error(err)
			}
		}
	}
	t.Log("Done")
}

输出如下

=== RUN   TestObjPool
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj 
    obj_pool_test.go:62: Done
--- PASS: TestObjPool (0.00s)
PASS

Process finished with exit code 0

sync.Pool 对象缓存

go 语言中自带了一个 sync.Pool 的工具类,虽然听名字这是一个 pool,可以用来缓存对象,其实不然,每⼀次GC,都会清除 sync.pool 缓存的对象,不能像 Buffered Channel 一样可以长时间保存对象,由于 GC 的发生是go语言内部进行调度的,无法明确 sync.Pool 中缓存对象的生命周期。

注意:Go 1.13对sync.Pool中的对象回收时机策略做出调整。在1.12版本及以前的版本中,在每轮垃圾回收过程中,每个sync.Pool实例中的所有缓存对象都将被无条件回收掉。从1.13版本开始,如果一个sync.Pool实例在上一轮垃圾回收过程结束之后仍然被使用过,则其中的缓存对象将不会被回收掉。此举对于使用sync.Pool来提升效率的程序来说,将大大减少周期性的因为缓存被清除而造成的瞬时效率下降

使用sync.Pool时,同样需要考虑sync.Pool带来的开销,sync.Pool是协程安全,不可避免就会有锁的开销,需要权衡是对象的创建和 GC 代价大还是锁的开销代价大。而且sync.Pool的生命周期受 GC 影响,不适合于做连接池等。

sync.Pool 对象获取

在这里插入图片描述
关于 Processor,可以参照如下 go 语言的协程机制原理图来进行理解。我们知道,在Go语言的并发中,不是以线程为基本的运行单元,而是以协程为基本的运行单元。go 自己实现了一个 Processor,从协程的工作队列中选择一个协程交给系统的线程进行执行。如果这个协程长时间没有被执行完毕,那么 go 语言的 Processor 会选择一个新的系统线程,继续执行协程工作队列中的其它协程。
在这里插入图片描述

sync.Pool 对象的放回

在这里插入图片描述

示例

func TestSyncPool(t *testing.T) {
	pool := &sync.Pool{
		New: func() interface{} {
			fmt.Println("Create a new Object.")
			return 100
		},
	}
	//本次获取时,私有对象不存在,共享池中没有对象,会执行Pool中的New方法,新创建对象并返回
	// 断言
	v := pool.Get().(int)
	fmt.Println(v)
	//第一次Put时,私有对象不存在,共享池中也没有对象,则保存为私有对象
	pool.Put(3)
	//第二次Put时,私有对象已经存在,则将对象放入共享池
	pool.Put(4)
	v1, _ := pool.Get().(int)
	fmt.Println(v1)
	//本次获取时,私有对象已经返回,私有对象不存在了,共享池中有对象,会返回共享池中的对象
	v2, _ := pool.Get().(int)
	fmt.Println(v2)
}

输出如下,符合预期。

=== RUN   TestSyncPool
Create a new Object.
100
3
4
--- PASS: TestSyncPool (0.00s)
PASS

go的1.12版本及以前的版本中,在每轮垃圾回收过程中,每个sync.Pool实例中的所有缓存对象都将被无条件回收掉。从1.13版本开始,如果一个sync.Pool实例在上一轮垃圾回收过程结束之后仍然被使用过,则其中的缓存对象将不会被回收掉。
我是用的 go 的版本为 1.16
在这里插入图片描述
如下,当本轮进行GC前,由于sync.Pool已经被使用过,因此,进行GC后,不会清除sync.Pool中缓存的对象,再次进行Get时,还是能获取到保存到sync.Pool中的对象(如下示例中获取到的是Processor中的私有对象)。

func TestSyncPoolWithGC(t *testing.T) {
	pool := &sync.Pool{
		New: func() interface{} {
			fmt.Println("Create a new Object.")
			return 100
		},
	}
	v := pool.Get().(int)
	fmt.Println(v)
	pool.Put(3)
	// 进行GC
	runtime.GC()
	v1, _ := pool.Get().(int)
	fmt.Println(v1)
}

输出

=== RUN   TestSyncPoolWithGC
Create a new Object.
100
3
--- PASS: TestSyncPoolWithGC (0.00s)
PASS

当连续进行两次GC时,输出的结果就不相同了。当连续进行两次GC时,sync.Pool中缓存的对象已经被清除掉了,因此再次获取时,不管是私有对象还是共享池中都不存在对象,Get时将会调用New函数创建一个新的对象并返回。

func TestSyncPoolWithGC(t *testing.T) {
	pool := &sync.Pool{
		New: func() interface{} {
			fmt.Println("Create a new Object.")
			return 100
		},
	}
	v := pool.Get().(int)
	fmt.Println(v)
	pool.Put(3)
	runtime.GC()
	runtime.GC()
	v1, _ := pool.Get().(int)
	fmt.Println(v1)
}

输出

=== RUN   TestSyncPoolWithGC
Create a new Object.
100
Create a new Object.
100
--- PASS: TestSyncPoolWithGC (0.00s)
PASS

如下,考虑在多协程的情况下使用 sync.Pool 。如下,往sync.Pool中Put了3个数,5个协程取依次去取出一个数据,只能够3个协程取数据,剩余2个协程去取数据时,sync.Pool 中已经没有数据了,因此会执行指定的New函数返回一个新的对象。

func TestSyncPoolInMultiGoroutine(t *testing.T) {
	pool := &sync.Pool{
		New: func() interface{} {
			fmt.Println("Create a new object.")
			return 10
		},
	}

	pool.Put(100)
	pool.Put(200)
	pool.Put(300)

	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			fmt.Println(pool.Get())
			wg.Done()
		}(i)
	}
	wg.Wait()
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本书作者带你一步一步深入这些方法。你将理解 Go语言为何选定这些并发模型,这些模型又会带来什么问题,以及你如何组合利用这些模型的原语去解决问题。学习那些让你在独立且自信的编写与实现任何规模并发系统时所需要用到的技巧和工具。 理解Go语言如何解决并发难以编写正确这一根本问题。 学习并发与并行的关键性区别。 深入到Go语言的内存同步原语。 利用这些模式的原语编写可维护的并发代码。 将模式组合成为一系列的实践,使你能够编写大规模的分布式系统。 学习 goroutine 背后的复杂性,以及Go语言的运行时如何将所有东西连接在一起。 作者简介 · · · · · · Katherine Cox-Buday是一名计算机科学家,目前工作于 Simple online banking。她的业余爱好包括软件工程、创作、Go 语言(igo、baduk、weiquei) 以及音乐,这些都是她长期的追求,并且有着不同层面的贡献。 目录 · · · · · · 前言 1 第1章 并发概述 9 摩尔定律,Web Scale和我们所陷入的混乱 10 为什么并发很难? 12 竞争条件 13 原子性 15 内存访问同步 17 死锁、活锁和饥饿 20 确定并发安全 28 面对复杂性的简单性 31 第2章 对你的代码建模:通信顺序进程 33 并发与并行的区别 33 什么是CSP 37 如何帮助你 40 Go语言并发哲学 43 第3章 Go语言并发组件 47 goroutine 47 sync包 58 WaitGroup 58 互斥锁和读写锁 60 cond 64 once 69 池 71 channel 76 select 语句 92 GOMAXPROCS控制 97 小结 98 第4章 Go语言并发模式 99 约束 99 for-select循环103 防止goroutine泄漏 104 or-channel 109 错误处理112 pipeline 116 构建pipeline的最佳实践 120 一些便利的生成器 126 扇入,扇出 132 or-done-channel 137 tee-channel 139 桥接channel模式 140 队列排队143 context包 151 小结 168 第5章 大规模并发 169 异常传递169 超时和取消 178 心跳 184 复制请求197 速率限制199 治愈异常的goroutine 215 小结 222 第6章 goroutine和Go语言运行时 223 工作窃取223 窃取任务还是续体 231 向开发人员展示所有这些信息 240 尾声 240 附录A 241

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值