conc:更好的go结构化并发

conc:更好的go结构化并发

本文译自:conc: better structured concurrency for go

原文截图

在这里插入图片描述

译文

conc是你在 go 中进行结构化并发的工具带,使常见任务更容易、更安全。

go get github.com/sourcegraph/conc

一目了然

所有池都是用 pool.New() or 创建的pool.NewWithResults[T](),然后用方法配置:

目标

该软件包的主要目标是:

  1. 让泄漏 goroutines 变得更难
  2. 从容应对恐慌
  3. 使并发代码更易于阅读

目标 #1:让泄漏 goroutines 变得更难

使用 goroutine 时的一个常见痛点是清理它们。很容易发出一条go语句而未能正确等待它完成。

conc采取固执己见的立场,即所有并发都应在范围内。也就是说,goroutines 应该有一个所有者,并且该所有者应该始终确保其拥有的 goroutines 正确退出。

conc中,goroutine 的所有者始终是conc.WaitGroup. Goroutines 在 with 中产生WaitGroup(*WaitGroup).Go()并且 (*WaitGroup).Wait()应该总是在WaitGroup超出范围之前被调用。

在某些情况下,您可能希望生成的 goroutine 比调用者的范围更持久。在这种情况下,您可以将 a 传递给WaitGroup生成函数。

func main() {
    var wg conc.WaitGroup
    defer wg.Wait()
    startTheThing(&wg)
}
func startTheThing(wg *conc.WaitGroup) {
    wg.Go(func() { ... })
}

有关为什么作用域并发很好的更多讨论,请查看此博客文章

目标 2:从容应对恐慌

在长时间运行的应用程序中,goroutines 的一个常见问题是处理恐慌。在没有 panic 处理程序的情况下生成的 goroutine 将在 panic 时使整个进程崩溃。这通常是不可取的。

但是,如果您确实向 goroutine 添加了一个 panic 处理程序,那么一旦您捕获到 panic,您将如何处理它?一些选项:

  1. 忽略它
  2. 登录
  3. 将其转化为错误并将其返回给 goroutine spawner
  4. 将 panic 传播到 goroutine spawner

忽略恐慌是一个坏主意,因为恐慌通常意味着实际上存在问题并且应该有人修复它。

仅仅记录 panic 也不是很好,因为这样就没有迹象表明 spawner 发生了不好的事情,并且即使您的程序处于非常糟糕的状态,它也可能会继续正常运行。

(3) 和 (4) 都是合理的选择,但都需要 goroutine 有一个所有者,该所有者可以实际接收到出错的消息。对于使用 spawned 生成的 goroutine 通常不是这样go,但在conc 包中,所有 goroutine 都有一个所有者,必须收集生成的 goroutine。在 conc 包中,Wait()如果任何派生的 goroutines 恐慌,则任何调用都会恐慌。此外,它使用来自子 goroutine 的堆栈跟踪来装饰恐慌值,这样您就不会丢失有关导致恐慌的原因的信息。

每次生成一些东西时都正确地完成这一切go并不是微不足道的,它需要大量的样板文件,这使得代码的重要部分更难阅读,所以conc这对你来说也是如此。

stdlibconc
type caughtPanicError struct {
    val   any
    stack []byte
}
func (e *caughtPanicError) Error() string {
    return fmt.Sprintf(
        "panic: %q\n%s",
        e.val,
        string(e.stack)
    )
}
func main() {
    done := make(chan error)
    go func() {
        defer func() {
            if v := recover(); v != nil {
                done <- caughtPanicError{
                    val: v,
                    stack: debug.Stack()
                }
            } else {
                done <- nil
            }
        }()
        doSomethingThatMightPanic()
    }()
    err := <-done
    if err != nil {
        panic(err)
    }
}
func main() {
    var wg conc.WaitGroup
    wg.Go(doSomethingThatMightPanic)
    // panics with a nice stacktrace
    wg.Wait()
}

目标 3:使并发代码更易于阅读

正确地进行并发是很困难的。以一种不混淆代码实际执行的方式进行操作更加困难。该conc包试图通过抽象尽可能多的样板复杂性来简化常见操作。

想要使用一组有界的 goroutine 运行一组并发任务?使用 pool.New()。想要并发处理有序的结果流,但仍保持顺序?尝试stream.New()。切片上的并发映射怎么样?看一看iter.Map()

浏览下面的一些示例,与手动执行这些操作进行一些比较。

例子

为了简单起见,这些示例中的每一个都放弃了传播恐慌。要查看会增加什么样的复杂性,请查看上面的“目标 #2”标题。

生成一组 goroutines 并等待它们完成:

stdlibconc
func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            // crashes on panic!
            doSomething()
        }()
    }
    wg.Wait()
}
func main() {
    var wg conc.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Go(doSomething)
    }
    wg.Wait()
}

在 goroutines 的静态池中处理流的每个元素:

stdlibconc
func process(stream chan int) {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for elem := range stream {
                handle(elem)
            }
        }()
    }
    wg.Wait()
}
func process(stream chan int) {
    p := pool.New().WithMaxGoroutines(10)
    for elem := range stream {
        elem := elem
        p.Go(func() {
            handle(elem)
        })
    }
    p.Wait()
}

在 goroutines 的静态池中处理切片的每个元素:

stdlibconc
func process(values []int) {
    feeder := make(chan int, 8)
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for elem := range feeder {
                handle(elem)
            }
        }()
    }
    for _, value := range values {
        feeder <- value
    }
    close(feeder)
    wg.Wait()
}
func process(values []int) {
    iter.ForEach(values, handle)
}

同时映射一个切片:

stdlibconc
func concMap(
    input []int,
    f func(int) int,
) []int {
    res := make([]int, len(input))
    var idx atomic.Int64
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for {
                i := int(idx.Add(1) - 1)
                if i >= len(input) {
                    return
                }
                res[i] = f(input[i])
            }
        }()
    }
    wg.Wait()
    return res
}
func concMap(
    input []int,
    f func(int) int,
) []int {
    return iter.Map(input, f)
}

同时处理有序流:

stdlibconc
func mapStream
    in chan int,
    out chan int,
    f func(int) int,
) {
    tasks := make(chan func())
    taskResults := make(chan chan int)
    // Worker goroutines
    var workerWg sync.WaitGroup
    for i := 0; i < 10; i++ {
        workerWg.Add(1)
        go func() {
            defer workerWg.Done()
            for task := range tasks {
                task()
            }
        }()
    }
    // Ordered reader goroutines
    var readerWg sync.WaitGroup
    readerWg.Add(1)
    go func() {
        defer readerWg.Done()
        for result := range taskResults {
            out <- result
        }
    }
    // Feed the workers with tasks
    for elem := range in {
        resultCh := make(chan int, 1)
        taskResults <- resultCh
        tasks <- func() {
            resultCh <- f(elem)
        }
    }
    // We've exhausted input.
    // Wait for everything to finish
    close(tasks)
    workerWg.Wait()
    close(taskResults)
    readerWg.Wait()
}
func mapStream(
    in chan int,
    out chan int,
    f func(int) int,
) {
    s := stream.New().WithMaxGoroutines(10)
    for elem := range in {
        elem := elem
        s.Go(func() stream.Callback {
            res := f(elem)
            return func() { out <- res }
        })
    }
    s.Wait()
}

地位

这个包目前是 pre-1.0。在我们稳定 API 并调整默认值时,在 1.0 版本发布之前可能会有一些小的破坏性变化。如果您有任何问题、疑虑或请求希望在 1.0 版本之前得到解决,请打开一个问题。目前,1.0 的目标是 2023 年 3 月。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值