如何实现流水线到goroutines?

时间:2019-02-09 16:32:34

标签: go

在了解如何使用管道获取将数据从一个goroutine传输到另一个goroutine方面,我需要一些帮助。

我读了golang blogpost on pipeline,我理解了它,但不能完全付诸实践,因此想到了寻求社区的帮助。

现在,我想出了这个丑陋的代码(Playground):

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    wg := sync.WaitGroup{}
    ch := make(chan int)
    for a := 0; a < 3; a++ {
        wg.Add(1)
        go func1(int(3-a), ch, &wg)
    }
    go func() {
        wg.Wait()
        close(ch)
    }()
    wg2 := sync.WaitGroup{}
    ch2 := make(chan string)
    for val := range ch {
        fmt.Println(val)
        wg2.Add(1)
        go func2(val, ch2, &wg2)
    }
    go func() {
        wg2.Wait()
        close(ch2)
    }()
    for val := range ch2 {
        fmt.Println(val)
    }
}

func func1(seconds int, ch chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    time.Sleep(time.Duration(seconds) * time.Second)
    ch <- seconds
}

func func2(seconds int, ch chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    ch <- "hello"
}

问题

我想使用管道或其他适当方法来完成此操作。

此外,博客文章中显示的管道不适用于goroutines,因此我自己不能这样做。

在现实生活中,那些func1func2是从网络获取资源的函数,因此它们是在自己的goroutine中启动的。

谢谢。
Temporarya
(golang noobie)

PS 。现实生活中的示例以及使用goroutine进行管道的使用也将有很大帮助。

1 个答案:

答案 0 :(得分:1)

该管道发布的关键模式是,您可以将通道的内容视为数据流,并编写一组协作的goroutine来构建数据处理流图。这可能是并发进入面向数据的应用程序的一种方式。

在设计方面,您可能还会发现,建立与goroutine结构无关的模块并将它们包装在通道中会更有帮助。这样可以更轻松地测试下层代码,并且如果您改变了是否在goroutine中运行代码的想法,那么添加或删除包装器将变得更加容易。

因此,在您的示例中,我将首先将最低级别的任务重构为它们自己的(同步)函数:

func fetch(ms int) int {
    time.Sleep(time.Duration(ms) * time.Millisecond)
    return ms
}

func report(ms int) string {
    return fmt.Sprintf("Hello after %d ms", ms)
}

由于示例的后半部分相当同步,因此很容易适应管道模式。我们编写了一个使用所有输入流并生成完整输出流的函数,完成后将其关闭。

func reportAll(mss <-chan int, out chan<- string) {
    for ms := range mss {
        out <- report(ms)
    }
    close(out)
}

调用异步代码的函数有些麻烦。在函数的主循环中,每次读取值时,都需要启动一个goroutine对其进行处理。然后,从输入通道中读取所有内容后,需要等待所有这些goroutine完成后再关闭输出通道。您可以在此处使用一个小的匿名函数来提供帮助。

func fetchAll(mss <-chan int, out chan<- int) {
    var wg sync.WaitGroup
    for ms := range mss {
        wg.Add(1)
        go func(ms int) {
            out <- fetch(ms)
            wg.Done()
        }(ms)
    }
    wg.Wait()
    close(out)
}

在这里(因为通道写入被阻塞)也很有用,因为它可以编写另一个函数来播种输入值。

func produceInputs(mss chan<- int) {
    for ms := 1000; ms > 0; ms -= 300 {
        mss <- ms
    }
    close(mss)
}

现在,您的主要功能需要在它们之间创建通道并运行最终使用者。

// main is the entry point to the program.
//
//                   mss        fetched       results
//     produceInputs --> fetchAll --> reportAll --> main
func main() {
    mss := make(chan int)
    fetched := make(chan int)
    results := make(chan string)

    go produceInputs(mss)
    go fetchAll(mss, fetched)
    go reportAll(fetched, results)

    for val := range results {
        fmt.Println(val)
    }
}

https://play.golang.org/p/V9Z7ECUVIJL是一个完整的示例。

我避免在这里手动传递sync.WaitGroup(通常这样做):除非您明确地将某个东西称为goroutine的顶层,否则您将没有WaitGroup。直到调用方为止的WaitGroup管理都使代码更具模块化;有关示例,请参见上面的fetchAll函数)。我怎么知道我所有的goroutine已经完成?我们可以通过以下方式进行跟踪:

  • 如果我已经到达main的结尾,则results频道将关闭。
  • results通道是reportAll的输出通道;如果关闭,则该函数执行完毕。然后如果发生这种情况,fetched频道将关闭。
  • fetched通道是fetchAll的输出通道; ...

另一种看待此问题的方式是,一旦管道的源(produceInputs)关闭其输出通道并完成操作,“我完成了”信号就会沿着管道向下流动并导致下游步骤关闭他们的输出通道也完成了。

该博客文章提到了一个单独的显式关闭渠道。我根本没有在这里讨论过。不过,自编写以来,标准库就获得了context软件包,该软件包现在是管理它们的标准习惯用法。您需要在主循环的主体中使用select语句,这会使处理变得更加复杂。可能看起来像:

func reportAllCtx(ctx context.Context, mss <-chan int, out chan<- string) {
    for {
        select {
            case <-ctx.Done():
                break
            case ms, ok := <-mss:
                if ok {
                    out <- report(ms)
                } else {
                    break
                }
            }
        }
    }
    close(out)
}