Go 并发编程

Go 并发编程:通道常见应用范式

2020-03-04  本文已影响0人  GoFuncChan

通道经典应用

一、闭包实现通道访问限制

在Go的并发编程中,创建通道和开辟协程是非常方便且容易的,正因如此,有可能会导致开发者滥用。如果在团队开发中没有良好的协商和规范,更可能会导致并发数据不安全。
例如:

func Demo() {
    ch := make(chan int, 0)
    go dosomething(ch, 10)
    go dosomething(ch, 20)
    dosomething(ch, 30)
}

func dosomething(ch chan int, num int) {
    for i := 1; i < num; i++ {
        ch <- i
    }
    close(ch1)
}

以上是个非常明显的不规范使用通道的例子,你能得到的只有死锁!

fatal error: all goroutines are asleep - deadlock!

那么能不能限制通道的使用呢,即特定通道只让特定协程使用?
有几种解决方案:

以下我们使用第三种方案:
闭包实现访问受限的通道,只允许特定协程使用

func Demo() {
    // 生产者:producter 内部开辟一条协程往里面发送数据,并返回一个只读通道
    producter := func() <-chan int {
        results := make(chan int, 5) // 该通道只作用于特定闭包内的作用域
        go func() {
            defer close(results)
            for i := 0; i <= 5; i++ {
                results <- i
            }
        }()
        return results
    }

    // 消费者:运行这个闭包时需要传入一个只读通道
    consumer := func(results <-chan int) { 
        for result := range results {
            fmt.Printf("Received: %d\n", result)
        }
        fmt.Println("Done receiving!")
    }
        
    consumer(producter())
}

二、for-select范式:关于多通道操作的整合

我们知道select语句是go专门为多通道操作提供的原语,单个select语句可以一次性的从多个通道选取一个来读写,只要哪个通道先不处于阻塞状态便选取哪个通道读写。而结合for循环语句构成的for-select结构可以循环不断的从多通道读写数据,直到特定条件退出。

for-select循环模式如下所示:

for { // 无限循环或遍历
    select {
    // 对通道进行操作
    }
}

常见的几种for-select循环的用法:
a. 在通道上发送迭代变量

for _, s := range []string{"a", "b", "c"} {
    select {
    case <-done:
        return
    case stringStream <- s:   // slice数据循环迭代写入channel
    }
}

b. 无限循环等待停止

// 第一种方式
for {
    select {
    case <-done: 
        return   // 停止返回
    default:
    }
    // 执行非抢占任务
}

// 第二种方式
for {
    select {
    case <-done:
        return 
    default:    
      // 将要执行的任务放入default分支中
      // 执行非抢占任务
    }
}

通过善用通道,我们可以在许多并发过程中尽量避免使用同步锁,select原语可以集中处理多个通道,大大提高了开发和运行效率。

三、or-channel :递归多个通道的或读取,只要有一个通道返回即完成


/*
递归多个通道的或读取,只要有一个通道返回即完成
*/
func Demo() {
    var or func(channels ...<-chan interface{}) <-chan interface{}
    // 建立了名为or的递归函数,接收数量可变的通道并返回单个通道。
    or = func(channels ...<-chan interface{}) <-chan interface{} 
    // 两个递归终止条件     
    switch len(channels) {
        case 0:  // 如果传入的切片是空的,我们简单的返回一个nil通道
            return nil
        case 1:  // 如果切片只含有一个元素,我们就返回给元素
            return channels[0]
        }
      
        orChannel := make(chan interface{})
        // 建立一个goroutine,以便可以不受阻塞地等待我们通道上的消息
        go func() { 
            defer close(orDone)

            switch len(channels) {
            case 2: // 由于我们这里是递归的,每次递归调用将至少有两个通道。作为保持goroutine数量受到限制的优化方法,们在这里为仅使用两个通道的时设置了一个特殊情况。
                select {
                case <-channels[0]:
                case <-channels[1]:
                }
            default: // 递归地在第三个索引之后,从切片中的所有通道中创建一个or通道,然后从中选择。递归操作会逐层累计直到取到第一个通道元素。我们在其中传递了orChannel通道,这样当该树状结构顶层的goroutines退出时,结构底层的goroutines也会退出。
                select {
                case <-channels[0]:
                case <-channels[1]:
                case <-channels[2]:
                case <-or(append(channels[3:], orChannel)...): 
                }
            }
        }()
        return orDone
    }



    // 下面这个例子将经过一段时间后关闭通道,然后使用or函数将这些通道合并到一个关闭的通道中:
    sig := func(after time.Duration) <-chan interface{} { // 创建了一个通道,当后续时间中指定的时间结束时将关闭该通道
        c := make(chan interface{})
        go func() {
            defer close(c)
            time.Sleep(after)
        }()
        return c
    }

    start := time.Now() // 设置追踪自or函数的通道开始阻塞的起始时间
    <-or(sig(2*time.Hour), sig(5*time.Minute), sig(1*time.Second), sig(1*time.Hour), sig(1*time.Minute))
    fmt.Printf("done after %v", time.Since(start)) // 打印阻塞发生的时间
}

这是一种奇妙的做法,你可以将任意数量的通道组合到单个通道中,只要任何作为组件的通道关闭或被写入,整个通道就会关闭。

四、chRange 封装安全的通道遍历读取

有时你会与来自系统不同部分的通道交互。与管道不同的是,当你使用的代码通过done通道取消操作时,你无法对通道的行为方式做出判断。也就是说,你不知道正在执行读取操作的goroutine现在是什么状态。出于这个原因,正如我们在“防止Goroutine泄漏”中所阐述的那样,需要用select语句来封装我们的读取操作和done通道。可以简单的写成这样:

for val := range myChan {
    // 对 val 进行处理
}

展开后可以写成这样:

loop:
    for {
        select {
        case <-done:
            break loop
        case maybeVal, ok := <-myChan:
            if ok == false {
                return // or maybe break from for
            }
            // Do something with val
        }
    }

这样做可以快速退出嵌套循环。继续使用goroutines编写更清晰的并发代码,而不是过早优化的主题,我们可以用一个goroutine来解决这个问题。 我们封装了细节,以便其他人调用更方便:

/*
封装一个通用的安全的通道读取器,以便于可安全地for range遍历任意通道
*/
var chRange = func(done, ch <-chan interface{}) <-chan interface{} {
    valStream := make(chan interface{})
    // 使用协程闭包封装安全的读取通道 
    go func() {
        defer close(valStream)
        for {
            select {
            case <-done:
                return
            case v, ok := <-ch:
                if ok == false {
                    return
                }
                select {
                case valStream <- v:
                case <-done:
                }
            }
        }
    }()

    return valStream
}

调用示例:这样对任意通道我们都可以简单安全的读取

func Demo() {
    done := make(chan interface{})
    defer close(done)

    ch := make(chan interface{})
    go func() {
        defer close(ch)
        for i := 0; i < 10; i++ {
            ch <- i
        }
    }()

    for val := range chRange(done, ch) {
        fmt.Printf("read %v \n", val)
    }
}

五、tee-channel 分割通道数据流

tee-channel类似Linux的tee命令,分割来自通道的值,以便将它们发送到两个独立区域。想象一下:你可能想要在一个通道上接收一系列操作指令,将它们发送给执行者,同时记录操作日志。

var tee = func(done <-chan interface{}, in <-chan interface{}) (_, _ <-chan interface{}) {

    out1 := make(chan interface{})
    out2 := make(chan interface{})

    go func() {
        defer close(out1)
        defer close(out2)
        for val := range chRange(done, in) {
            select {
            case <-done:
            default:
                out1 <- val
                out2 <- val
            }

        }
    }()
    return out1, out2
}

注意写入out1和out2是紧密耦合的。 直到out1和out2都被写入,迭代才能继续。 通常这不是问题,因为无论如何,处理来自每个通道的读取流程的吞吐量应该是tee之外的关注点,但值得注意。 这是一个快速调用示例:

func Demo()  {
    done := make(chan interface{})
    defer close(done)

    ch := make(chan interface{})
    go func() {
        for i := 0; i < 10; i++ {
            ch <- i
        }
        defer close(ch)
    }()

    out1, out2 := tee(done, ch)

    for val1 := range out1 {
        fmt.Printf("out1: %v, out2: %v\n", val1, <-out2)
    }

利用这种模式,很容易使用通道作为系统数据的连接点。

六、bridge-channel

在某些情况下,你可能会发现自己想要使用一系列通道,即你可能需要从一个通道中获取多个通道的值:

<-chan <-chan interface{}

这与将某个通道的数据切片合并到一个通道中稍有不同,这种调用方式意味着一系列通道有序的写入操作。从通道读取一系列通道的值 ,类似多通道过独木桥。

// 通道桥接
var bridge = func(done <-chan interface{}, chanStream <-chan <-chan interface{}) <-chan interface{} {

    valStream := make(chan interface{}) // 1
    go func() {
        defer close(valStream)
        for { // 2
            var stream <-chan interface{}
            select {
            case maybeStream, ok := <-chanStream:
                if ok == false {
                    return
                }
                stream = maybeStream
            case <-done:
                return
            }
            for val := range chDone(done, stream) { // 3
                select {
                case valStream <- val:
                case <-done:
                }
            }
        }
    }()
    return valStream
}

使用示例:

func Demo() {
    genVals := func() <-chan <-chan interface{} {

        chanStream := make(chan (<-chan interface{}))

        go func() {
            defer close(chanStream)
            for i := 0; i < 10; i++ {
                stream := make(chan interface{}, 1)
                stream <- i
                close(stream)
                chanStream <- stream
            }
        }()
        return chanStream
    }

    for v := range bridge(nil, genVals()) {
        fmt.Printf("%v ", v)
    }
}
上一篇下一篇

猜你喜欢

热点阅读