Go语言常用的并发模式(上)

版权声明:所有的博客都是作为个人笔记的。。。。。。 https://blog.csdn.net/qq_35976351/article/details/82151607

Confinement

该模式用于处理数据限制问题,类似于生产者和消费者模式。使用channel的方式通过共享信息的方式进行。有一个协程专门负责生产,另外一个协程负责接收数据。代码中使用随机的时间模拟实际情况中耗时部分。

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    chanOwner := func(n int) <-chan int {
        results := make(chan int, 5)
        go func() {
            defer close(results)
            for i := 0; i < n; i++ {
                results <- i
                fmt.Printf("produce %d\n", i)
                t := rand.Intn(2000)
                time.Sleep(time.Duration(t) * time.Millisecond) // 随机睡眠0-2秒
            }
        }()
        return results
    }

    consumer := func(results <-chan int) {
        for result := range results {
            t := rand.Intn(2000)
            time.Sleep(time.Duration(t) * time.Millisecond) // 随机睡眠0-2秒
            fmt.Printf("Received %d\n", result)
        }
        fmt.Println("Done receiving")
    }
    results := chanOwner(5)
    consumer(results)
}
/*
代码输出:
produce 0
produce 1
Received 0
produce 2
Received 1
produce 3
produce 4
Received 2
Received 3
Received 4
Done receiving
*/

这种模式可以根据实际情况定义生产和消费的方式,不用担心出现数据竞争的问题。

for-select循环

最常规的模式:

for { // 死循环
    select {
    // 在这里采取有关操作
    }
}

通过迭代的方式把数据写入channel

for _, s := range []string{"a", "b", "c"} {
    select {
    case <-done:  // 这里是完成条件的标记
        return
    case stringStream <- s:  // 在这里写入数据
    }
}

死循环等待结束标记。
这种方式会尽可能早的结束工作,只要done信号到达,立刻终止:

for {
    select {
    case <-done:
        return
    default:
    }
    // do something here
}

另一个等价方式

for {
    select {
    case <-done:
        return
    default:
        // do something here
    }
}

防止goroutine泄露

尽管goroutine是一种轻量级的进程,而且一般不必担心使用太多的协程导致内存的问题,Go语言的有自动回收机制;但是,在某些情况下确实需要考虑出现某些协程一直无法回收的问题,这可还会引发一些其它的不良后果,给出下面的例子:

doWork := func(strings <-chan string) <-chan interface{} {
    completed := make(chan interface{})
    go func() {
        defer fmt.Println("doWork exited")
        defer close(completed)
        for s := range strings {
            // Do something
            fmt.Println(s)
        }
    }()
    return completed
}
doWork(nil)
fmt.Println("Done")

在上述的代码中,doWork会永远阻塞,因为空的string channel不会有任何内容输出。本例子中的开销可能很小,但是在实际的工程中,这可能会引发大的问题。解决方法是通过父协程结束子协程。通过父协程给子协程发射终止的信号,使得子协程自动终止。

给出一般的操作方式:

   doWork := func(
        done <-chan interface{},  // 终止的信号
        strings <-chan string,    // 等待读取的字符串
    ) <-chan interface{} {        // 子协程返回自己终止的信号
        terminated := make(chan interface{})
        go func() {
            defer fmt.Println("doWork exited")
            defer close(terminated)
            for {
                select {
                case s := <-strings:
                    // do something
                    fmt.Println(s)
                case <-done:  // 如果关闭,则直接执行return
                    return
                }
            }
        }()
        return terminated
    }

    done := make(chan interface{})
    terminated := doWork(done, nil)  // 接收子协程的终止信号
    go func() {
        // cancel the operation after 1 second
        time.Sleep(time.Second)
        fmt.Println("Canceling doWork goroutine...")
        close(done)  // 关闭后相当于不存在阻塞的情况了。。。
    }()
    <-terminated  // 在这里等待子协程的终止
    fmt.Println("Done.")

上述代码是读数据的例子,下面给出写数据时发生协程泄露的例子:

package main

import (
    "fmt"
    "math/rand"
)

func main() {
    newRandStream := func() <-chan int {
        randStream := make(chan int)
        go func() {
            defer fmt.Println("newRandStream closure exited.")
            defer close(randStream)
            for {
                randStream <- rand.Int()
            }
        }()
        return randStream
    }

    randStream := newRandStream()
    n := 3
    fmt.Printf("%d random ints:\n", n)
    for i := 0; i < n; i++ {
        fmt.Printf("%d: %d\n", i, <-randStream)  // 注意这种使用方式,也是合法的
    }
}
/*
输出结果:
3 random ints:
0: 5577006791947779410
1: 8674665223082153551
2: 6129484611666145821
*/

上述代码中,randStream始终没有结束,出现了协程泄露。。

改进方案:和写数据的方式类似,通过父协程给子协程发射结束信号即可。代码方案:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    newRandStream := func(done <-chan interface{}) <-chan int {
        randStream := make(chan int)
        go func() {
            defer fmt.Println("newRandStream closure exited...")
            defer close(randStream)
            for {
                select {
                case randStream <- rand.Int():
                case <-done:
                    return
                }
            }
        }()
        return randStream
    }

    done := make(chan interface{})
    randStream := newRandStream(done)
    n := 3
    fmt.Printf("%d random ints\n", n)
    for i := 0; i < 3; i++ {
        fmt.Printf("%d:%d\n", n, <-randStream)
    }
    close(done)
    // 等待同步看效果,不用等待也可以正常结束的,这里仅仅是为了显式说明一下
    time.Sleep(time.Second)
}
/*
输出结果:
3 random ints
0:5577006791947779410
1:8674665223082153551
2:6129484611666145821
newRandStream closure exited...
*/

or-channel方式

这种模式的方式是:把多个channeldone连接到一个done上,如果这些channel中任何至少一个关闭,则关闭这个done。 代码中,如果出现一个任意一个协程结束,那么就出现终止信号。终止信号出现后,如果有协程没有结束,他们会继续执行,代码只是检测是否有协程终止,而不主动结束协程。
给出实例代码:

package main

import (
    "fmt"
    "time"
)

func main() {
    // 从这里传入各个channel的done
    var or func(channels ...<-chan interface{}) <-chan interface{}
    or = func(channels ...<-chan interface{}) <-chan interface{} {
        switch (len(channels)) {
        case 0: // 递归结束的条件
            return nil
        case 1: // 只有一个直接返回
            return channels[0]
        }
        orDone := make(chan interface{}) // 这是自己的标记
        // 可以理解成一棵协程树,父节点需要孩子节点结束才能销毁。。。
        // 在这里进行协程孩子节点的拓展,WTF好难理解。。。。。
        // 在匿名函数中,如果一个channel的任何一个子channel结束,那么匿名函数的阻塞就会立刻结束,
        // 之后会执行内部的defer操作,然后return一个关闭了的channel,相当于解除阻塞
        go func() {
            defer close(orDone) // 结束的时候释放本身的done信号
            switch len(channels) {
            case 2:
                select {
                case <-channels[0]:
                case <-channels[1]:
                }
            default:
                select {
                // 如果case失败,则进行default,需要再判断一下,防止此次突然有结束的信号了
                case <-channels[0]:
                case <-channels[1]:
                case <-channels[2]:
                // 在这里追加父节点的协程终止信号,因为这是一棵或的树,只要有一个节点成功就可以释放掉
                // 因此把父节点一起传入,只要有一个释放掉,父节点的channel就立刻进行释放......好机智的操作
                // 这里追加自己的orDone,是为了`
                case <-or(append(channels[3:], orDone)...): // 注意使用...符号
                }
            }
        }()
        return orDone
    }

    sig := func(after time.Duration) <-chan interface{} {
        c := make(chan interface{})  
        go func() {
            defer close(c) // 所在的goroutine结束后close,使用时间模拟工作时间
            time.Sleep(after)
        }()
        return c
    }

    start := time.Now()
    <-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\n", time.Since(start))
}
/*
输出结果:
done after 1.000182106s
*/

从上述看出,仅仅执行到结果最短的那个,相当于一个“或”操作。代码采用了尾递归的方式,因为select方式无法预判channel的数量,而循环的方式需要处理大量的阻塞问题,不如尾递归的方式简洁。

上述代码最后的递归中,有一个地方不太理解:代码递归的过程中为什么要加入orDone?希望有明白的同学可以解释一下!

猜你喜欢

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