lxzan / gws

simple, fast, reliable websocket server & client, supports running over tcp/kcp/unix domain socket. keywords: ws, proxy, chat, go, golang...
https://pkg.go.dev/github.com/lxzan/gws
Apache License 2.0
1.36k stars 87 forks source link

non-blocking write性能、消耗、时序 #3

Closed lesismal closed 1 year ago

lesismal commented 1 year ago

简单看了下,每个conn上自带一个size为8的workerQueue,但是这样似乎有几个问题:

  1. doWrite中是有锁的,所以即使多个协程同时写,也是在锁这里排队,而且还加剧了锁的竞争自旋、可能cpu消耗更高了,所以这里的多协程异步写目测并不能提高性能
  2. 高并发时协程数量、资源消耗更多
  3. 这个workerQueue并发执行时,好像并不能保证发送队列中的数据按照WriteAsync时入队列的顺序。 而单个conn的消息,很多时候是需要时序保证的。比如流媒体推流需要广播不能允许被单个conn卡其他conn,所以就得WriteAsync,但是推送的数据顺序错乱了音视频就乱码了丢帧了。比如游戏一些玩家操作顺序广播出去,本来是ABC,用户收到变成了CAB。。。

兄弟我建议这块就按常规简单的方式来,单个协程+limited size chan+select default/timeout循环发送就可以了,继续这样下去好像越走越远了。。。

lesismal commented 1 year ago

基于标准库阻塞io的websocket,melody虽然也有些浪费的地方,但它在gorilla基础上的封装方式是做得比较不错的,可以参考下: https://github.com/olahol/melody

lxzan commented 1 year ago

async read/write都是可选的,对时序有要求可以采用同步模式. 它们都通过了autobahn的测试,相比同步模式多了一些not strict.

lxzan commented 1 year ago

事实上,我们跑benchmark才会出现单连接高并发的情况,实际业务锁竞争不会特别激烈.

lxzan commented 1 year ago

基于标准库阻塞io的websocket,melody虽然也有些浪费的地方,但它在gorilla基础上的封装方式是做得比较不错的,可以参考下: https://github.com/olahol/melody

下午去看看借鉴下

lesismal commented 1 year ago

async read/write都是可选的,对时序有要求可以采用同步模式. 它们都通过了autobahn的测试,相比同步模式多了一些not strict. 事实上,我们跑benchmark才会出现单连接高并发的情况,实际业务锁竞争不会特别激烈.

这两条的前提都是基于运气好。业务量再小,只要时间足够,也会遇到问题——墨菲定律了解一下。

如果基础设施的代码基于运气,谁敢用啊兄弟,:joy:

lesismal commented 1 year ago

时序的问题,如果你想节省协程也可以做到,消息入队列如果是队首则启动go后for循环去发送队列里的数据,这样也能做到没有待发送的数据时不用占用协程,类似我这里: https://github.com/lesismal/nbio/blob/master/conn.go#L103

你现在的workerqueue size设置为1、do的时候改造下判断是否队首应该就可以。目前没有判断队首直接改size为1应该是不行的,并发AddJob会乱

lxzan commented 1 year ago

async read/write都是可选的,对时序有要求可以采用同步模式. 它们都通过了autobahn的测试,相比同步模式多了一些not strict. 事实上,我们跑benchmark才会出现单连接高并发的情况,实际业务锁竞争不会特别激烈.

这两条的前提都是基于运气好。业务量再小,只要时间足够,也会遇到问题——墨菲定律了解一下。

如果基础设施的代码基于运气,谁敢用啊兄弟,😂

并不是运气. 对于线上环境, 单连接内出现高并发, 十有八九是DDOS. 而且, 我这套基于任务队列的IO模型是有改进空间的. channel并发写之所以高效, 是因为并行写入内存(加锁解锁时间很短), 串行写入net.Conn.

lxzan commented 1 year ago

时序的问题,如果你想节省协程也可以做到,消息入队列如果是队首则启动go后for循环去发送队列里的数据,这样也能做到没有待发送的数据时不用占用协程,类似我这里: https://github.com/lesismal/nbio/blob/master/conn.go#L103

你现在的workerqueue size设置为1、do的时候改造下判断是否队首应该就可以。目前没有判断队首直接改size为1应该是不行的,并发AddJob会乱

被你发现了, 我就是不想增加常驻协程.

lesismal commented 1 year ago

1.14后,go的调度是抢占式的了,即使纯cpu消耗的代码也可能被中途打断的。所以这并不是你的并发量有多大的问题,只要发送队列大于等于2,基于墨菲定律,就肯定会发生。 这还不是运气式啥!?清醒一点

lxzan commented 1 year ago

1.14后,go的调度是抢占式的了,即使纯cpu消耗的代码也可能被中途打断的。所以这并不是你的并发量有多大的问题,只要发送队列大于等于2,基于墨菲定律,就肯定会发生。 这还不是运气式啥!?清醒一点

就墨菲定律来说, 你无法100%保证所有用户的QoS, 就像机械效率无法达到100%.

lesismal commented 1 year ago

被你发现了, 我就是不想增加常驻协程.

nbio里使用的默认协程池没任务的时候就是退出的不占用资源的,但是基于自然均衡的需求,留了一个带size chan当队列的常驻协程,一个协程成本无所谓。

lesismal commented 1 year ago

就墨菲定律来说, 你无法100%保证所有用户的QoS, 就像机械效率无法达到100%.

计算机科学的分层思想很有道理。就可靠性来讲,我觉得:

  1. 每一层应该尽量保障自己这一层的可用性;
  2. 从硬件到软件协议栈,每一层都有校验和、可靠协议校验失败丢弃重发之类的机制,都是尽量确保自己这一层的稳定性可用性;
  3. 如果每一层都觉得其他层无法100%所以自己也基于运气,那整个系统的可靠性就更低了

而且这个问题,完全可以做到更好的可靠性代码+小于等于当前方案的消耗+更低的理论消耗上限

lesismal commented 1 year ago

这个workerqueue设计得挺好,但更适合用于通用协程池的场景。单个conn这种有时序要求的,再特化一下会更好

lesismal commented 1 year ago

字节的那个workerpool本质也是类似的,细节差异罢了

lxzan commented 1 year ago

这个workerqueue设计得挺好,但更适合用于通用协程池的场景。单个conn这种有时序要求的,再特化一下会更好

再加一个有锁队列(RWMutex),可以达到channel的效果

lxzan commented 1 year ago

这个workerqueue设计得挺好,但更适合用于通用协程池的场景。单个conn这种有时序要求的,再特化一下会更好

从我的concurrency库复制过来的. 看过字节和ants两个库,都没看明白原理😂

lesismal commented 1 year ago

字节的主要是自己的简单链表+pool略有优化,用的for循环,你的是数组+递归,本质上你俩这个是相同的。

ants写的太复杂,我没深入读过它代码,简单扫了下应该是用的条件变量,类似c/cpp那套。因为写的太复杂,而且我看issue里有人遇到一些莫名其妙的bug,而且较高版本go里我benchmark了下它更慢,所以就更没打算看它源码了。

我那个协程池为了减少一个协程池只有单个数组或者链表时锁操作的竞争浪费,保留了一个常驻协程用chan做队列,当前并发度没有达到协程数量限制时则只要一个原子操作,比无竞争锁需要加锁解锁省掉一些原子操作,比竞争时try lock部分自旋节省更多,以一个常驻协程代价换更均衡的。对于单个conn做了上面说的那个时序保证、但这是conn自己的部分、并不是协程池本身的功能。我这里单独实现了一个有序化的,相当于把conn上那个拆出来了,可以配合任何协程池使用: https://github.com/lesismal/serial

lesismal commented 1 year ago

再加一个有锁队列(RWMutex),可以达到channel的效果

应该是不需要用RWMutex的,这种队列+线程/协程池,通常是需要push/pop的原子性保障,读锁并发时没法保证push/pop的原子性,所以可能会带来bug。 如果只使用Lock/Unlock,则Mutex性能好于RWMutex,看下源码就懂了。lockSlow那个内联的注释让人心情非常愉悦: https://github.com/golang/go/blob/master/src/sync/mutex.go#L89

lxzan commented 1 year ago

字节的主要是自己的简单链表+pool略有优化,用的for循环,你的是数组+递归,本质上你俩这个是相同的。

ants写的太复杂,我没深入读过它代码,简单扫了下应该是用的条件变量,类似c/cpp那套。因为写的太复杂,而且我看issue里有人遇到一些莫名其妙的bug,而且较高版本go里我benchmark了下它更慢,所以就更没打算看它源码了。

我那个协程池为了减少一个协程池只有单个数组或者链表时锁操作的竞争浪费,保留了一个常驻协程用chan做队列,当前并发度没有达到协程数量限制时则只要一个原子操作,比无竞争锁需要加锁解锁省掉一些原子操作,比竞争时try lock部分自旋节省更多,以一个常驻协程代价换更均衡的。对于单个conn做了上面说的那个时序保证、但这是conn自己的部分、并不是协程池本身的功能。我这里单独实现了一个有序化的,相当于把conn上那个拆出来了,可以配合任何协程池使用: https://github.com/lesismal/serial

一个常驻协程无所谓, 连接上面的就可观了

lesismal commented 1 year ago

回到问题本身,不管是否优化协程占用,应该严格保证发送顺序、而不是基于运气默认认为不会出现不一致。

lesismal commented 1 year ago

一个常驻协程无所谓, 连接上面的就可观了

我说的一个常驻协程是一个协程池只有一个常驻,一个nbio Engine上的所有conn共用同一个或几个这种协程池,并不是像gws这种一个conn上一个workerpool,所以不存在协程数量可观的问题。 一个conn上只是一个数组作为执行队列、不绑定协程池,而是这个conn解析出一个完整message时,把任务加入到这个conn的执行队列: https://github.com/lesismal/nbio/blob/master/conn.go#L103 如果这个message的任务就是队首,则用协程池去异步执行并且循环取任务直到执行完所有,这里的c.p.g.Execute是协程池的pool.Go: https://github.com/lesismal/nbio/blob/master/conn.go#L115 这样就能保证并发入队列的时候单个conn多个任务也只会启动并临时占用一个协程,执行完当前任务列表就退出,并且保证了顺序加入队列的任务的有序执行

lesismal commented 1 year ago

对于传统的c/cpp那些框架,我这个时序的方案也是适用的,这样至少可以让多逻辑线程被均衡利用起来。不只是单个conn时序保证,还可以根据模块,每个模块继承/集成一个这种队列+线程池,就能做到逻辑多线程+各种时序保证了

lxzan commented 1 year ago

回到问题本身,不管是否优化协程占用,应该严格保证发送顺序、而不是基于运气默认认为不会出现不一致。

时序问题在v1.3.1已经解决了. 肝了一天, 累死了, 异步IO真是让人头大.

lesismal commented 1 year ago

时序问题在v1.3.1已经解决了. 肝了一天, 累死了, 异步IO真是让人头大.

这。。。我简单review了下,应该是没解决的。我展开了说,你看一下是不是这个道理:

1.3.1是每次WriteAsync都先入队列,然后AddJob,每个job执行时是doWriteAsync,但是doWriteAsync是每次先取出所有再循环发送。 比如连续WriteAsync M 个消息,入队列与实际执行写的job并不是一一对应的、不是入队一个写一个的。 而是很可能M个消息分散成了小于M份比如M/2份,被M/2个job分别拿到了去发送,而你一共AddJob了M次,所以还会有M/2个job被异步执行时实际是空跑、浪费了。而且,由于可能同时存在M/2个job在执行,仍然没有保障时序。

今天头大了可以先停一停,休息下散散步喝喝茶,等思路清晰了再改,否则一下子卡住可能越改越乱,我以前也是,好几次肝到凌晨5点甚至早上8点,整个人状态都不好了。。。

lesismal commented 1 year ago

这里的85-88行取出所有后解锁了,下面循环发送的过程中,别的地方可能又WriteAsync并触发了新的job异步执行: https://github.com/lxzan/gws/blob/master/writer.go#L85

这里取所有如果只加锁、等所有发送完再解锁,那其他地方WriteAsync又可能阻塞,所以这里取所有message不管加不加锁都不是正解。我前面说不要使用RWMutex、保证不了原子性也是类似的原因。 要不你先看下我那个conn.Execute或者serial再改:joy:

lxzan commented 1 year ago

时序问题在v1.3.1已经解决了. 肝了一天, 累死了, 异步IO真是让人头大.

这。。。我简单review了下,应该是没解决的。我展开了说,你看一下是不是这个道理:

1.3.1是每次WriteAsync都先入队列,然后AddJob,每个job执行时是doWriteAsync,但是doWriteAsync是每次先取出所有再循环发送。 比如连续WriteAsync M 个消息,入队列与实际执行写的job并不是一一对应的、不是入队一个写一个的。 而是很可能M个消息分散成了小于M份比如M/2份,被M/2个job分别拿到了去发送,而你一共AddJob了M次,所以还会有M/2个job被异步执行时实际是空跑、浪费了。而且,由于可能同时存在M/2个job在执行,仍然没有保障时序。

今天头大了可以先停一停,休息下散散步喝喝茶,等思路清晰了再改,否则一下子卡住可能越改越乱,我以前也是,好几次肝到凌晨5点甚至早上8点,整个人状态都不好了。。。

写入顺序就是入队顺序, 已经跑通了Autobahn测试, 没有再出现额外的几个Non-Strict了. 暂未发现异常, 跑Benchmark性能更强了 😂.

lesismal commented 1 year ago

还有一点,nbio的阻塞io的websocket conn的写,是按配置项决定是否开启单独协程负责写的。用户如果不需要广播,不开启就能省点协程。但是如果需要广播、用户配置了异步写,我是用单独的常驻协程处理异步写的,没有做成这种动态协程,因为动态协程性能要差一些,跨协程变量逃逸、调度亲和性都要差。 而且既然用户都使用了阻塞或者混合模式来处理conn而且配置了异步写,应该也不差这点协程数、而是追求性能多一些,并且如果是用混合模式,肯定也是配置了阻塞io的连接数量的,这个数量不会太大,多出这个数量的协程也没压力,其他超过阈值的部分的conn交给poller部分去节省,完全能cover住

lesismal commented 1 year ago

写入顺序就是入队顺序, 已经跑通了Autobahn测试, 没有再出现额外的几个Non-Strict了. 暂未发现异常, 跑Benchmark性能更强了

一些问题压测是测不出来的,autobahn也跑通也不代表就没有问题啊。。。 你先看我分析的代码逻辑,是不是确实存在这种可能性

lxzan commented 1 year ago

这里的85-88行取出所有后解锁了,下面循环发送的过程中,别的地方可能又WriteAsync并触发了新的job异步执行: https://github.com/lxzan/gws/blob/master/writer.go#L85

这里取所有如果只加锁、等所有发送完再解锁,那其他地方WriteAsync又可能阻塞,所以这里取所有message不管加不加锁都不是正解。我前面说不要使用RWMutex、保证不了原子性也是类似的原因。 要不你先看下我那个conn.Execute或者serial再改😂

改了再看吧, 今天改休息了

lesismal commented 1 year ago

嗯,先休息下吧,状态好了再看可能就清晰了。并发这块,没那么简单的,需要更多想象力去推演代码实际执行的情况

lxzan commented 1 year ago

写入顺序就是入队顺序, 已经跑通了Autobahn测试, 没有再出现额外的几个Non-Strict了. 暂未发现异常, 跑Benchmark性能更强了

一些问题压测是测不出来的,autobahn也跑通也不代表就没有问题啊。。。 你先看我分析的代码逻辑,是不是确实存在这种可能性

可以写单元测试测一下, 调用WriterAsync写入整数序列, Wait后检查IsSorted

lesismal commented 1 year ago

你要不说,我还没细看Wait(),这个Wait()。。。 我感觉你越走越远了。。。:joy:

lesismal commented 1 year ago

因为调度的先后是不可控的,所以如果代码本身存在时序错乱的可能性,那么简单测试正常也并不代表解决了问题,尤其是本地测试环境过于稳定、无法跑出临界情况很正常。即使概率再低,生产环境也是会出现的。

lxzan commented 1 year ago

因为调度的先后是不可控的,所以如果代码本身存在时序错乱的可能性,那么简单测试正常也并不代表解决了问题,尤其是本地测试环境过于稳定、无法跑出临界情况很正常。即使概率再低,生产环境也是会出现的。

有单元测试肯定好过没测试, 可以模拟各种情况,加随机延迟.

lesismal commented 1 year ago

有些临界情况,本地测试是很难触发的,甚至没办法模拟出来,但生产环境几乎肯定会出现。 所以,单元测试是相当于必需品,得有,但它解决不了一切,还是需要更多工作。

lesismal commented 1 year ago

我之前给melody的pr,就是读代码时肉眼看到的,一看就有问题,但它的测试以及那么多人用于生产那么久都没有人碰到或或者指出来。那个问题不复杂,你这里的并发时序性比那个还要复杂些,需要想象力去推演,因为很难通过测试模拟触发,因为本地测试环境比较稳定,即使是压测,多数时候也是按最顺畅的分支去执行的,所以触发不了,但问题可能确实存在

lxzan commented 1 year ago

我之前给melody的pr,就是读代码时肉眼看到的,一看就有问题,但它的测试以及那么多人用于生产那么久都没有人碰到或或者指出来。那个问题不复杂,你这里的并发时序性比那个还要复杂些,需要想象力去推演,因为很难通过测试模拟触发,因为本地测试环境比较稳定,即使是压测,多数时候也是按最顺畅的分支去执行的,所以触发不了,但问题可能确实存在

我用net.Pipe写单元测试时碰到了一个很奇怪的问题,conn.Write之后直接没有往下执行了,导致后面的OnError没有被解发

lesismal commented 1 year ago

我用net.Pipe写单元测试时碰到了一个很奇怪的问题,conn.Write之后直接没有往下执行了,导致后面的OnError没有被解发

可以把完整示例发一份,我有空了也看一下

lxzan commented 1 year ago

简单看了下,每个conn上自带一个size为8的workerQueue,但是这样似乎有几个问题:

  1. doWrite中是有锁的,所以即使多个协程同时写,也是在锁这里排队,而且还加剧了锁的竞争自旋、可能cpu消耗更高了,所以这里的多协程异步写目测并不能提高性能
  2. 高并发时协程数量、资源消耗更多
  3. 这个workerQueue并发执行时,好像并不能保证发送队列中的数据按照WriteAsync时入队列的顺序。 而单个conn的消息,很多时候是需要时序保证的。比如流媒体推流需要广播不能允许被单个conn卡其他conn,所以就得WriteAsync,但是推送的数据顺序错乱了音视频就乱码了丢帧了。比如游戏一些玩家操作顺序广播出去,本来是ABC,用户收到变成了CAB。。。

兄弟我建议这块就按常规简单的方式来,单个协程+limited size chan+select default/timeout循环发送就可以了,继续这样下去好像越走越远了。。。

这样子吗?

image
lxzan commented 1 year ago

我用net.Pipe写单元测试时碰到了一个很奇怪的问题,conn.Write之后直接没有往下执行了,导致后面的OnError没有被解发

可以把完整示例发一份,我有空了也看一下

跑一下TestRead, 必现; 有点担心net.TcpConn和tls.Conn是不是也有这个问题; 得到妥善解决的话就可以去掉Wait了, https://github.com/lxzan/gws/blob/debug/reader_test.go

lxzan commented 1 year ago

我用net.Pipe写单元测试时碰到了一个很奇怪的问题,conn.Write之后直接没有往下执行了,导致后面的OnError没有被解发

可以把完整示例发一份,我有空了也看一下

可有可能只有net.Pipe()创建的连接有这个问题, 在一个协程内读写会造成死锁

lxzan commented 1 year ago

我用net.Pipe写单元测试时碰到了一个很奇怪的问题,conn.Write之后直接没有往下执行了,导致后面的OnError没有被解发

可以把完整示例发一份,我有空了也看一下

应该是pipe本身的问题, 某一端在read函数里面调用write会造成死锁

net.Conn和tls.Conn没这毛病

lesismal commented 1 year ago

简单看了下,每个conn上自带一个size为8的workerQueue,但是这样似乎有几个问题:

  1. doWrite中是有锁的,所以即使多个协程同时写,也是在锁这里排队,而且还加剧了锁的竞争自旋、可能cpu消耗更高了,所以这里的多协程异步写目测并不能提高性能
  2. 高并发时协程数量、资源消耗更多
  3. 这个workerQueue并发执行时,好像并不能保证发送队列中的数据按照WriteAsync时入队列的顺序。 而单个conn的消息,很多时候是需要时序保证的。比如流媒体推流需要广播不能允许被单个conn卡其他conn,所以就得WriteAsync,但是推送的数据顺序错乱了音视频就乱码了丢帧了。比如游戏一些玩家操作顺序广播出去,本来是ABC,用户收到变成了CAB。。。

兄弟我建议这块就按常规简单的方式来,单个协程+limited size chan+select default/timeout循环发送就可以了,继续这样下去好像越走越远了。。。

这样子吗? image

不是完整代码,没法细分析。但单从这个锁+for循环chan就感觉不对,锁粒度太大了容易卡

lesismal commented 1 year ago

net.Pipe好像是写的一方要等读的一方读取对应字节数之后才会返回、否则一直阻塞,测试代码我好没细看,你看下是不是这个原因

lesismal commented 1 year ago

我用net.Pipe写单元测试时碰到了一个很奇怪的问题,conn.Write之后直接没有往下执行了,导致后面的OnError没有被解发

可以把完整示例发一份,我有空了也看一下

可有可能只有net.Pipe()创建的连接有这个问题, 在一个协程内读写会造成死锁

如果是同一个协程内先写后读,那肯定是阻塞了。没读取完成或者close之前写会一直阻塞不返回。你看下源码: https://github.com/golang/go/blob/master/src/net/pipe.go#L195 这里就是先写入buf到chan,从另一个chan阻塞读取对方传入的已读字节数,如果对方没读完,循环继续写,同一个协程内先写后读肯定是要卡这里的

养成习惯,多看标准库源码

lxzan commented 1 year ago

net.Pipe好像是写的一方要等读的一方读取对应字节数之后才会返回、否则一直阻塞,测试代码我好没细看,你看下是不是这个原因

把pipe.go复制过来魔改了下,解决单元测试死锁了

lxzan commented 1 year ago

我用net.Pipe写单元测试时碰到了一个很奇怪的问题,conn.Write之后直接没有往下执行了,导致后面的OnError没有被解发

可以把完整示例发一份,我有空了也看一下

可有可能只有net.Pipe()创建的连接有这个问题, 在一个协程内读写会造成死锁

如果是同一个协程内先写后读,那肯定是阻塞了。没读取完成或者close之前写会一直阻塞不返回。你看下源码: https://github.com/golang/go/blob/master/src/net/pipe.go#L195 这里就是先写入buf到chan,从另一个chan阻塞读取对方传入的已读字节数,如果对方没读完,循环继续写,同一个协程内先写后读肯定是要卡这里的

养成习惯,多看标准库源码

断点追踪到pipe.go里面了

lesismal commented 1 year ago

可以试试去给官方pr下解决pipe的这个问题

lesismal commented 1 year ago

review了下新的release,异步写这块应该还是不对:

首先,writePublic执行实际写入,是有可能阻塞的,比如tcp窗口拥塞

通常是入chan这块怕阻塞需要select default: https://github.com/lxzan/gws/blob/master/writer.go#L87 writePublic阻塞后,正在异步写的协程阻塞了,其他地方调用WriteAsync多次导致chan的size默认8也满了,那继续WriteAsync这里入chan就会阻塞,WriteAsync就阻塞了

而从chan里取出来执行写的协程,通常是不应该在外部范围加锁了: https://github.com/lxzan/gws/blob/master/writer.go#L93 一是循环发送多个消息,这个锁粒度偏大,二是同样的writePublic可能阻塞了,这里锁的时间就更久了,其他地方WriteMessage就也会卡住

还有就是前面提到过的,你的AddJob是可能导致同时有多个协程存在,你这里循环发送外部加了锁,保证了时序,但是第一个循环发送的协程把所有消息都取出来发完了,其他几个协程进入循环后就是一次空跑浪费

我理解你这里这样设计是为了尽量不占用常驻协程,但用chan的方式好像是很难解决或者不是最优的

lxzan commented 1 year ago

review了下新的release,异步写这块应该还是不对:

首先,writePublic执行实际写入,是有可能阻塞的,比如tcp窗口拥塞

通常是入chan这块怕阻塞需要select default: https://github.com/lxzan/gws/blob/master/writer.go#L87 writePublic阻塞后,正在异步写的协程阻塞了,其他地方调用WriteAsync多次导致chan的size默认8也满了,那继续WriteAsync这里入chan就会阻塞,WriteAsync就阻塞了

而从chan里取出来执行写的协程,通常是不应该在外部范围加锁了: https://github.com/lxzan/gws/blob/master/writer.go#L93 一是循环发送多个消息,这个锁粒度偏大,二是同样的writePublic可能阻塞了,这里锁的时间就更久了,其他地方WriteMessage就也会卡住

还有就是前面提到过的,你的AddJob是可能导致同时有多个协程存在,你这里循环发送外部加了锁,保证了时序,但是第一个循环发送的协程把所有消息都取出来发完了,其他几个协程进入循环后就是一次空跑浪费

我理解你这里这样设计是为了尽量不占用常驻协程,但用chan的方式好像是很难解决或者不是最优的

现在的任务队列读写分离了,addJob不会导致并行多个协程,因为写队列并行度是1. 业务中应该尽量使用WriteMessage而不是WriteAsync,广播场景才迫切需要非阻塞.