如何在不阅读的情况下检查通道是否关闭?


80

这是@Jimt在Go中编写的工作程序和控制器模式的一个很好的示例,以回答“在golang中是否有某种优雅的方式来暂停和恢复任何其他goroutine?

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

// Possible worker states.
const (
    Stopped = 0
    Paused  = 1
    Running = 2
)

// Maximum number of workers.
const WorkerCount = 1000

func main() {
    // Launch workers.
    var wg sync.WaitGroup
    wg.Add(WorkerCount + 1)

    workers := make([]chan int, WorkerCount)
    for i := range workers {
        workers[i] = make(chan int)

        go func(i int) {
            worker(i, workers[i])
            wg.Done()
        }(i)
    }

    // Launch controller routine.
    go func() {
        controller(workers)
        wg.Done()
    }()

    // Wait for all goroutines to finish.
    wg.Wait()
}

func worker(id int, ws <-chan int) {
    state := Paused // Begin in the paused state.

    for {
        select {
        case state = <-ws:
            switch state {
            case Stopped:
                fmt.Printf("Worker %d: Stopped\n", id)
                return
            case Running:
                fmt.Printf("Worker %d: Running\n", id)
            case Paused:
                fmt.Printf("Worker %d: Paused\n", id)
            }

        default:
            // We use runtime.Gosched() to prevent a deadlock in this case.
            // It will not be needed of work is performed here which yields
            // to the scheduler.
            runtime.Gosched()

            if state == Paused {
                break
            }

            // Do actual work here.
        }
    }
}

// controller handles the current state of all workers. They can be
// instructed to be either running, paused or stopped entirely.
func controller(workers []chan int) {
    // Start workers
    for i := range workers {
        workers[i] <- Running
    }

    // Pause workers.
    <-time.After(1e9)
    for i := range workers {
        workers[i] <- Paused
    }

    // Unpause workers.
    <-time.After(1e9)
    for i := range workers {
        workers[i] <- Running
    }

    // Shutdown workers.
    <-time.After(1e9)
    for i := range workers {
        close(workers[i])
    }
}

但是此代码也有一个问题:如果要在退出workers时删除工作通道,则会worker()发生死锁。

如果您使用close(workers[i]),则下次控制器写入该命令将导致恐慌,因为go无法写入一个已关闭的通道。如果您使用某些互斥锁来保护它,则workers[i] <- Running由于worker它不会从通道读取任何内容,并且将阻止写入,因此互斥锁将导致死锁。您也可以为频道提供更大的缓冲空间,但这还不够。

因此,我认为解决此问题的最佳方法是worker()退出时关闭通道,如果控制器发现通道已关闭,它将跳过该通道而无所作为。但是在这种情况下,我找不到如何检查通道是否已关闭的信息。如果尝试读取控制器中的通道,则控制器可能被阻止。所以我现在很困惑。

PS:恢复引发恐慌是我尝试过的方法,但是它将关闭引起恐慌的goroutine。在这种情况下它将是控制器,所以没有用。

尽管如此,我认为对于Go团队在下一版Go中实现此功能还是有用的。


处理工人的状态!如果关闭通道,则无需再次写入。
jurka

在这里,我做了这个:github.com/atedja/go-tunnel
tedja

Answers:


62

通过变通的方式,可以通过恢复引发的恐慌来尝试写入的通道。但是您无法检查读取通道是否在未读取的情况下关闭。

要么你会

  • 最终从中读取“ true”值(v <- c
  • 读取“真”值和“未关闭”指示符(v, ok <- c
  • 读取零值和“已关闭”指示器(v, ok <- c
  • 会永远封锁该频道(v <- c

从技术上讲,只有最后一个不会从该频道读取,但这没什么用。


1
我已经尝试过恢复引发的恐慌,但是它将关闭引发恐慌的goroutine。在这种情况下,controller它就没用了:)
Reck Hou

您还可以使用不安全的方法写hack并反映程序包,请参阅我的答案
youssif

76

无法编写安全的应用程序,您需要在其中知道通道是否打开而不进行交互。

要做您想做的事情的最好方法是使用两个渠道-一个用于工作,一个用于表明改变状态的愿望(以及重要的是完成状态改变)。

频道便宜。复杂的设计过载语义不是。

[也]

<-time.After(1e9)

是一种非常令人困惑且非显而易见的写作方式

time.Sleep(time.Second)

保持简单,每个人(包括您)都可以理解它们。


6

我知道这个答案太晚了,我已经写了这个解决方案,Hacking Go运行时,这不是安全的,它可能会崩溃:

import (
    "unsafe"
    "reflect"
)


func isChanClosed(ch interface{}) bool {
    if reflect.TypeOf(ch).Kind() != reflect.Chan {
        panic("only channels!")
    }

    // get interface value pointer, from cgo_export 
    // typedef struct { void *t; void *v; } GoInterface;
    // then get channel real pointer
    cptr := *(*uintptr)(unsafe.Pointer(
        unsafe.Pointer(uintptr(unsafe.Pointer(&ch)) + unsafe.Sizeof(uint(0))),
    ))

    // this function will return true if chan.closed > 0
    // see hchan on https://github.com/golang/go/blob/master/src/runtime/chan.go 
    // type hchan struct {
    // qcount   uint           // total data in the queue
    // dataqsiz uint           // size of the circular queue
    // buf      unsafe.Pointer // points to an array of dataqsiz elements
    // elemsize uint16
    // closed   uint32
    // **

    cptr += unsafe.Sizeof(uint(0))*2
    cptr += unsafe.Sizeof(unsafe.Pointer(uintptr(0)))
    cptr += unsafe.Sizeof(uint16(0))
    return *(*uint32)(unsafe.Pointer(cptr)) > 0
}

https://gist.github.com/youssifsayed/ca0cfcf9dc87905d37a4fee7beb253c2


go vet在最后一行返回“可能滥用unsafe.Pointer” return *(*uint32)(unsafe.Pointer(cptr)) > 0cptr += unsafe.Sizeof(unsafe.Pointer(uintptr(0))) 是否可以选择在没有这些行为的情况下使用unsafe.Pointer?
Effi Bar-She'an

您需要在一个表达式中完成所有指针运算,以使兽医满意。该解决方案是一场数据竞赛,不是有效的Go语言,您还必须至少读取atomic.LoadUint32关闭的内容。不管怎样,这都是一个非常脆弱的hack,如果hchan在Go版本之间进行更改,它将被破坏。
Eloff

这可能是非常聪明的,但感觉像在另一个问题之上添加一个问题
ЯрославРахматуллин

3

那么,可以使用default分支检测到它,对于一个封闭的通道将被选择,例如:下面的代码将选择defaultchannelchannel,第一选择没有被阻塞。

func main() {
    ch := make(chan int)

    go func() {
        select {
        case <-ch:
            log.Printf("1.channel")
        default:
            log.Printf("1.default")
        }
        select {
        case <-ch:
            log.Printf("2.channel")
        }
        close(ch)
        select {
        case <-ch:
            log.Printf("3.channel")
        default:
            log.Printf("3.default")
        }
    }()
    time.Sleep(time.Second)
    ch <- 1
    time.Sleep(time.Second)
}

版画

2018/05/24 08:00:00 1.default
2018/05/24 08:00:01 2.channel
2018/05/24 08:00:01 3.channel

3
此解决方案存在一个问题(以及写得很好的go101.org/article/channel-closing.html提出了类似的解决方案)-如果您使用的是缓冲通道并且其中包含未读内容,则该方法不起作用数据
Angad

@Angad的确,这不是检测关闭频道的完美解决方案。这是检测读取通道是否会阻塞的理想解决方案。(即,如果读取通道将被阻塞,那么我们知道它没有关闭;如果读取通道将不会被阻塞,我们知道它可能是关闭的)。
tombrown52

0

从文档中:

可以使用内置功能关闭通道来关闭通道。接收操作符的多值赋值形式报告在关闭通道之前是否发送了接收值。

https://golang.org/ref/spec#Receive_operator

Golang in Action的示例显示了这种情况:

// This sample program demonstrates how to use an unbuffered
// channel to simulate a game of tennis between two goroutines.
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// wg is used to wait for the program to finish.
var wg sync.WaitGroup

func init() {
    rand.Seed(time.Now().UnixNano())
}

// main is the entry point for all Go programs.
func main() {
    // Create an unbuffered channel.
    court := make(chan int)
    // Add a count of two, one for each goroutine.
    wg.Add(2)
    // Launch two players.
    go player("Nadal", court)
    go player("Djokovic", court)
    // Start the set.
    court <- 1
    // Wait for the game to finish.
    wg.Wait()
}

// player simulates a person playing the game of tennis.
func player(name string, court chan int) {
    // Schedule the call to Done to tell main we are done.
    defer wg.Done()
    for {
        // Wait for the ball to be hit back to us.
        ball, ok := <-court
        fmt.Printf("ok %t\n", ok)
        if !ok {
            // If the channel was closed we won.
            fmt.Printf("Player %s Won\n", name)
            return
        }
        // Pick a random number and see if we miss the ball.
        n := rand.Intn(100)
        if n%13 == 0 {
            fmt.Printf("Player %s Missed\n", name)
            // Close the channel to signal we lost.
            close(court)
            return
        }

        // Display and then increment the hit count by one.
        fmt.Printf("Player %s Hit %d\n", name, ball)
        ball++
        // Hit the ball back to the opposing player.
        court <- ball
    }
}

2
问题是如何在不读取通道的情况下(即在写入通道之前)检查关闭状态。
彼得


-5

首先更容易检查通道是否包含元素,以确保该通道处于活动状态。

func isChanClosed(ch chan interface{}) bool {
    if len(ch) == 0 {
        select {
        case _, ok := <-ch:
            return !ok
        }
    }
    return false 
}

2
正如达斯汀(Dustin)所说,没有办法安全地做到这一点。到你进入if身体的时候len(ch),什么都可以。(例如,另一个内核上的goroutine在您选择读取之前将一个值发送到通道)。
Dave C

-8

如果您收听此频道,则始终可以发现该频道已关闭。

case state, opened := <-ws:
    if !opened {
         // channel was closed 
         // return or made some final work
    }
    switch state {
        case Stopped:

但是请记住,您不能两次关闭一个通道。这会引起恐慌。


5
我说“不读”,-1为不认真阅读问题。
Reck Hou 2013年

> PS:我曾尝试过恢复引发的恐慌,但是它将关闭引发恐慌的goroutine。在这种情况下,它将是控制器,所以没有用。您总是可以去func(chan z){延迟func(){//处理恢复} close(z)}
jurka 2013年

但是我必须保留控制器,close(z)它将由工作人员而不是控制器调用。
Reck Hou 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.