Go 如何并行化递归函数

Go 如何并行化递归函数,go,Go,我正在尝试并行化Go中的递归问题,我不确定最好的方法是什么 我有一个递归函数,其工作原理如下: func recFunc(输入字符串)(结果[]字符串){ 对于子输入:=范围getSubInputs(输入){ 子输出:=recFunc(子输入) result=result.append(结果、子输出…) } result=result.append(result,getOutput(input)…) } func main(){ 输出:=recFunc(“某些输入”) ... } 因此,该函数

我正在尝试并行化Go中的递归问题,我不确定最好的方法是什么

我有一个递归函数,其工作原理如下:

func recFunc(输入字符串)(结果[]字符串){
对于子输入:=范围getSubInputs(输入){
子输出:=recFunc(子输入)
result=result.append(结果、子输出…)
}
result=result.append(result,getOutput(input)…)
}
func main(){
输出:=recFunc(“某些输入”)
...
}
因此,该函数调用自身
N
次(其中N在某个级别为0),生成自己的输出并返回列表中的所有内容

现在我想让这个函数并行运行。但我不确定最干净的方法是什么。我的想法是:

  • 有一个“result”通道,所有函数调用都将结果发送到该通道
  • 在main函数中收集结果
  • 有一个等待组,它决定何时收集所有结果
问题是:我需要等待等待组并并行收集所有结果。我可以为此启动一个单独的go函数,但是我如何退出这个单独的go函数呢

func recFunc(输入字符串)(结果[]字符串,输出通道chan[]字符串,waitGroup&sync.waitGroup){
延迟waitGroup.Done()
waitGroup.Add(len(getSubInputs(input))
对于子输入:=范围getSubInputs(输入){
go recFunc(子输入)
}
输出通道
我可以为此启动一个单独的go函数,但是我如何退出这个单独的go函数呢

您可以在单独的go例行程序中通过输出通道进行
range
。在这种情况下,当通道关闭时,go例行程序将安全退出

go func() {
   for nextResult := range outputChannel {
     result = append(result, nextResult ...)
   }
}
因此,现在我们需要注意的是,在作为递归函数调用的一部分生成的所有go例程成功存在之后,通道将关闭

为此,您可以在所有go例程中使用共享的waitgroup,并在主函数中等待该waitgroup,就像您已经在做的那样。等待结束后,关闭outputChannel,以便其他go例程也安全退出

func recFunc(input string, outputChannel chan, wg &sync.WaitGroup) {
    defer wg.Done()
    for subInput := range getSubInputs(input) {
        wg.Add(1)
        go recFunc(subInput)
    }
    outputChannel <-getOutput(input)
}

func main() {
    outputChannel := make(chan []string)
    waitGroup := sync.WaitGroup{}

    waitGroup.Add(1)
    go recFunc("some_input", outputChannel, &waitGroup)

    result := []string{}
    go func() {
     for nextResult := range outputChannel {
      result = append(result, nextResult ...)
     }
    }
    waitGroup.Wait()
    close(outputChannel)        
}
func recFunc(输入字符串、输出通道、wg&sync.WaitGroup){
推迟工作组完成()
对于子输入:=范围getSubInputs(输入){
工作组.添加(1)
go recFunc(子输入)
}
输出通道tl;dr

  • 递归算法应该对昂贵的资源(网络连接、goroutine、堆栈空间等)有一定的限制
  • 应支持取消-以确保在不再需要结果时可以快速清理昂贵的操作
  • 分支遍历应该支持错误报告;这允许错误在堆栈中冒泡&返回部分结果,而不会使整个递归遍历失败

对于非同步结果(无论是否使用递归),建议使用通道。此外,对于具有许多goroutine的长时间运行的作业,请提供cancelation()方法以帮助清理

由于递归可能导致资源的指数级消耗,因此设置限制非常重要(请参阅)

以下是我经常用于异步任务的设计模式:

  • 始终支持选择取消
  • 任务所需的工人数量
  • 返回结果的
    chan
    chan错误(只返回一个错误或
    nil

连接各部分:
recJob
创建:

  • 输入和输出通道-由所有工人共享
  • “递归”
    WaitGroup
    检测所有工作线程空闲的时间
    • “输出”通道可安全关闭
  • 所有工作人员的错误通道
  • 通过将初始输入写入输入通道启动递归工作负载


func recJob(ctx context.context,workers int,输入字符串)(resultsC您首先应该了解的是,在Go中您无法控制并行性。您可以控制并发性,并且。我只是想说,我真的不明白您为什么会获得否决票。这是一个正确表述的问题。也许工作池模式是您可以实现的,而不是递归aApproach@Nathan我看到的问题是,使用相同的输出通道时,所有挂起的调用都将在同一通道上等待值,而您无法确定谁需要在哪个级别上读取通道;一个有效的解决方案是在您想要的每个级别上都有一个新通道。递归算法会导致指数增长-所以您需要一个有限的数字我可以为您提出一个使用结果通道(而不是切片)的解决方案—如果您可以接受的话?如果您想要并行,这是唯一的选择—因为使用并发时无法保证结果的顺序。
var (
    workers = 10
    ctx     = context.TODO() // use request context here - otherwise context.Background()
    input   = "abc"
)

resultC, errC := recJob(ctx, workers, input) // returns results & `error` channels

// asynchronous results - so read that channel first in the event of partial results ...
for r := range resultC {
    fmt.Println(r)
}

// ... then check for any errors
if err := <-errC; err != nil {
    log.Fatal(err)
}
func recFunc(ctx context.Context, input string, in chan string, out chan<- string, rwg *sync.WaitGroup) error {

    defer rwg.Done() // decrement recursion count when a depth of recursion has completed

    subInputs, err := getSubInputs(ctx, input)
    if err != nil {
        return err
    }

    for subInput := range subInputs { 
        rwg.Add(1) // about to recurse (or delegate recursion)

        select {
        case in <- subInput:
            // delegated - to another goroutine

        case <-ctx.Done():
            // context canceled...

            // but first we need to undo the earlier `rwg.Add(1)`
            // as this work item was never delegated or handled by this worker
            rwg.Done()
            return ctx.Err()

        default:
            // noone available to delegate - so this worker will need to recurse this item themselves
            err = recFunc(ctx, subInput, in, out, rwg)
            if err != nil {
                return err
            }
        }

        select {
        case <-ctx.Done():
            // always check context when doing anything potentially blocking (in this case writing to `out`)
            // context canceled
            return ctx.Err()

        case out <- subInput:
        }
    }

    return nil
}
func recJob(ctx context.Context, workers int, input string) (resultsC <-chan string, errC <-chan error) {

    // RW channels
    out := make(chan string)
    eC := make(chan error, 1)

    // R-only channels returned to caller
    resultsC, errC = out, eC

    // create workers + waitgroup logic
    go func() {

        var err error // error that will be returned to call via error channel

        defer func() {
            close(out)
            eC <- err
            close(eC)
        }()

        var wg sync.WaitGroup
        wg.Add(1)
        in := make(chan string) // input channel: shared by all workers (to read from and also to write to when they need to delegate)

        workerErrC := createWorkers(ctx, workers, in, out, &wg)

        // get the ball rolling, pass input job to one of the workers
        // Note: must be done *after* workers are created - otherwise deadlock
        in <- input

        errCount := 0

        // wait for all worker error codes to return
        for err2 := range workerErrC {
            if err2 != nil {
                log.Println("worker error:", err2)
                errCount++
            }
        }

        // all workers have completed
        if errCount > 0 {
            err = fmt.Errorf("PARTIAL RESULT: %d of %d workers encountered errors", errCount, workers)
            return
        }

        log.Printf("All %d workers have FINISHED\n", workers)
    }()

    return
}
func createWorkers(ctx context.Context, workers int, in chan string, out chan<- string, rwg *sync.WaitGroup) (errC <-chan error) {

    eC := make(chan error) // RW-version
    errC = eC              // RO-version (returned to caller)

    // track the completeness of the workers - so we know when to wrap up
    var wg sync.WaitGroup
    wg.Add(workers)

    for i := 0; i < workers; i++ {
        i := i
        go func() {
            defer wg.Done()

            var err error

            // ensure the current worker's return code gets returned
            // via the common workers' error-channel
            defer func() {
                if err != nil {
                    log.Printf("worker #%3d ERRORED: %s\n", i+1, err)
                } else {
                    log.Printf("worker #%3d FINISHED.\n", i+1)
                }
                eC <- err
            }()

            log.Printf("worker #%3d STARTED successfully\n", i+1)

            // worker scans for input
            for input := range in {

                err = recFunc(ctx, input, in, out, rwg)
                if err != nil {
                    log.Printf("worker #%3d recurseManagers ERROR: %s\n", i+1, err)
                    return
                }
            }

        }()
    }

    go func() {
        rwg.Wait() // wait for all recursion to finish
        close(in)  // safe to close input channel as all workers are blocked (i.e. no new inputs)
        wg.Wait()  // now wait for all workers to return
        close(eC)  // finally, signal to caller we're truly done by closing workers' error-channel
    }()

    return
}