如何从主线程退出

时间:2019-11-17 07:16:29

标签: go

func GoCountColumns(in chan []string, r chan Result, quit chan int) {
    for {
        select {
        case data := <-in:
            r <- countColumns(data) // some calculation function
        case <-quit:
            return // stop goroutine
        }
    }

}

func main() {
    fmt.Println("Welcome to the csv Calculator")
    file_path := os.Args[1]
    fd, _ := os.Open(file_path)
    reader := csv.NewReader(bufio.NewReader(fd))
    var totalColumnsCount int64 = 0
    var totallettersCount int64 = 0
    linesCount := 0
    numWorkers := 10000
    rc := make(chan Result, numWorkers)
    in := make(chan []string, numWorkers)
    quit := make(chan int)
    t1 := time.Now()

    for i := 0; i < numWorkers; i++ {
        go GoCountColumns(in, rc, quit)
    }
    //start worksers
    go func() {
        for {
            record, err := reader.Read()
            if err == io.EOF {
                break
            }
            if err != nil {
                log.Fatal(err)
            }

            if linesCount%1000000 == 0 {
                fmt.Println("Adding to the channel")
            }
            in <- record
            //data := countColumns(record)
            linesCount++
            //totalColumnsCount = totalColumnsCount + data.ColumnCount
            //totallettersCount = totallettersCount + data.LettersCount
        }
        close(in)
    }()

    for i := 0; i < numWorkers; i++ {
        quit <- 1 // quit goroutines from main
    }
    close(rc)
    for i := 0; i < linesCount; i++ {
        data := <-rc
        totalColumnsCount = totalColumnsCount + data.ColumnCount
        totallettersCount = totallettersCount + data.LettersCount
    }

    fmt.Printf("I counted %d lines\n", linesCount)
    fmt.Printf("I counted %d columns\n", totalColumnsCount)
    fmt.Printf("I counted %d letters\n", totallettersCount)
    elapsed := time.Now().Sub(t1)
    fmt.Printf("It took %f seconds\n", elapsed.Seconds())
}

“我的世界”是一个程序,可以读取csv文件并将其传递给频道。然后,goroutines应该从此通道进行消耗。 我的问题是我不知道如何从主线程中检测到所有数据都已处理并且可以退出程序。

3 个答案:

答案 0 :(得分:2)

在其他答案之上。

  • (非常)注意关闭通道应该在写调用站点而不是读调用站点上进行。在GoCountColumns的{​​{1}}通道中,关闭该通道的责任在r函数上。技术上的原因是,这是唯一知道该通道将不再被写入并因此可以安全关闭的演员。
GoCountColumns
  • 如果可以说,函数参数的命名约定是将目标作为第一个参数,将源作为第二个参数,以及其他参数。 func GoCountColumns(in chan []string, r chan Result, quit chan int) { defer close(r) // this line. for { select { case data := <-in: r <- countColumns(data) // some calculation function case <-quit: return // stop goroutine } } } 最好写为:
GoCountColumns
  • 该过程开始后,您正在呼叫 func GoCountColumns(dst chan Result, src chan []string, quit chan int) { defer close(dst) for { select { case data := <-src: dst <- countColumns(data) // some calculation function case <-quit: return // stop goroutine } } } 。它不合逻辑。该quit命令是强制退出序列,一旦检测到退出信号,就应调用该命令,以可能的最佳状态强制退出当前处理,可能会全部中断。换句话说,您应该依靠quit包捕获退出事件,并通知您的工作人员退出。参见https://golang.org/pkg/os/signal/#example_Notify

要编写更好的并行代码,请首先列出管理程序生命周期所需的例程,并确定需要阻止的例程以确保程序在退出之前已完成。

在您的代码中,存在signal.Notifyread。为了确保完整的处理,程序主要功能必须确保退出map时捕获到信号。请注意,map函数无关紧要。

然后,您还将需要捕获用户输入中的退出事件所需的代码。

总体而言,看来我们需要阻止两个事件来管理生命周期。

read

这个简单的代码对func main(){ go read() go map(mapDone) go signal() select { case <-mapDone: case <-sig: } } 很有用。确实,当捕获到用户事件时,程序将立即退出,而不会给其他例程提供机会在停止时执行所需的操作。

要改善这些行为,您首先需要一种方法来向程序发出要离开其他程序的信号,其次,需要一种等待这些例程完成其停止序列再离开的方法。

要发出退出事件或取消的信号,您可以使用process or die,将其传递给工作人员,让他们听。

再次示意性地

context.Context

(更多内容供以后阅读和映射)

要等待完成,只要线程安全,许多事情都是可能的。通常,使用func main(){ ctx,cancel := context.WithCancel(context.WithBackground()) go read(ctx) go map(ctx,mapDone) go signal() select { case <-mapDone: case <-sig: cancel() } } 。或者,在像您这样的情况下,只有一个例程可以等待,我们可以重新使用当前的sync.WaitGroup频道。

mapDone

这很简单直接。但这并不完全正确。最后一个mapDone chan可能永远阻塞,并使程序无法停止。因此,您可以实现第二个信号处理程序或超时。

从原理上讲,超时解决方案是

func main(){
    ctx,cancel := context.WithCancel(context.WithBackground())
    go read(ctx)
    go map(ctx,mapDone)
    go signal()
    select {
        case <-mapDone:
        case <-sig:
            cancel()
            <-mapDone
    }
}

您可能还会在最后一次选择中累积信号处理和超时。

最后,关于func main(){ ctx,cancel := context.WithCancel(context.WithBackground()) go read(ctx) go map(ctx,mapDone) go signal() select { case <-mapDone: case <-sig: cancel() select { case <-mapDone: case <-time.After(time.Second): } } } read上下文监听的事情很少。

map开始,该实现需要定期读取map通道以检测context.Done

这是简单的部分,它只需要更新select语句。

cancellation

现在 func GoCountColumns(ctx context.Context, dst chan Result, src chan []string) { defer close(dst) for { select { case <-ctx.Done(): <-time.After(time.Minute) // do something more useful. return // quit. Notice the defer will be called. case data := <-src: dst <- countColumns(data) // some calculation function } } } 部分比较棘手,因为它是一个IO,它没有提供read可用的编程接口,因此侦听上下文通道取消似乎是矛盾的。它是。由于IO受阻,因此无法监听上下文。从上下文通道读取时,无法读取IO。在您的情况下,该解决方案需要了解您的读取循环与程序寿命无关(回想一下,我们仅侦听mapDone吗?),并且我们可以忽略上下文。

在其他情况下,例如,如果您想在最后一个字节读取时重新启动(因此,在每次读取时,我们将n递增,计数字节,并希望在停止时保存该值)。然后,需要启动一个新的例程,因此,多个例程要等待完成。在这种情况下,select更合适。

示意性地

sync.WaitGroup

在最后的代码中,正在传递等待组。例程负责调用func main(){ var wg sync.WaitGroup processDone:=make(chan struct{}) ctx,cancel := context.WithCancel(context.WithBackground()) go read(ctx) wg.Add(1) go saveN(ctx,&wg) wg.Add(1) go map(ctx,&wg) go signal() go func(){ wg.Wait() close(processDone) }() select { case <-processDone: case <-sig: cancel() select { case <-processDone: case <-time.After(time.Second): } } } ,当所有例程完成后,wg.Done()通道将关闭,以发出选择信号。

processDone

尚不确定哪种模式是优选的,但是您可能还会看到 func GoCountColumns(ctx context.Context, dst chan Result, src chan []string, wg *sync.WaitGroup) { defer wg.Done() defer close(dst) for { select { case <-ctx.Done(): <-time.After(time.Minute) // do something more useful. return // quit. Notice the defer will be called. case data := <-src: dst <- countColumns(data) // some calculation function } } } 仅在呼叫站点受到管理。

waitgroup

除了所有这些问题和OP问题,您必须始终预先评估给定任务的并行处理的相关性。没有独特的配方,练习和衡量代码性能。参见pprof。

答案 1 :(得分:1)

您可以使用通道来阻塞const initial = [[1, 'a'], [2, 'b'], [3, 'c']]; const result = R.transpose(initial); console.log('result is', result); const data = R.transpose(result); console.log('data is', data);,直到完成goroutine。

<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js" integrity="sha256-xB25ljGZ7K2VXnq087unEnoVhvTosWWtqXB4tAtZmHU=" crossorigin="anonymous"></script>

无需在通道中写入任何内容。在读取数据或关闭通道之前,将阻止读取。

答案 2 :(得分:0)

此代码中发生了太多事情。您应该将代码重组为适合特定目的的简短函数,以使某人可以轻松地帮助您(也可以帮助自己)。

您应该阅读以下Go文章,其中涉及了并发模式: https://blog.golang.org/pipelines

有多种方法可以使一个例行程序等待其他工作完成。最常见的方法是使用等待组(例如我提供的示例)或渠道。


func processSomething(...) {
    ...
}

func main() {
    workers := &sync.WaitGroup{}

    for i := 0; i < numWorkers; i++ {
        workers.Add(1) // you want to call this from the calling go-routine and before spawning the worker go-routine

        go func() {
            defer workers.Done() // you want to call this from the worker go-routine when the work is done (NOTE the defer, which ensures it is called no matter what)
            processSomething(....) // your async processing
        }()
    }

    // this will block until all workers have finished their work
    workers.Wait()
}