这是工人和工作人员的一个很好的例子。控制器模式在Go由@Jimt编写,回答 “Is there some elegant way to pause & resume any other goroutine in 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()
关闭通道,如果控制器发现通道已关闭,它将跳过它并且什么都不做。但在这种情况下,我无法找到如何检查通道是否已经关闭。如果我尝试读取控制器中的通道,则可能会阻止控制器。所以我现在很困惑。
尽管如此,我认为Go团队在下一版Go中实现此功能非常有用。
答案 0 :(得分:61)
没有办法编写一个安全的应用程序,你需要知道一个频道是否打开而不与之交互。
做你想要做的事情的最好方法是有两个渠道 - 一个用于工作,另一个用于表示改变状态的愿望(以及完成状态改变,如果这很重要的话)。 / p>
频道很便宜。复杂的设计重载语义不是。
[也]
<-time.After(1e9)
是一种非常混乱且非显而易见的写作方式
time.Sleep(time.Second)
保持简单,每个人(包括你)都能理解它们。
答案 1 :(得分:43)
以一种黑客的方式,可以通过恢复引发的恐慌来尝试写入的频道。但是如果没有读取它就无法检查读取通道是否已关闭。
你要么
v <- c
)v, ok <- c
)v, ok <- c
)v <- c
)从技术上讲,只有最后一个没有从频道读取,但这没用。
答案 2 :(得分:5)
我知道这个答案太晚了,我写了这个解决方案,黑客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
}
https://gist.github.com/youssifsayed/ca0cfcf9dc87905d37a4fee7beb253c2
答案 3 :(得分:2)
我在使用多个并发 goroutine 时经常遇到这个问题。
这可能是也可能不是一个好的模式,但我为我的工作人员定义了一个结构体,并为工作人员状态定义了一个退出通道和字段:
type Worker struct {
data chan struct
quit chan bool
stopped bool
}
然后你可以让控制器为工人调用停止函数:
func (w *Worker) Stop() {
w.quit <- true
w.stopped = true
}
func (w *Worker) eventloop() {
for {
if w.Stopped {
return
}
select {
case d := <-w.data:
//DO something
if w.Stopped {
return
}
case <-w.quit:
return
}
}
}
这为您提供了一种很好的方法,可以让您的工作人员彻底停止,而不会出现任何挂起或产生错误的情况,这在容器中运行时尤其有用。
答案 4 :(得分:0)
也许我错过了一些东西,但似乎处理这个的简单而正确的方法是发送&#34;停止&#34;到通道(终止go-routine),关闭通道并将其设置为nil。
如果您认为需要在不阅读的情况下检查已关闭的频道,那么您的设计就会出现问题。 (请注意,代码还存在其他问题,例如&#34;忙碌的循环&#34;暂停的工作人员。)
答案 5 :(得分:0)
来自文档:
可以在内置功能关闭的情况下关闭通道。接收运算符的多值赋值形式报告在通道关闭之前是否发送了接收值。
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
}
}
答案 6 :(得分:0)
嗯,您可以使用default
分支来检测它,因为将选择一个封闭的频道,例如:以下代码将选择default
,channel
,{{1} },第一个选择未被阻止。
channel
答案 7 :(得分:0)
除了关闭频道外,您还可以将其设置为nil。这样,您可以检查它是否为零。
在操场上的例子: https://play.golang.org/p/v0f3d4DisCz
编辑: 如下面的示例所示,这实际上是一个糟糕的解决方案, 因为在函数中将通道设置为nil会破坏它: https://play.golang.org/p/YVE2-LV9TOp
答案 8 :(得分:0)
ch1 := make(chan int)
ch2 := make(chan int)
go func(){
for i:=0; i<10; i++{
ch1 <- i
}
close(ch1)
}()
go func(){
for i:=10; i<15; i++{
ch2 <- i
}
close(ch2)
}()
ok1, ok2 := false, false
v := 0
for{
ok1, ok2 = true, true
select{
case v,ok1 = <-ch1:
if ok1 {fmt.Println(v)}
default:
}
select{
case v,ok2 = <-ch2:
if ok2 {fmt.Println(v)}
default:
}
if !ok1 && !ok2{return}
}
}
答案 9 :(得分:-4)
如果频道有元素,则更容易检查,这将确保频道有效。
func isChanClosed(ch chan interface{}) bool {
if len(ch) == 0 {
select {
case _, ok := <-ch:
return !ok
}
}
return false
}
答案 10 :(得分:-5)
如果你听这个频道,你总能发现该频道已经关闭。
case state, opened := <-ws:
if !opened {
// channel was closed
// return or made some final work
}
switch state {
case Stopped:
但请记住,你不能两次关闭一个频道。这会引起恐慌。