web-dev-qa-db-ja.com

チャンネルを読むことなく閉じているかどうかを確認する方法は?

これは、@ 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])
    }
}

ただし、このコードには問題もあります。worker()が終了したときにworkersのワーカーチャネルを削除すると、デッドロックが発生します。

close(workers[i])の場合、goが閉じたチャネルに書き込むことができないため、次回コントローラが書き込みを行うとパニックが発生します。ミューテックスを使用して保護すると、workerがチャネルから何も読み取っていないため、workers[i] <- Runningに固定され、書き込みがブロックされ、ミューテックスがデッドロックを引き起こします。回避策としてチャネルに大きなバッファを与えることもできますが、それでは十分ではありません。

したがって、これを解決する最善の方法は、worker()終了時にチャネルを閉じることです。コントローラがチャネルを閉じていることを検出すると、それを飛び越えて何もしません。しかし、この状況でチャンネルが既に閉じられているかどうかを確認する方法が見つかりません。コントローラでチャネルを読み取ろうとすると、コントローラがブロックされる可能性があります。だから今のところとても混乱しています。

PS:発生したパニックを回復することは私が試したことですが、パニックを発生させたゴルーチンを閉じます。この場合、コントローラーになりますので、使い物になりません。

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

56
Reck Hou

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

どちらか

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

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

45
zzzz

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

やりたいことを行う最善の方法は、2つのチャネルを使用することです。1つは仕事用で、もう1つは状態を変更したいという意向を示します(それが重要な場合は状態の変更を完了します)。

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

[また]

<-time.After(1e9)

書くのは本当に紛らわしくて非自明な方法です

time.Sleep(time.Second)

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

64
Dustin

私はこの答えが非常に遅いことを知っています、私はこの解決策を書きました、ハッキング 実行時 、それは安全ではなく、クラッシュする可能性があります:

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

5
youssif

ドキュメントから:

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

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

Golangの実際の例は、このケースを示しています。

// 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
    }
}
0
Israel Barba

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)
}
0
acrazing

たぶん私は何かを見逃しているかもしれませんが、これを処理するための簡単で正しい方法は、チャネルに「停止」を送信して(ゴールーチンを終了する)、チャネルを閉じてnilに設定することです。

閉じたチャネルを読み取らずに確認する必要があると思われる場合は、設計に問題があります。 (一時停止したワーカーの「ビジーループ」など、コードには他の問題があることに注意してください。)

0