这是我的程序,它会产生死锁,如何避免死锁,以及建议使用哪种模式来处理这种情况。

问题是超时后如何检测我的频道上没有阅读器?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var wg sync.WaitGroup

func main() {  
    wg.Add(1)
    c := make(chan int)
    go readFromChannel(c, time.After(time.Duration(2)*time.Second))
    time.Sleep(time.Duration(5) * time.Second)
    c <- 10
    wg.Wait()
}

func readFromChannel(c chan int, ti <-chan time.Time) {
    select {
    case x := <-c:
        fmt.Println("Read", x)
    case <-ti:
        fmt.Println("TIMED OUT")
    }
    wg.Done()
}

因此,让我们看一下源代码中真正发生的事情。您有两个goroutines(有两个以上,但我们将重点放在显式的)上,mainreadFromChannel

让我们看看readFromChannel的作用:

1
2
if channel `c` is not empty before `ti` has expired, print its contents and return, after signalling its completion to wait group.
if `ti` has expired before `c` is not empty, print"TIMED OUT" and return, after signalling its completion to wait group.

现在主要:

1
2
3
4
5
6
adds to waitgroup
make a channel `c`
start a goroutine `readFromChannel`
sleep for 5 seconds
send 10 to channel `c`
call wait for waitgroup

现在,让我们同时浏览代码的执行流程(请记住,您的代码可能/可能不会每次都按此顺序执行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1) wg.Add(1)
2) c := make(chan int)
3) go readFromChannel(c, time.After(time.Duration(2)*time.Second))
#timer ti starts#
4) time.Sleep(time.Duration(5) * time.Second)
#MAIN Goroutine begins sleep
#timer ti expires#
5) case <-ti:
6) fmt.Println("TIMED OUT")
7) wg.Done()
# readFromChannel Goroutine returns #
#MAIN Goroutine exits sleep#
8) c<-10
9) ......#DEADLOCK#

现在您可以猜测为什么会陷入僵局。进行中,非缓冲通道将阻塞,直到该通道的另一端发生任何事情,无论您是发送还是接收。因此,c <- 10将一直阻塞,直到从c的另一端读取某些内容为止,但是您拥有的goroutine在2秒钟前就退出了画面。因此,c会永远阻塞,并且由于main是剩下的最后一个goroutine,因此会出现死锁。

怎么预防呢?使用通道时,请确保每个send的通道另一端始终有一个receive。您也可以使用缓冲通道,但是在上面的代码中,它不是"正确的"解决方案。

这是我的僵局解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
    wg.Add(1)
    c := make(chan int)
    go readFromChannel(c, time.After(time.Duration(2)*time.Second))
    time.Sleep(time.Duration(5) * time.Second)
    c <- 10
    wg.Wait()
}

func readFromChannel(c chan int, ti <-chan time.Time) {
        // the forloop will run forever
    loop: // **
    for {
        select {
            case x := <-c:
                    fmt.Println("Read", x)
                    break loop // breaks out of the for loop and the select **
            case <-ti:
                    fmt.Println("TIMED OUT")
            }
    }
    wg.Done()
}

**有关详细信息,请参见此答案

  • 这是我一段时间以来阅读的最有用的答案之一。 只是要确保我遵循:"修复"之所以有效,是因为即使在超时后,它也可使接收器通道继续运行。 因此,只有从c读入内容时,wg.Done()(并终止main go例程)才会发生,对吗?
  • 没错,但是要清理一点,它可以使goroutine保持运行状态。

您有一个无缓冲的频道。根据文档:

If the channel is unbuffered, the sender blocks until the receiver has
received the value. If the channel has a buffer, the sender blocks
only until the value has been copied to the buffer

通过将通道更改为缓冲通道,我们可以避免死锁。

1
c := make(chan int, 10) // holds 10 ints

我还建议阅读https://golang.org/doc/effective_go.html#channels,那里有一些与频道有关的好东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func main() {

    wg.Add(1)
    c := make(chan int)
    go func() {
        c <- 10

    }()
    go readFromChannel(c, time.After(time.Duration(2)*time.Second))
    time.Sleep(time.Duration(5) * time.Second)

    wg.Wait()

}//This will read without deadlock

func main() {
    wg.Add(1)
    c := make(chan int)

    go readFromChannel(c, time.After(time.Duration(2)*time.Second))
    time.Sleep(time.Duration(5) * time.Second)

    go func() {
        c <- 10

    }()
    wg.Wait()

}//Time out without deadlock
  • 这可以解释您所做的事情/与原始内容有何不同

这是一个比较老的问题,但是我本人正在深入学习渠道,并在这里找到了。

我认为您只需要在发送完频道后关闭它?

码:

1
2
3
4
5
6
7
8
9
func main() {
    wg.Add(1)
    c := make(chan int)
    go readFromChannel(c, time.After(time.Duration(2)*time.Second))
    time.Sleep(time.Duration(5) * time.Second)
    c <- 10
    close(c) // <- CLOSE IT HERE
    wg.Wait()
}

您的问题是您正在使用select语句,但没有在goroutine内部使用。

1
2
3
4
5
6
7
8
9
10
go func() {
    for {
        select {
        case x := <-c:
            fmt.Println("Read", x)
        case <-ti:
            fmt.Println("TIMED OUT")
        }
    }
}()

可以使用select关键字从不同的并发执行的goroutine中获取值,该关键字与switch控制语句非常相似,有时也称为通讯switch。

在带有默认大小写的select语句中使用send操作可确保发送将是非阻塞的!如果没有情况,则选择将永远阻止执行。

https://play.golang.org/p/Ai1ggveb4s