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
    评论
### 回答1: 学习Go语言的并发编程,需要掌握以下几个方面: 1. 学习Goroutine:Goroutine是Go语言并发编程的核心,它是一种轻量级的线程,可以在程序中创建多个Goroutine并发执行,以实现效的并发编程。需要学习如何创建和管理Goroutine。 2. 学习Channel:Channel是Goroutine之间进行通信的重要手段,可以用于数据传输和同步等操作。需要学习如何创建和使用Channel。 3. 学习Select语句:Select语句是Goroutine之间进行多路复用的重要语法,可以同时监听多个Channel,从而实现效的并发处理。需要学习如何使用Select语句。 4. 学习Mutex和WaitGroup:Mutex和WaitGroup是Go语言中实现同步和互斥的重要机制,可以用于保护共享资源和协调Goroutine的执行。需要学习如何使用Mutex和WaitGroup。 5. 学习并发编程的设计模式:并发编程的设计模式是一些常用的并发编程思想和模式,可以用于解决并发编程中的常见问题。需要学习如何应用并发编程的设计模式。 为了学习并发编程,可以参考一些优秀的Go语言并发编程书籍,比如《Go并发编程实战》、《Go语言并发与微服务实战》等。同时也可以参考一些优秀的开源项目,如etcd、Docker等,深入了解Go语言并发编程的应用场景和实现方式。 ### 回答2: 学习Go语言的并发编程可以按照以下步骤进行。 1. 学习并理解Go语言的并发模型:Go语言的并发编程基于goroutine和channel。首先,需要了解goroutine的概念,它是Go语言中的一种轻量级线程。然后学习如何使用channel进行通信和同步。 2. 掌握goroutine的创建和管理:学习如何创建和管理goroutine,可以通过使用go关键字来创建一个新的goroutine,以便并发地执行任务。 3. 理解channel的使用:掌握channel的使用,了解如何创建、发送和接收数据。学习不同类型的channel以及它们在并发编程中的应用场景。 4. 学习互斥锁和读写锁:Go语言提供了互斥锁和读写锁来实现资源的安全访问。深入理解锁的概念和使用方法,学习如何避免并发访问导致的数据竞争。 5. 掌握并发编程的常见模式:学习并发编程中的常见模式,例如生产者-消费者模式、多路复用模式、线程等。熟悉这些模式可以帮助我们更好地设计并发程序。 6. 阅读优秀的并发编程代码和文档:阅读优秀的并发编程代码可以提供实际的应用示例和启发。同时,阅读官方文档和相关书籍也是学习的重要途径。 7. 实践和调试:编写自己的并发代码,利用调试工具对程序进行调试,观察并发执行的过程和结果。通过实践来提并发编程的理解和应用能力。 总之,学习Go语言的并发编程需要理解并发模型、掌握goroutine和channel的使用、了解锁的概念和使用方法,并通过实践来提自己的技能。 ### 回答3: 学习Go语言的并发编程可以通过以下几个步骤来进行。 首先,了解并发编程的概念和原则。并发编程是指同时进行多个任务,使用多个线程或协程来提程序的效率和性能。了解并发编程的基本概念,如协程、锁、原子操作等,对学习Go语言的并发编程非常重要。 其次,学习Go语言的并发特性。Go语言内置了丰富的并发编程工具和特性,如goroutine、channel、select等。通过学习这些特性,可以更好地理解Go语言中的并发编程模型,并能够正确地使用它们。 然后,阅读相关的书籍和文章。有很多经典的书籍和文章涉及到Go语言的并发编程,这些资源可以帮助你更深入地理解并发编程的原理和实践。值得推荐的书籍有《Go语言实战》和《Go并发编程实战》。 接着,进行实践和项目练习。通过编写一些小型的并发程序和项目,可以将理论知识应用到实践中,加深对并发编程的理解和掌握。可以尝试使用goroutine和channel来实现一些并发任务,如爬虫、并发请求等。 最后,参与社区和交流。加入Go语言的社区,如论坛、聊天群等,与其他开发者交流和分享经验。通过与他人的交流,可以学习到更多实践中的经验和技巧,不断提升自己的并发编程能力。 总之,学习Go语言的并发编程需要系统地学习基本概念和原则,掌握语言特性,进行实践和项目练习,并积极参与社区和交流。只有不断实践和学习,才能在并发编程领域不断进步。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值