Как проверить, закрыт канал или нет, не читая его?

82

Это хороший пример режима рабочих и контроллеров в Go, написанный @Jimt в ответ на вопрос « Есть ли какой-нибудь элегантный способ приостановить и возобновить любую другую горутину в 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
Вот что я сделал: github.com/atedja/go-tunnel .
atedja 07

Ответы:

64

Хакерским способом это можно сделать для каналов, на которые делается попытка записи, восстановив возникшую панику. Но вы не можете проверить, закрыт ли канал чтения, не читая из него.

Либо ты будешь

  • в конце концов прочтите от него "истинное" значение ( v <- c)
  • прочитать значение "истина" и индикатор "не закрыто" ( v, ok <- c)
  • считать нулевое значение и индикатор «закрыто» ( v, ok <- c)
  • заблокирует в канале чтение навсегда ( v <- c)

Только последний технически с канала не читает, но толку от этого мало.

zzzz
источник
1
Я пытался восстановить возникшую панику, но это закроет горутину, которая вызвала панику. В таком случае это будет controllerбесполезно :)
Reck Hou
вы также можете написать hack, используя unsafe и отражать пакет, см. мой ответ
youssif 08
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
}
youssif
источник
1
go vetвозвращает "возможное неправильное использование unsafe.Pointer" в последней строке, return *(*uint32)(unsafe.Pointer(cptr)) > 0и cptr += unsafe.Sizeof(unsafe.Pointer(uintptr(0))) есть ли возможность сделать это без unsafe.Pointer в этих строках?
Effi Bar-She'an
2
Вам нужно выполнить всю арифметику с указателями в одном выражении, чтобы оставаться счастливым. Это решение представляет собой гонку данных и недействителен Go, вам также придется как минимум читать закрытые с помощью atomic.LoadUint32. В любом случае это довольно хрупкий хак, если чан изменится между версиями Go, это сломается.
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, который предлагает аналогичное решение) - оно не работает, если вы используете буферизованный канал и он содержит непрочитанные data
Angad
@Angad Это правда, что это не идеальное решение для обнаружения закрытого канала. Это идеальное решение для определения блокирования чтения канала. (т.е. если чтение канала будет заблокировано, значит, мы знаем, что он не закрыт; если чтение канала не заблокируется, мы знаем, что он может быть закрыт).
tombrown52
0

Вы можете обнулить свой канал в дополнение к его закрытию. Таким образом вы можете проверить, равно ли оно нулю.

пример на детской площадке: https://play.golang.org/p/v0f3d4DisCz

edit: на самом деле это плохое решение, как показано в следующем примере, потому что установка канала на ноль в функции приведет к его поломке: 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
Вопрос был в том, как проверить закрытое состояние без чтения канала, т.е. перед записью в него.
Питер
-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 за то, что не прочитал вопрос внимательно.
Рек Хоу
> PS: Я пытался восстановить возникшую панику, но это закроет горутину, которая вызвала панику. В этом случае это будет контроллер, поэтому бесполезно. Вы всегда можете пойти func (chan z) {defer func () {// обработать восстановление} close (z)}
jurka
Но я должен зарезервировать контроллер, и close(z)он будет вызываться рабочим вместо контроллера.
Рек Хоу