1. ホーム
  2. go

[解決済み] チャンネルを読まずに閉じたかどうかを確認する方法は?

2023-01-09 09:09:48

質問

これは @Jimt によって書かれた、Go の workers & controller モードの良い例で、次のような答えになっています。 に対する回答です。 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]) に書き込むと、閉じたチャネルに書き込むことができないため、 次回コントローラが書き込んだときにパニックが発生します。もし、mutex を使ってそれを保護した場合、それは workers[i] <- Running で止まってしまいます。 worker はチャネルから何も読んでいないので、書き込みはブロックされ、mutex はデッドロックを引き起こします。回避策としてチャネルに大きなバッファを与えることもできますが、十分なものではありません。

というわけで、これを解決する最良の方法は worker() を使用して、もしコントローラが閉じたチャンネルを見つけたら、それを飛び越えて何もしないようにします。しかし、この状況でチャンネルがすでに閉じられているかどうかを確認する方法が見つかりません。コントローラでチャンネルを読み込もうとすると、コントローラがブロックされるかもしれません。というわけで、今のところ非常に困っています。

追記:パニックが発生したときのリカバリは私も試しましたが、パニックが発生したgoroutineを閉じてしまいます。この場合、コントローラになるので仕方ないですね。

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

どのように解決するのですか?

パニックを起こしたチャネルを回復することで、書き込みを試みたチャネルに対して行うことができます。しかし、読み取りチャネルが閉じているかどうかは、そこから読み取ることなしに確認することはできません。

どちらか一方は

  • 最終的にそこから "true" 値を読み取ります ( v <- c )
  • true"値と'not closed'インジケータを読み取ります ( v, ok <- c )
  • を読む ゼロ値 と、「閉じた」インジケータ( v, ok <- c ) ( )
  • は、チャネルリードで永遠にブロックされます ( v <- c )

最後の1つだけが技術的にチャンネルから読み込まれませんが、それはほとんど意味がありません。