需要帮助理解为什么select{}不会永远阻塞。

huangapple go评论72阅读模式
英文:

Need help understanding why select{} isn't blocking forever

问题

我正在处理一个使用通道实现队列的练习。具体来说,我试图使用通道的大小来限制同时运行的goroutine的数量。为此,我编写了以下代码:

package main

import "fmt"
import "time"
import "math/rand"

func runTask(t string, ch *chan bool) {
    start := time.Now()
    fmt.Println("starting task", t)
    time.Sleep(time.Millisecond * time.Duration(rand.Int31n(1500))) // 模拟处理时间
    fmt.Println("done running task", t, "in", time.Since(start))
    <-*ch
}

func main() {
    numWorkers := 3
    files := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}

    activeWorkers := make(chan bool, numWorkers)

    for _, f := range files {
        activeWorkers <- true
        fmt.Printf("activeWorkers is %d long.\n", len(activeWorkers))
        go runTask(f, &activeWorkers)
    }
    select {}
}

目前,代码会崩溃并显示以下错误信息:

throw: all goroutines are asleep - deadlock!

我原本期望调用select会永远阻塞,并让goroutine在没有死锁的情况下终止。

所以我有一个双重问题:为什么select不会永远阻塞,除了在for循环之后加入time.Sleep()调用之外,我如何避免死锁?

谢谢!

英文:

I am working on an exercise in using channels to implement a queue. Specifically, I am trying to use the size of a channel to limit the number of simultaneous goroutines. To wit, I have written the following code:

package main

import &quot;fmt&quot;
import &quot;time&quot;
import &quot;math/rand&quot;

func runTask (t string, ch *chan bool) {
        start := time.Now()
        fmt.Println(&quot;starting task&quot;, t)
        time.Sleep(time.Millisecond * time.Duration(rand.Int31n(1500))) // fake processing time
        fmt.Println(&quot;done running task&quot;, t, &quot;in&quot;, time.Since(start))
        &lt;- *ch
}

func main() {
        numWorkers := 3
        files := []string{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;, &quot;f&quot;, &quot;g&quot;, &quot;h&quot;, &quot;i&quot;, &quot;j&quot;}

        activeWorkers := make(chan bool, numWorkers)

        for _, f := range files {
                activeWorkers &lt;- true
                fmt.Printf(&quot;activeWorkers is %d long.\n&quot;, len(activeWorkers))
                go runTask(f, &amp;activeWorkers)
        }
        select{}
}

Right now, the code crashes with:

throw: all goroutines are asleep - deadlock!

My expectation was that the call to select would block forever and let the goroutines terminate without a deadlock.

So I have a two-fold question: why isn't select blocking forever and, short of throwing in a time.Sleep() call after the for loop, how can I avoid deadlocks?

Cheers,

-mtw

答案1

得分: 6

Arlen Cuss已经写了一个很好的答案。我只是想为你的工作队列提出另一种设计建议。你可以不限制通道可以缓冲的条目数量,而是只生成有限数量的工作goroutine,这样感觉更自然。像这样:

package main

import "fmt"
import "time"
import "math/rand"

func runTask(t string) string {
    start := time.Now()
    fmt.Println("starting task", t)
    time.Sleep(time.Millisecond * time.Duration(rand.Int31n(1500))) // fake processing time
    fmt.Println("done running task", t, "in", time.Since(start))
    return t
}

func worker(in chan string, out chan string) {
    for t := range in {
        out <- runTask(t)
    }
}

func main() {
    numWorkers := 3

    // spawn workers
    in, out := make(chan string), make(chan string)
    for i := 0; i < numWorkers; i++ {
        go worker(in, out)
    }

    files := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}

    // schedule tasks
    go func() {
        for _, f := range files {
            in <- f
        }
    }()

    // get results
    for _ = range files {
        <-out
    }
}

如果你只想等待所有任务执行完毕,你也可以使用sync.WaitGroup,但是使用out通道的优点是你可以稍后聚合结果。例如,如果每个任务返回文件中的单词数,最后的循环可以用来累加所有单词计数。

英文:

Arlen Cuss has already written a good answer. I just want to suggest another design for your work queue. Instead of limiting the number of entries your channel can buffer, you can also just spawn a limited number of worker goroutines which feels more natural imho. Something like that:

package main

import &quot;fmt&quot;
import &quot;time&quot;
import &quot;math/rand&quot;

func runTask(t string) string {
	start := time.Now()
	fmt.Println(&quot;starting task&quot;, t)
	time.Sleep(time.Millisecond * time.Duration(rand.Int31n(1500))) // fake processing time
	fmt.Println(&quot;done running task&quot;, t, &quot;in&quot;, time.Since(start))
	return t
}

func worker(in chan string, out chan string) {
	for t := range in {
		out &lt;- runTask(t)
	}
}

func main() {
	numWorkers := 3

	// spawn workers
	in, out := make(chan string), make(chan string)
	for i := 0; i &lt; numWorkers; i++ {
		go worker(in, out)
	}

	files := []string{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;, &quot;f&quot;, &quot;g&quot;, &quot;h&quot;, &quot;i&quot;, &quot;j&quot;}

	// schedule tasks
	go func() {
		for _, f := range files {
			in &lt;- f
		}
	}()

	// get results
	for _ = range files {
		&lt;-out
	}
}

You can also use a sync.WaitGroup if you just want to wait until all tasks have been executed, but using an out channel has the advantage that you can aggregate the results later. For example if each tasks returns the number of words in that file, the final loop might be used to sum up all individual word counts.

答案2

得分: 4

首先,您不需要传递一个指向通道的指针;通道,就像映射和其他一样,是引用类型,这意味着底层数据不会被复制,只会复制指向实际数据的指针。如果您需要一个指向chan本身的指针,那么在需要的时候您会知道。

崩溃发生是因为程序进入了一个所有goroutine都被阻塞的状态。这应该是不可能的;如果每个goroutine都被阻塞,那么没有可能的进程能够唤醒另一个goroutine(因此您的程序将被挂起)。

主要的goroutine最终进入了一个select {}的状态,不等待任何人,只是挂起。一旦最后一个runTask goroutine完成,只剩下主要的goroutine,它没有等待任何人。

您需要添加一些方法来知道每个goroutine何时完成;也许另一个通道可以接收完成事件。

这有点丑陋,但可能会给您一些灵感。

package main

import "fmt"
import "time"
import "math/rand"

func runTask(t string, ch chan bool, finishedCh chan bool) {
    start := time.Now()
    fmt.Println("starting task", t)
    time.Sleep(time.Millisecond * time.Duration(rand.Int31n(1500))) // fake processing time
    fmt.Println("done running task", t, "in", time.Since(start))
    <-ch
    finishedCh <- true
}

func main() {
    numWorkers := 3
    files := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}

    activeWorkers := make(chan bool, numWorkers)
    finishedWorkers := make(chan bool)
    done := make(chan bool)

    go func() {
        remaining := len(files)
        for remaining > 0 {
            <-finishedWorkers
            remaining -= 1
        }

        done <- true
    }()

    for _, f := range files {
        activeWorkers <- true
        fmt.Printf("activeWorkers is %d long.\n", len(activeWorkers))
        go runTask(f, activeWorkers, finishedWorkers)
    }

    <-done
}
英文:

Firstly, you don't need to pass a pointer to the channel; channels, like maps and others,
are references
, meaning the underlying data isn't copied, only a pointer to the actual data. If you need a pointer to a chan itself, you'll know when that time comes.

The crash occurs because the program gets into a state where every goroutine is blocked. This should be impossible; if every goroutine is blocked, then no possible process could come and wake up another goroutine (and your program would consequently be hung).

The primary goroutine winds up in a select {}—not waiting for anyone, just hanging. Once the last runTask goroutine finishes, there's only the primary goroutine left, and it's waiting on no-one.

You'll need to add some way to know when every goroutine has finished; perhaps another channel can receive finish events.

This is a bit ugly, but might be some inspiration.

package main

import &quot;fmt&quot;
import &quot;time&quot;
import &quot;math/rand&quot;

func runTask(t string, ch chan bool, finishedCh chan bool) {
	start := time.Now()
	fmt.Println(&quot;starting task&quot;, t)
	time.Sleep(time.Millisecond * time.Duration(rand.Int31n(1500))) // fake processing time
	fmt.Println(&quot;done running task&quot;, t, &quot;in&quot;, time.Since(start))
	&lt;-ch
	finishedCh &lt;- true
}

func main() {
	numWorkers := 3
	files := []string{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;, &quot;f&quot;, &quot;g&quot;, &quot;h&quot;, &quot;i&quot;, &quot;j&quot;}

	activeWorkers := make(chan bool, numWorkers)
	finishedWorkers := make(chan bool)
	done := make(chan bool)

	go func() {
		remaining := len(files)
		for remaining &gt; 0 {
			&lt;-finishedWorkers
			remaining -= 1
		}

		done &lt;- true
	}()

	for _, f := range files {
		activeWorkers &lt;- true
		fmt.Printf(&quot;activeWorkers is %d long.\n&quot;, len(activeWorkers))
		go runTask(f, activeWorkers, finishedWorkers)
	}

	&lt;-done
}

答案3

得分: 2

tux21b已经发布了一个更符合习惯用法的解决方案,但我想以不同的方式回答你的问题。select{}确实会永久阻塞。当所有goroutine都被阻塞时,就会发生死锁。如果所有其他的goroutine都完成了,那么只剩下被阻塞的主goroutine,这就是一个死锁。

通常情况下,你希望在所有其他goroutine完成后,在主goroutine中做一些事情,可以使用它们的结果,或者只是清理工作,对此你可以像tux21b建议的那样做。如果你真的只想让主goroutine完成并让其他goroutine继续它们的工作,可以在主函数的顶部放置defer runtime.Goexit()。这将导致主goroutine退出而不退出整个程序。

英文:

tux21b has already posted a more idiomatic solution, but I would like to answer your question a different way. select{} does block forever, yes. A deadlock occurs when all goroutines are blocked. If all your other goroutines finish, then you only have the blocked main goroutine left, which is a deadlock.

Normally, you want to do something in your main goroutine after all the others have finished, either by using their results, or just cleaning up, and for that you'd do what tux21b suggested. If you really just want main to finish and leave the rest of the goroutines to do their job, put defer runtime.Goexit() at the top of your main function. This will cause it to exit without exiting to the program.

huangapple
  • 本文由 发表于 2012年4月16日 17:51:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/10171941.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定