Як перевірити, чи канал закритий чи ні, не прочитавши його?


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

Відповіді:


64

По-хакі це можна зробити для каналів, на які намагається писати, відновлюючи підняту паніку. Але ви не можете перевірити, чи закритий канал читання, не читаючи з нього.

Або ти будеш

  • врешті-решт прочитати з нього "істинне" значення ( v <- c)
  • прочитати значення "true" та індикатор "not closed" ( v, ok <- c)
  • прочитати нульове значення та індикатор "закрито" ( v, ok <- c)
  • заблокує в каналі читання назавжди ( v <- c)

Тільки останній технічно не читає з каналу, але це мало користі.


1
Відновлення піднятої паніки - це те, що я намагався, але це закриє горутину, яка підняла паніку. У цьому випадку це буде controllerтак, що це ні до чого :)
Reck Hou

Ви також можете написати хак, використовуючи небезпечний пакет і відображати пакунок, див. мою відповідь
youssif

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)) > 0таcptr += unsafe.Sizeof(unsafe.Pointer(uintptr(0))) чи є можливість зробити це без unsafe.Pointer у цих рядках?
Effi Bar-She'an

2
Вам потрібно виконати всю арифметику покажчика в одному виразі, щоб ветеринар був щасливим. Це рішення є гонкою даних і не є дійсним Go, вам також доведеться щонайменше виконати зчитування closed з atomic.LoadUint32. У будь-якому випадку це досить крихкий хак, якщо hchan зміниться між версіями Go, це зламається.
Елофф

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 Це правда, це не ідеальне рішення для виявлення закритого каналу. Це ідеальне рішення для виявлення блокування каналу читання каналу. (тобто якщо читання каналу заблокується, ми знаємо, що він не закритий; якщо читання каналу не заблокується, ми знаємо, що він може бути закритий)
tombrown52

0

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

приклад на дитячому майданчику: https://play.golang.org/p/v0f3d4DisCz

редагувати: Це насправді погане рішення, як продемонстровано в наступному прикладі, оскільки встановлення каналу на нуль у функції порушить його: 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 за не уважно прочитане запитання.
Reck Hou

> PS: Відновлення піднятої паніки - це те, що я намагався, але це закриє горутину, яка викликала паніку. У цьому випадку це буде контролер, тому користі немає. Ви завжди можете перейти до func (chan z) {defer func () {// handle recovery} close (z)}
jurka

Але я повинен зарезервувати контролер, і close(z)його буде викликати працівник замість контролера.
Reck Hou
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.