チャンネルを読まずにチャンネルが閉じているかどうかを確認するにはどうすればよいですか?


82

これは、@ Jimtによって書かれたGoのワーカーとコントローラーモードの良い例です。golangで他のゴルーチンを一時停止および再開するためのエレガントな方法はありますか?

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:発生したパニックを回復することは私が試みたものですが、それはパニックを発生させたゴルーチンを閉じます。この場合はコントローラーになるので無駄です。

それでも、Goチームがこの機能を次のバージョンのGoに実装することは有用だと思います。


あなたの労働者の状態を処理してください!チャネルを閉じた場合、再度書き込む必要はありません。
jurka 2013

ここで、私はこれを作成しました:github.com/atedja/go-tunnel
atedja 2017年

回答:


64

ハッキーな方法で、発生したパニックを回復することによって書き込みを試みるチャネルに対してそれを行うことができます。ただし、読み取りチャネルを読み取らずに、読み取りチャネルが閉じているかどうかを確認することはできません。

どちらか

  • 最終的にそれから「真の」値を読み取ります(v <- c
  • 「真の」値と「閉じていない」インジケータを読み取る(v, ok <- c
  • ゼロ値と「閉じた」インジケータを読み取ります(v, ok <- c
  • 永久に読み取られるチャネルでブロックされます(v <- c

技術的には最後の1つだけがチャネルから読み取られませんが、それはほとんど役に立ちません。


1
発生したパニックを回復することは私が試みたものですが、それはパニックを発生させたゴルーチンを閉じます。この場合controller
Reck Hou

安全でないパッケージを使用してハックを作成し、パッケージを反映することもできます。私の答えを参照してください
youssif 2016

78

チャネルが開いているかどうかを操作せずに知る必要がある安全なアプリケーションを作成する方法はありません。

やりたいことを行うための最良の方法は、2つのチャネルを使用することです。1つは作業用で、もう1つは状態を変更したいことを示します(重要な場合は状態変更の完了も同様です)。

チャンネルは安いです。複雑な設計のオーバーロードセマンティクスはそうではありません。

[また]

<-time.After(1e9)

本当に紛らわしく、自明ではない書き方です

time.Sleep(time.Second)

物事をシンプルに保ち、誰もが(あなたを含めて)理解できるようにします。


7

私はこの答えがとても遅いことを知っています、私はこの解決策を書きました、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
}

1
go vet最後の行に戻ります「unsafe.Pointerの可能誤用」return *(*uint32)(unsafe.Pointer(cptr)) > 0cptr += unsafe.Sizeof(unsafe.Pointer(uintptr(0))) それらの線にunsafe.Pointerせずにそれを行うためのオプションがありますか?
Effi Bar-She'an 2017年

2
ベテランを満足させるには、1つの式ですべてのポインタ演算を実行する必要があります。このソリューションはデータの競合であり、有効なGoではありません。少なくともatomic.LoadUint32でclosedの読み取りを行う必要があります。いずれにせよ、これはかなり壊れやすいハックですが、hchanがGoバージョン間で変更されると、これは壊れます。
Eloff

1
これはおそらく、非常に賢いですが、それは別の問題の上に問題を追加することのように感じている
ЯрославРахматуллин

2

さて、あなたは使用することができ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
このソリューションには1つの問題があります(同様のソリューションを提案するかなりうまく書かれたgo101.org/article/channel-closeing.htmlと同様に)-バッファリングされたチャネルを使用していて、未読が含まれている場合は機能しませんデータ
Angad 2018年

@Angad確かに、これは閉じたチャネルを検出するための完璧なソリューションではありません。これは、チャネルの読み取りがブロックされるかどうかを検出するための完璧なソリューションです。(つまり、チャネルの読み取りがブロックされる場合は、チャネルが閉じられていないことがわかります。チャネルの読み取りがブロックされない場合は、チャネルが閉じている可能性があることがわかります)。
tombrown 5220年

0

チャンネルを閉じるだけでなく、チャンネルをnilに設定することもできます。そうすれば、それがゼロかどうかを確認できます。

遊び場での例:https//play.golang.org/p/v0f3d4DisCz

編集:次の例で示すように、これは実際には悪い解決策です。関数でチャネルをnilに設定すると壊れてしまうためです:https//play.golang.org/p/YVE2-LV9TOp


アドレスによって(またはアドレスによって渡された構造体の)チャネルを通過
ChuckCottrill

-1

ドキュメントから:

組み込み関数closeを使用してチャネルを閉じることができます。受信演算子の複数値割り当てフォームは、チャネルが閉じられる前に受信値が送信されたかどうかを報告します。

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 
}

3
以下のようダスティンで述べた、安全にこれを行う方法はありません。あなたがあなたのif体に入る時までlen(ch)に何でもありえます。(たとえば、別のコアのゴルーチンは、selectが読み取ろうとする前にチャネルに値を送信します)。
デイブC

-7

このチャンネルを聞くと、いつでもそのチャンネルが閉じられていることがわかります。

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

ただし、1つのチャネルを2回閉じることはできません。これはパニックを引き起こします。


5
私は「それを読まずに」と言いました、質問を注意深く読まなかったために-1。
Reck Hou

> PS:発生したパニックを回復することは私が試みたものですが、パニックを発生させたゴルーチンを閉じます。この場合はコントローラーになるので無駄です。あなたはいつでも行くことができますfunc(chan z){defer func(){//ハンドル回復} close(z)}
jurka 2013

ただし、コントローラーを予約する必要があり、コントローラーでclose(z)はなくワーカーから呼び出されます。
Reck Hou
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.