채널을 읽지 않고 닫혔는지 여부를 확인하는 방법은 무엇입니까?


82

이것은 @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])
    }
}

하지만이 코드는 문제가 있습니다 : 당신이 노동자 채널을 제거하려면 workersworker()종료가 죽은 잠금이 발생합니다.

만약 당신 close(workers[i])이 다음 번에 컨트롤러가 그것에 쓸 때 go가 닫힌 채널에 쓸 수 없기 때문에 패닉이 발생할 것입니다. 당신이 그것을 보호하기 위해 몇 가지 뮤텍스를 사용하는 경우, 그것은에 붙어있을 것입니다 workers[i] <- Running(가)부터 worker차단됩니다 채널 및 쓰기에서 아무것도 읽을되지 않으며, 뮤텍스는 죽은 잠금의 원인이됩니다. 해결 방법으로 채널에 더 큰 버퍼를 제공 할 수도 있지만 충분하지 않습니다.

따라서이 문제를 해결하는 가장 좋은 방법은 worker()종료 할 때 채널을 닫는 것입니다. 컨트롤러가 닫힌 채널을 발견하면 해당 채널을 건너 뛰고 아무것도하지 않습니다. 하지만이 상황에서 채널이 이미 닫혔는지 여부를 확인하는 방법을 찾을 수 없습니다. 컨트롤러에서 채널을 읽으려고하면 컨트롤러가 차단 될 수 있습니다. 그래서 지금은 매우 혼란 스럽습니다.

추신 : 제기 된 패닉을 복구하는 것은 내가 시도한 것이지만 패닉을 일으킨 고 루틴을 닫을 것입니다. 이 경우 컨트롤러가되므로 쓸모가 없습니다.

그래도 Go 팀이 다음 버전의 Go에서이 기능을 구현하는 것이 유용하다고 생각합니다.


작업자의 상태를 처리하십시오! 채널을 닫으면 다시 쓸 필요가 없습니다.
jurka

여기에서 만들었습니다 : github.com/atedja/go-tunnel .
atedja

답변:


64

해키 방식으로 제기 된 패닉을 복구하여 쓰기를 시도하는 채널에 대해 수행 할 수 있습니다. 그러나 읽기 채널이 읽히지 않고 닫혀 있는지 확인할 수 없습니다.

당신은

  • 결국 "true"값을 읽습니다 ( v <- c).
  • "참"값 및 '닫히지 않음'표시기 ( v, ok <- c) 읽기
  • 0 값 및 '닫힌'표시기 ( v, ok <- c) 읽기
  • 채널에서 영구적으로 읽기 ( v <- c) 차단됩니다.

기술적으로 마지막 하나만 채널에서 읽지 않지만 거의 사용되지 않습니다.


1
제기 된 패닉을 회복하는 것은 제가 시도한 것이지만 패닉을 일으킨 고 루틴을 닫을 것입니다. 이 경우는 것 controller이 :) 아무 소용이 그래서
관계하다 허우

당신은 내 대답을 참조 안전하지 않은를 사용하여 해킹 작성하고 패키지를 반영 할 수
여 시프

78

채널과 상호 작용하지 않고 채널이 열려 있는지 여부를 알아야하는 안전한 애플리케이션을 작성할 수있는 방법은 없습니다.

원하는 작업을 수행하는 가장 좋은 방법은 두 개의 채널을 사용하는 것입니다. 하나는 작업을위한 것이고 다른 하나는 상태를 변경하려는 욕구를 나타내는 것입니다 (중요한 경우 해당 상태 변경 완료).

채널은 저렴합니다. 복잡한 디자인 오버로딩 의미론은 그렇지 않습니다.

[또한]

<-time.After(1e9)

작성하는 데 정말 혼란스럽고 분명하지 않은 방법입니다.

time.Sleep(time.Second)

일을 단순하게 유지하면 모든 사람이 이해할 수 있습니다.


7

이 답변이 너무 늦었다는 것을 알고 있습니다.이 솔루션을 작성했습니다. Hacking Go run-time , 안전하지 않습니다. 충돌 할 수 있습니다.

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 2011

2
계속해서 행복을 유지하려면 하나의 표현식에서 모든 포인터 산술을 수행해야합니다. 이 솔루션은 데이터 경쟁이며 유효한 Go가 아닙니다. 또한 최소한 atomic.LoadUint32로 closed를 읽어야합니다. 어쨌든 꽤 깨지기 쉬운 해킹이지만, Go 버전간에 hchan이 변경되면 깨질 것입니다.
Eloff

1
이것은 아마도 매우 영리하다, 그러나 그것은 또 다른 문제의 상단에 문제를 추가하는 느낌
Ярослав Рахматуллин

2

글쎄, 당신은 사용할 수 있습니다 default예를 들어, 선택됩니다 폐쇄 채널, 그것을 감지하는 지점을 다음 코드를 선택합니다 default, channel, channel는 먼저 차단되지 않습니다를 선택합니다.

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

채널을 닫는 것 외에도 채널을 nil로 설정할 수 있습니다. 그렇게하면 nil인지 확인할 수 있습니다.

놀이터의 예 : https://play.golang.org/p/v0f3d4DisCz

편집 : 이것은 실제로 다음 예제에서 보여 주듯이 나쁜 해결책입니다. 왜냐하면 함수에서 채널을 nil로 설정하면 깨지기 때문입니다 : https://play.golang.org/p/YVE2-LV9TOp


주소로 상기 채널을 통과 (또는 구조체의 주소로 전달)
ChuckCottrill

-1

문서에서 :

내장 기능 닫기로 채널을 닫을 수 있습니다. 수신 연산자의 다중 값 할당 양식은 채널이 닫히기 전에 수신 된 값이 전송되었는지 여부를보고합니다.

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
문제는 채널을 읽지 않고, 즉 쓰기 전에 닫힌 상태를 확인하는 방법이었습니다.
Peter

-5

채널에 요소가 있는지 먼저 확인하는 것이 더 쉽습니다. 그러면 채널이 살아 있는지 확인할 수 있습니다.

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

3
으로 더스틴 언급 안전하게이 작업을 수행 할 수있는 방법이 없습니다. if몸에 들어갈 때 쯤이면 len(ch)뭐든지 될 수 있습니다. (예를 들어 다른 코어의 고 루틴은 선택이 읽기를 시도하기 전에 채널에 값을 보냅니다).
Dave C

-7

이 채널을 들으면 항상 해당 채널이 닫 혔음을 알 수 있습니다.

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

그러나 한 채널을 두 번 닫을 수는 없습니다. 이것은 공포를 일으킬 것입니다.


5
나는 "읽지 않고", -1 질문을주의 깊게 읽지 않았다고 말했다.
Reck Hou

> PS : 제기 된 패닉을 회복하는 것은 제가 시도한 것이지만 패닉을 일으킨 고 루틴을 닫을 것입니다. 이 경우 컨트롤러가되므로 쓸모가 없습니다. func (chan z) {defer func () {// handle recover} close (z)}
jurka

하지만 컨트롤러를 예약해야하는데, 컨트롤러 close(z)대신 작업자가 호출합니다.
Reck Hou
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.