Golang的并发模式

版权声明:博客仅作为博主的个人笔记,未经博主允许不得转载。 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 交换数据,在通道操作完成时一定保证对方接收到了数据

猜你喜欢

转载自blog.csdn.net/qq_35976351/article/details/82017421