首页 > 其他分享 >如何优雅的关闭channel?

如何优雅的关闭channel?

时间:2022-12-02 14:47:45浏览次数:62  
标签:sender dataCh 关闭 receiver 发送数据 优雅 channel

一、channel使用存在的不方便地方

1、在不改变channel自身状态的情况下,无法获知一个channnel是否关闭。

2、关闭一个已经关闭的channel,会导致panic。因此,如果关闭channel的一方在不知道channel是否关闭状态时就去贸然关闭channel时件很危险的事。

3、向一个已经关闭的channel发送数据会导致panic。因此,如果向channel发送数据的一方不知道channel是否处于关闭状态就贸然向channel发送数据是很危险的事情。

一个比较粗糙的检查channel是否关闭的函数:

func IsClosed(ch <-chan interface{}) bool {
    select {
    case <-ch:
        return true
    default:
    }
    return false
}

func main() {
    c := make(chan interface{})
    fmt.Println(IsClosed(c))  // false
    close(c)
    fmt.Println(IsClosed(c)) // true
}

上面的代码其实存在很多问题。

首先,IsClosed函数是一个有副作用的函数。每调用一次,都会读出channel里面的一个元素,改变了channel的状态,这不是一个好函数,干活就干活,还顺手牵羊?

其次,IsClosed函数返回的结果仅代表调用时候的那个瞬间,并不能保证调用之后不会有其他goroutine对这个channel进行了一些操作,改变了这个channel的状态。比如:IsClosed函数返回true,但这时有另外一个goroutine关闭了这个channel,这时候我们就会拿着这个过时的"channel未关闭"信息,向其发送数据,就会导致panic的发生。

当然,一个channel不会被重复关闭两次,如果IsClosed函数返回的结果是true,说明channel是真的关闭了。

有一句广泛流传的关闭channel的原则:

不要从一个 receiver 侧关闭 channel,也不要在有多个 sender 时,关闭 channel。

向channel发送元素的就是sender,因此sender可以决定何时不发送数据,并且关闭channel。但是如果有多个sender,某个sender同样没法确定其他sender的情况。这时候也不能贸然的关闭channel。

有两个不优雅关闭channel的方法:

1、使用defer-recover机制,放心大胆地关闭channel或者向channel发送数据,即使发生了panic,有defer-recover在兜底。

2、使用sync.Once来保证只关闭一次。

二、如何优雅关闭channel

根据sender和receiver的个数,可以分为以下几种情况:

1、一个sender,一个receiver

2、一个sender,M个receiver

3、N个sender,一个receiver

4、N个sender,M个receiver

对于1,2这两种情况,只有一个sender,直接从sender段关闭就好。

第3种情形下,优雅关闭channel的方法是:唯一的receiver发出一个关闭channel的信号,senders监听到关闭信号后,停止发送数据。

func main() {
    rand.Seed(time.Now().UnixNano())

    const Max = 10000
    const NumSenders = 1000

    dataChan := make(chan int, 100)
    stopChan := make(chan struct{})

    // senders
    for i := 0; i < NumSenders; i++ {
        go func() {
            for {
                select {
                // 监听关闭channel的信号,退出
                case <-stopChan:
                    return
                // 给dataChan中发送数据
                case dataChan <- rand.Intn(Max):
                }
            }
        }()
    }

    // receiver
    go func() {
        // 从dataChan中遍历数据
        for value := range dataChan {
            // 如果value的值为9999,通知sender停止发送数据
            if value == Max-1 {
                fmt.Println("send stop signal to senders.")
                close(stopChan)
                return
            }
            fmt.Println(value)
        }
    }()

    select {
    // 阻塞一小时退出
    case <-time.After(time.Hour):
    }
}

这里的stopCh就是信号channel,它本身只有一个sender,因此可以直接关闭它。senders收到了关闭信号后,select分支case<-stopCh被选中,退出函数,不再发送数据。

需要说明的是,上面的代码并没有明确关闭dataCh。在Go语言中,对于一个channel,如果最终没有任何goroutine引用它,不管channel有没有被关闭,最终都会被GC回收。所以在这种情形下,所谓的优雅地关闭channel就是不关闭channel,让GC代劳。

最后一种情况,优雅关闭channel的方法是:any one of them says “let’s end the game” by notifying a moderator to close an additional signal channel。

和第三种情况不同,这里有M个receiver,如果,采用第3种解决方案,由receiver直接关闭stopCh的话,就会重复关闭一个channel,导致panic。因此需要增加一个中间人,M个receiver都向它发送关闭dataCh的"请求",中间人收到第一个请求后,就会直接下达关闭dataCh的指令(通过关闭stopCh,这时就不会发生重复关闭的情况,因为stopCh的发送方只有中间人一个)。另外这里N个sender也可以向中间人发送关闭dataCh的请求。

func main() {
    rand.Seed(time.Now().UnixNano())

    const Max = 10000
    const NumReceivers = 10
    const NumSenders = 1000

    dataCh := make(chan int, 100)
    stopCh := make(chan struct{})

    // 这里必须是一个有缓冲的channel
    toStop := make(chan string, 1)

    var stoppedBy string

    // 中间人
    go func() {
        // 接收到了关闭channel的请求
        stoppedBy = <-toStop
        // 发送关闭dataCh的信号
        close(stopCh)
    }()

    // senders
    for i := 0; i < NumSenders; i++ {
        go func(id string) {
            for {
                value := rand.Intn(Max)
                // 如果value=0,则给中间人发送关闭channel的信号
                if value == 0 {
                    select {
                    case toStop <- "sender#" + id:
                    // 此处是为了防止toStop这个channel阻塞
                    default:
                    }
                    return
                }

                select {
                // 监听关闭channel的信号
                case <-stopCh:
                    return
                case dataCh <- value:
                }
            }
        }(strconv.Itoa(i))
    }

    // receivers
    for i := 0; i < NumReceivers; i++ {
        go func(id string) {
            for {
                select {
                // 监听关闭信号,退出,停止接收数据
                case <-stopCh:
                    return
                // 接收数据
                case value := <-dataCh:
                    // 如果接收到了9999,则给中间人发送关闭channel的信号
                    if value == Max-1 {
                        select {
                        case toStop <- "receiver#" + id:
                        default:
                        }
                        return
                    }
                    fmt.Println(value)
                }
            }
        }(strconv.Itoa(i))
    }

    select {
    case <-time.After(time.Hour):
    }

}

代码里toStop就是中间人的角色,使用它来接收senders和receivers发送过来的关闭dataCh请求。

这里将toStop声明成了一个缓冲型的channel。假设toStop声明的是一个非缓冲型的channel,那么第一个发送的关闭dataCh请求可能会丢失。因为无论是sender还是receiver都是通过select语句来发送请求,如果中间人所在的goroutine没有准备好,那么select语句就不会被选中,直接走default选项了,什么都不做。这样,第一个关闭dataCh的请求就会丢失。

如果把toStop的容量声明成Num(senders) + Num(receivers),那么发送dataCh请求的部分可以改写成更简洁的形式:

func main() {
	rand.Seed(time.Now().UnixNano())

	const Max = 10000
	const NumReceivers = 10
	const NumSenders = 1000

	dataCh := make(chan int, 100)
	stopCh := make(chan struct{})

	// 这里必须是一个有缓冲的channel
	toStop := make(chan string, NumSenders+NumReceivers)

	var stoppedBy string

	// 中间人
	go func() {
		// 接收到了关闭channel的请求
		stoppedBy = <-toStop
		// 发送关闭dataCh的信号
		close(stopCh)
	}()

	// senders
	for i := 0; i < NumSenders; i++ {
		go func(id string) {
			for {
				value := rand.Intn(Max)
				// 如果value=0,则给中间人发送关闭channel的信号
				if value == 0 {
					toStop <- "sender#" + id
					return
				}

				select {
				// 监听关闭channel的信号
				case <-stopCh:
					return
				case dataCh <- value:
				}
			}
		}(strconv.Itoa(i))
	}

	// receivers
	for i := 0; i < NumReceivers; i++ {
		go func(id string) {
			for {
				select {
				// 监听关闭信号,退出,停止接收数据
				case <-stopCh:
					return
				// 接收数据
				case value := <-dataCh:
					// 如果接收到了9999,则给中间人发送关闭channel的信号
					if value == Max-1 {
						toStop <- "receiver#" + id
						return
					}
					fmt.Println(value)
				}
			}
		}(strconv.Itoa(i))
	}

	select {
	case <-time.After(time.Hour):
	}
}

直接向toStop发送请求,因为toStop容量足够大,所以不用担心阻塞,自然也就不用select语句再加一个default case来避免阻塞。

可以看到,这里同样没有真正关闭dataCh,原样通第3种情况。

参考链接:

https://golang.design/go-questions/channel/graceful-close/

标签:sender,dataCh,关闭,receiver,发送数据,优雅,channel
From: https://www.cnblogs.com/huiyichanmian/p/16944427.html

相关文章

  • 每日一抄 Go语言关闭通道后继续使用
    packagemainimport"fmt"/*通道是一个引用对象,和map类似。map在没有任何外部引用时,Go语言程序在运行时(runtime)会自动对内存进行垃圾回收(GarbageCollection,GC)......
  • DrawerLayout去除阴影,阴影点击不关闭抽屉,屏蔽触发相关view,抽屉侧滑关闭响应
    1,点击阴影位置抽屉不关闭2,只有点击侧边栏里面的按钮才会收起抽屉3,禁止手势滑动划出和关闭抽屉功能/**mainActivty的其他控件在抽屉打开的情况下不可以点击*/drawer......
  • 面试官本拿求素数搞我,但被我优雅的“回击“了(素数筛)
    前言现在的面试官,是无数开发者的梦魇,能够吊打面试官的属实不多,因为大部分面试官真的有那么那几下子。但在面试中,我们这些小生存者不能全盘否定只能单点突破—从某个问题上......
  • tomcat_关闭以及部署项目方式
    tomcat_关闭1.正常关闭 bin/shutdown.bat  ctrl+c也可以正常关闭2.强制关闭点击启动窗口x  tomcat_部署项目方式配置:部署项目的方......
  • channel 死锁
    死锁:-单个协程永久阻塞-两个或两个以上的协程执行过程中,由于竞争资源或由于彼此通信而造成的一种阻塞的现象。channel死锁场景:-非缓存channel只写不读-......
  • 微信公众号页面问题-关闭按钮
    场景:企业号应用A,首页加个按钮,点击直接关闭此页面,回到进入前的页面解决:调用微信 WeixinJSBridge.call('closeWindow');代码如下:1handleReturn(){2......
  • .net core/5/6/7中WPF如何优雅的开始开发
    .netcore/5/6/7中WPF如何优雅的开始开发 WPF是微软的.net平台中的一个桌面客户端应用程序框架,经常用于企业开发windows桌面客户端,广泛应用于中小企业快速开发一款......
  • pythonfloat优雅的四舍五入
    开发中经常会有float四舍五入转int的需求,先看看浮点数直接转int的情形:无论如何float直接转int都不会四舍五入,而是直接抹去小数点。这个需求很简单,实现也很简单,看过网友的......
  • .net如何优雅的使用EFCore
    .net如何优雅的使用EFCore EFCore是微软官方的一款ORM框架,主要是用于实体和数据库对象之间的操作。功能非常强大,在老版本的时候叫做EF,后来.netcore问世,EFCore也随之......
  • 关闭 ORACLE trace 日志功能
    关闭ORACLEtrace日志功能altersystemsettrace_enabled=falseselect*fromv$parameterwhereNAMElike'%trace%'; 关闭ORACLEtrace日志功能altersystem......