版权声明:博客仅作为博主的个人笔记,未经博主允许不得转载。 https://blog.csdn.net/qq_35976351/article/details/82017421
简介
主要学习3个在工程中常用的并发包,同时学习这些包常用的情景模式。
runner
runner
可以给一组任务进行顺序分配,然后进行总体的时间限制。该方式确定了任务的顺序后,可以让任务顺序执行,直到时间限制到了或者任务完成或者人为中断。本例子中同时允许使用Ctrl+C
进行人工中断。
runner
包源代码分析:
package main
import (
"os"
"time"
"errors"
"os/signal"
)
type Runner struct {
interrupt chan os.Signal // 用于接收系统信号
complete chan error // 是否完成任务的标记
timeout <-chan time.Time // 是否超时的标记
tasks []func(int) // 任务组,输入int参数,返回空
}
// 自定义两个错误类型
var ErrTimeout = errors.New("received timeout")
var ErrInterrupt = errors.New("received interrupt")
// 返回一个新的准备使用的runner,注意返回指针
func New(d time.Duration) *Runner {
return &Runner{
interrupt: make(chan os.Signal, 1),
complete: make(chan error),
timeout: time.After(d),
}
}
// Add将一个任务附加到Runner上,这个任务是一个接受int类型的ID作为参数的函数
func (r *Runner) Add(tasks ...func(int)) {
r.tasks = append(r.tasks, tasks...)
}
// 验证是否接收到了中断信号,经典的select使用方法
func (r *Runner) gotInterrupt() bool {
select {
// 中断时间被触发时发出的信号
case <-r.interrupt:
signal.Stop(r.interrupt) // 停止接受后续的任何信号
return true
default: // 正常执行
return false
}
}
func (r *Runner) run() error {
for id, task := range r.tasks {
if r.gotInterrupt() { // 检测到操作系统的中断信号
return ErrInterrupt
}
task(id) // 执行已经注册的任务
}
return nil
}
// 执行所有的任务,并监视通道时间
func (r *Runner) Start() error {
// 我们希望接受所有中断信号
signal.Notify(r.interrupt, os.Interrupt)
// 用不同的goroutine执行不同的任务
go func() {
r.complete <- r.run()
}()
select {
case err := <-r.complete: // 任务处理完成时发出信号
return err
case <-r.timeout:
return ErrTimeout // 运行超时发出信号
}
}
// 定义超时时间
const timeout = 10 * time.Second
func createTask() func(int) {
return func(i int) {
fmt.Printf("Processo - Task #%d. ", i)
fmt.Println("start time: ", time.Now().String())
time.Sleep(time.Duration(i) * time.Second)
}
}
func main() {
fmt.Println("Staring work.")
r := New(timeout)
r.Add(createTask(), createTask(), createTask()) // 创建3个任务队列
if err := r.Start(); err != nil {
switch err {
case ErrTimeout:
fmt.Println("Terminating due to timeout")
os.Exit(1)
case ErrInterrupt:
fmt.Println("Terminating due to interrupt")
os.Exit(2)
}
}
fmt.Println("Process ended")
}
执行结果:
Staring work.
Processo - Task #0. start time: 2018-08-23 14:38:31.829257686 +0800 CST m=+0.000299050
Processo - Task #1. start time: 2018-08-23 14:38:31.829384003 +0800 CST m=+0.000425300
Processo - Task #2. start time: 2018-08-23 14:38:32.829587124 +0800 CST m=+1.000628536
Process ended
在上述的任务中,我们定义了超时10秒,因此会正常结束。。。
pool
pool
的作用是实现有缓冲通道的资源池,管理在任意数量的goroutine
共享以及独立使用的资源。该模式在需要共享一组静态资源的情况(比如共享数据库链接或者内存缓冲区)的情况下非常有用。如果goroutine
需要从池里获得资源中的一个,可以从池中申请,使用完后归还到池里;如果池里没有资源,那么就返回一个新建立的资源;向池中返回资源时,如果池中资源已经满了,则释放掉这个资源。
代码实例:
package main
import (
"sync"
"io"
"errors"
"fmt"
"sync/atomic"
"time"
"math/rand"
)
// Pool管理一组可以安全地在多个goroutine间共享资源,
// 被管理的资源必须实现io.Close接口
type Pool struct {
m sync.Mutex
resources chan io.Closer
factory func() (io.Closer, error)
closed bool
}
var ErrPoolClosed = errors.New("Pool has been closed")
// New创建一个用来管理资源的池,这个池需要一个可以分配新资源的函数,并规定池的大小
func New(fn func() (io.Closer, error), size uint) (*Pool, error) {
if size <= 0 {
return nil, errors.New("Size value too small")
}
return &Pool{
factory: fn,
resources: make(chan io.Closer, size),
}, nil
}
// Acquire从池中获取一个资源
func (p *Pool) Acquire() (io.Closer, error) {
select {
case r, ok := <-p.resources:
fmt.Println("Acquire:", "Shared Resource")
if !ok {
return nil, ErrPoolClosed
}
return r, nil
// 没有空闲资源可用,提供新的资源
default:
fmt.Println("Acquire:", "New Resource")
return p.factory()
}
}
func (p *Pool) Release(r io.Closer) {
p.m.Lock() // 保证本操作和Close操作的安全
defer p.m.Unlock()
// 如果池关闭,则销毁这个资源
if p.closed {
r.Close()
return
}
select {
case p.resources <- r: // 尝试将该资源放入队列
fmt.Println("Release:", "In Queue")
default: // 如果队列慢,则关闭这个资源
fmt.Println("Release:", "Closing")
r.Close()
}
}
// 让资源池停止工作,关闭现有的资源
func (p *Pool) Close() {
p.m.Lock()
defer p.m.Unlock()
if p.closed { // 如果pool已经被关闭,什么也不做
return
}
p.closed = true
close(p.resources) // 清空通道里的资源之前,需要关闭通道,否则引发死锁
for r := range p.resources { // 释放掉所有的资源
r.Close()
}
}
const (
maxGoroutines = 25 // goroutine的数量
pooledResources = 2 // 池中资源的数量
)
// 模拟要共享的资源,在这里假设是数据库链接
type dbConnection struct {
ID int32
}
// Colse实现了额io.CLose接口,以便dbConnection可以被池管理,
// Close用来完成任意资源的释放管理
func (dbConn *dbConnection) Close() error {
fmt.Println("Close: Connection", dbConn.ID)
return nil
}
var idCounter int32 // 给每个连接分配一个独一无二的id
// 工厂函数,创建新连接时调用
func createConnection() (io.Closer, error) {
id := atomic.AddInt32(&idCounter, 1) // 带有锁保护的递增
fmt.Println("Create: New Connection", id)
return &dbConnection{id}, nil
}
// 用来测试连接的资源池
func performQueries(query int, p *Pool) {
conn, err := p.Acquire()
if err != nil {
fmt.Println(err)
return
}
defer p.Release(conn) // 将该连接放回池里
// 用等待来模拟查询响应
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
fmt.Printf("QID[%d] CID[%d]\n", query, conn.(*dbConnection).ID)
}
func main() {
var wg sync.WaitGroup
wg.Add(maxGoroutines)
// 创建用来管理连接的池
p, err := New(createConnection, pooledResources)
if err != nil {
fmt.Println(err)
}
//用池里的连接完成查询
for query := 0; query < maxGoroutines; query++ {
// 每个goroutine需要自己复制一份查询的副本,
// 否则所有的查询会共享同一个查询变量
go func(q int) {
performQueries(q, p)
wg.Done()
}(query)
}
wg.Wait()
fmt.Println("Shutdown Program")
p.Close()
}
work
缓冲队列只有一个容量,所有的线程初始情况都是堵塞的情况,直到添加合适的任务。所有的goroutine
控制一组工作。所有外加的任务必须把任务放在自己的Task()
函数中,这个函数自己实现。
package main
import (
"sync"
"fmt"
"time"
)
type Worker interface {
Task()
}
type Pool struct {
work chan Worker
wg sync.WaitGroup
}
func New(maxGoroutines int) *Pool {
p := Pool{
work: make(chan Worker), // 使用无缓冲通道的工作池
}
p.wg.Add(maxGoroutines) // 工作线程数量
// 循环会一直阻塞,除非遇到合适的任务
for i := 0; i < maxGoroutines; i++ {
go func() {
for w := range p.work {
w.Task()
}
p.wg.Done()
}()
}
return &p
}
// 放置任务
func (p *Pool) Run(w Worker) {
p.work <- w
}
// 关闭整个工作池
func (p *Pool) Shutdown() {
close(p.work)
p.wg.Wait()
}
var names = []string{
"steve",
"bob",
"mary",
"therese",
"jason",
}
type namePrinter struct {
name string
}
func (m *namePrinter) Task() {
fmt.Println(m.name)
time.Sleep(time.Second)
}
func main() {
p := New(2) // 使用2个goroutine创建工作池
var wg sync.WaitGroup
wg.Add(100 * len(names))
for i := 0; i < 100; i++ {
for _, name := range names {
np := namePrinter{name: name}
go func() {
p.Run(&np)
wg.Done()
}()
}
}
wg.Wait()
p.Shutdown()
}
总结
- 可以使用通道来控制程序的生命周期。
- 带 default 分支的 select 语句可以用来尝试向通道发送或者接收数据,而不会阻塞。
- 有缓冲的通道可以用来管理一组可复用的资源。
- 语言运行时会处理好通道的协作和同步。
- 使用无缓冲的通道来创建完成工作的
goroutine
池。 - 任何时间都可以用无缓冲的通道来让两个
goroutine
交换数据,在通道操作完成时一定保证对方接收到了数据。