- 主题:stackoverflow 有个比较 c++ stackful 和 stackless 协程的帖子
这个是基于处理 socket 连接,源于 unix 系统可以由多个线程抢同一个 listening socket. 这个机制受系统的影响比较大,有一定的可能性几个线程抢了所有的 accepted socket, 而大部分线程饿死。
所以如果协程是处理其它事情,或者想要更好的负载均衡效果,最好是搞自定义的队列。
【 在 ylh0315 的大作中提到: 】
: 预备一定数量的context池(协程),客户端连接一个分配一个,设置好epoll等待呼叫。然后如楼上所说。最后,disconnect时销毁并归还context。
--
FROM 110.81.0.*
我谨慎怀疑你的内存是不是被以某种不正常的方式耗光了
听起来一个客户端是一个较短时间的连接,一个客户端引发的负载不会很高,只有10000个客户端,不至于把8台16核的机器搞得登录都不能登陆吧
【 在 ylh0315 的大作中提到: 】
: 做过一个压力测试,服务器8台各16核,
: 管理器12核,12线程。模拟了10000个客户端,把8台服务器干到了100%,管理器也干到了85%的CPU利用率。
: 多线程协程自己写的,用的makecontext那一组函数,stackfull。自己造轮子,没有哪个现成的适用。
: ...................
--
FROM 113.120.108.*
我的看法:
如果要提供编程框架给别人用,为了让别人用起来像阻塞式变成一样方便,那这时候可能需要考虑用协程的方式实现你的框架
如果是为了自己用,完全可以自己用一个控制块来对应一个连接或者请求,到底对应什么取决于实际业务逻辑,实现一个状态机,然后直接用传统的方式epoll或者封装过的libev或者asio,来写代码,连接或者请求的状态变化在控制块里面记录。然后根据不同的状态和消息驱动运转状态机,相比攒一个协程的实现方式,这种方式既直观又省力
【 在 hgoldfish 的大作中提到: 】
: 这个是基于处理 socket 连接,源于 unix 系统可以由多个线程抢同一个 listening socket. 这个机制受系统的影响比较大,有一定的可能性几个线程抢了所有的 accepted socket, 而大部分线程饿死。
: 所以如果协程是处理其它事情,或者想要更好的负载均衡效果,最好是搞自定义的队列。
:
--
FROM 113.120.108.*
因为是压力测试,10000个客户端,每人100次业务调用,业务是计算密集型的,每个计算耗时秒级。所以这个压力要进行很长时间服务器都在满载状态。进行其他作业时,如login,非常迟钝。
【 在 wallyz 的大作中提到: 】
: 我谨慎怀疑你的内存是不是被以某种不正常的方式耗光了
: 听起来一个客户端是一个较短时间的连接,一个客户端引发的负载不会很高,只有10000个客户端,不至于把8台16核的机器搞得登录都不能登陆吧
:
--
FROM 221.218.61.*
一个主线程,进行完所有的初始化后,就进行accept。它负责接收客户端的连接,分配context,把它丢给epoll,然后就循环进行下一个accept。之后就是工作线程(线程池)和协程的活儿了,主线程不管。唯一可以accept的,就是这个主线程。没人抢活儿,也不使用epoll。
至于后来多个线程同时epoll_wait,有可能惊群,有处理方案。
如:
epv.events = flg?EPOLLOUT:EPOLLIN;
epv.events |= EPOLLONESHOT;//防止惊群
#ifdef EPOLLEXCLUSIVE
epv.events |= EPOLLEXCLUSIVE;
#endif
在线程得到context后,要测试惊群:
}
task = (TCB *)event.data.ptr;
if(task->events) {
ShowLog(1,"%s:tid=%lx,TCB_no=%d,task->events=%08X,conflict!",__FUNCTION__,
pthread_self(),task->sv.TCB_no,task->events);//发现惊群
task=NULL;
continue;//丢掉它
}
task->events=event.events;
前边所说的流程,1天就写完了,但是调试到稳定,用了4个月,反复进行压力测试。就是这些犄角旮旯的事麻烦。
生产者消费者模型,到处都是:
pthread_mutex_lock(&rpool.mut);
while(!(task=rdy_get())) {
if(rpool.flg >= tpool.rdy_num) break;
rpool.flg++;
ret=pthread_cond_wait(&rpool.cond,&rpool.mut); //没有任务,等待
rpool.flg--;
}
pthread_mutex_unlock(&rpool.mut);
熟悉不?线程条件锁。干这个用的。
【 在 hgoldfish 的大作中提到: 】
: 这个是基于处理 socket 连接,源于 unix 系统可以由多个线程抢同一个 listening socket. 这个机制受系统的影响比较大,有一定的可能性几个线程抢了所有的 accepted socket, 而大部分线程饿死。
: 所以如果协程是处理其它事情,或者想要更好的负载均衡效果,最好是搞自定义的队。
:
--
修改:ylh0315 FROM 221.218.61.*
FROM 221.218.61.*
accept确实很难测试惊群,也很难处理。
正好,主线程初始化系统之后,没事了,就让他进行accept,充当接生婆的角色。生出一个就丢进epoll一个,这孩子以后的事情就不归它管了。
这系统开始没有协程。发现一些家伙长时间进行IO占死一个线程,影响了其他人的服务。后来添加的协程。
使用协程后,单个任务的响应略有延长,但总体吞吐量和流畅性提高了。
【 在 hgoldfish 的大作中提到: 】
: 这个是基于处理 socket 连接,源于 unix 系统可以由多个线程抢同一个 listening socket. 这个机制受系统的影响比较大,有一定的可能性几个线程抢了所有的 accepted socket, 而大部分线程饿死。
: 所以如果协程是处理其它事情,或者想要更好的负载均衡效果,最好是搞自定义的队列。
:
--
修改:ylh0315 FROM 221.218.61.*
FROM 221.218.61.*
说的非常对。
交易管理器的设计,原来是没有协程的。采用接续模式。就是把任务划分成一个个的部分(我管他叫珠子),由IO(epoll,链子)接续起来。这样也能解决80~90%的问题。
但是有两个问题:
1是在数据包比较大,网络比较卡的情况,IO会长时间占用线程。
2一些步骤,如密钥协商,和登录认证,本身是一个完整的程序,很难劈开,占用时间又比较长。
所以把底层的IO函数重载成协程式的,就解决了这些问题。
【 在 wallyz 的大作中提到: 】
: 我的看法:
: 如果要提供编程框架给别人用,为了让别人用起来像阻塞式变成一样方便,那这时候可能需要考虑用协程的方式实现你的框架
: 如果是为了自己用,完全可以自己用一个控制块来对应一个连接或者请求,到底对应什么取决于实际业务逻辑,实现一个状态机,然后直接用传统的方式epoll或者封装过的libev或者asio,来写代码,连接或者请求的状态变化在控制块里面记录。然后根据不同的状态和消息驱动运转状态机,相比攒一个协程的实现方式,这种方式既直观又省力
: ...................
--
FROM 221.218.61.*
if(!rs->tc.uc_stack.ss_sp) {
ShowLog(5,"%s:%lx create fiber for TCB_no=%d",__FUNCTION__,rs->tid,task->sv.TCB_no);
task->uc.uc_stack.ss_sp=mmap(0, use_stack_size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON | MAP_GROWSDOWN, -1, 0);
我的栈分配,向下生长,自增。
【 在 hgoldfish 的大作中提到: 】
: 空间占用不严重。因为现在 linux/openbsd 等现代发行版都早就实现了自动增长的栈。一开始只给你 4KB,随着协程函数的运行才会继续增长。
: 所以你一次性创建 1m 协程,也只会占用 4GB 的内存空间。
: Windows 也有这个功能,但我现在还没有找到哪个 API 可以创建这种自动增长的栈内存。但我知道 Windows 确实是有实现的。因为 CreateFiber() 这个函数有这个功能。
: ...................
--
FROM 221.218.61.*
我有点纳闷,如果“IO会长时间占用线程”,这里我理解的IO肯定是非阻塞式的IO,那非阻塞IO怎么会长时间占用线程呢?
即使要发的数据包特别长,在socket buffer未满的情况下,本质上也还是内存拷贝(甚至零拷贝),也是不会长时间占用线程的
而如果socket buffer满了,遇见EAGAIN,这时候不应该就地重试send或者sleep或者就地poll,而是应该放弃这次send,转而处理别的连接,等待之前的socket的EPOLLOUT事件之后再重新send,所以没有道理会有一个连接长时间占用线程
而如果是read,你可以level trigger选择read一些,也可以选择edge trigger全部读完或者level trigger也可以选择全部读完,然后一个连接一个连接的过;无论哪种,对所有的连接都是机会均等的,
所以我很纳闷“IO会长时间占用线程”是如何发生的?
另一个角度,相比于你说的接续模式,你协程的实现本质上只是你的应用逻辑的组织形式和调用方式发生了变化,并没有改变你的IO模型(如果你之前就是用的非阻塞IO的话),也就是说从你的IO操作期间,比如调用send,到send返回,这期间你的协程所在的线程不能被别的协程抢占,read同理;那如果以前用接续模式会出现“IO会长时间占用线程”,那换成协程的方式,“IO会长时间占用线程”又是如何凭空消失的?
难道你在非协程的时候,选择一次读完,协程的时候选择读一些,造成了最后的结果差异?按理说一次读完的IO效率会更好才对;或者你在IO读完之后回调应用层逻辑的时候,数据的长短造成了应用层逻辑的行为有显著差异?
总之,对于协程解决了“IO会长时间占用线程”这个结论,我是很纳闷的
【 在 ylh0315 的大作中提到: 】
: 说的非常对。
: 交易管理器的设计,原来是没有协程的。采用接续模式。就是把任务划分成一个个的部分(我管他叫珠子),由IO(epoll,链子)接续起来。这样也能解决80~90%的问题。
: 但是有两个问题:
: ...................
--
FROM 113.120.108.*
如果说"密钥协商,和登录认证"造成了“IO长时间占用线程”的话,那应该说明的是,之前“接续模式”的程序本身的IO方式有些问题
因为SSL_do_handshake本身支持以非阻塞方式进行,主要是适当的处理SL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE;
登录认证就更不用说了,肯定只是一些应用层的消息交互,如果设计没问题,这必然不可能造成IO长时间占用线程
实际上,我理解中stackful的协程,内存占用天然的会比你所讲的接续模式或者stackless协程多,因为后二者只需要把必要的数据保留在控制块或者协程体中,而stackful本质上是把stack里的东西全部保留住直到下一次被调度(等待的事件到来),stack里面的东西必然有尚未弹出的非必要数据,这些非必要数据一起被保留在内存里,所以内存占用必然会多
【 在 ylh0315 的大作中提到: 】
: 说的非常对。
: 交易管理器的设计,原来是没有协程的。采用接续模式。就是把任务划分成一个个的部分(我管他叫珠子),由IO(epoll,链子)接续起来。这样也能解决80~90%的问题。
: 但是有两个问题:
: ...................
--
FROM 113.120.108.*