- 主题:stackoverflow 有个比较 c++ stackful 和 stackless 协程的帖子
预备一定数量的context池(协程),客户端连接一个分配一个,设置好epoll等待呼叫。然后如楼上所说。最后,disconnect时销毁并归还context。
【 在 hgoldfish 的大作中提到: 】
: 这个太简单了。弄个生产者消费者队列就行了。每个线程启动 100 个协程,一共 16 个核心是 1600 个协程。这些协程都在消费者队列里面抢任务。
: 如果打算节约资源,就在每个线程里面启动一个监管协程,检测空闲协程的数量。空闲协程太多,就杀掉一些协程,少了就再启动预备状态的协程。
: 由编程语言,比如 go, java, c# 那样子把协程自动地在各种线程之间调来调去一般都是没有必要的。
: ...................
--
FROM 221.218.61.*
因为是压力测试,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,协程模式下是非阻塞的。
【 在 wallyz 的大作中提到: 】
: 我有点纳闷,如果“IO会长时间占用线程”,这里我理解的IO肯定是非阻塞式的IO,那非阻塞IO怎么会长时间占用线程呢?
: 即使要发的数据包特别长,在socket buffer未满的情况下,本质上也还是内存拷贝(甚至零拷贝),也是不会长时间占用线程的
: 而如果socket buffer满了,遇见EAGAIN,这时候不应该就地重试send或者sleep或者就地poll,而是应该放弃这次send,转而处理别的连接,等待之前的socket的EPOLLOUT事件之后再重新send,所以没有道理会有一个连接长时间占用线程
: ...................
--
FROM 221.218.61.*
我说的密钥协商和登录认证,是举个例子,还有一些工作,细节很难切割成接续模式。
登录认证,在用户级是一个复杂的过程,其中会调用一些框架程序。我试图把它切成三段。结果解决了90%的问题,还是有一些边边角角的事会耽误一点点。
【 在 wallyz 的大作中提到: 】
: 如果说"密钥协商,和登录认证"造成了“IO长时间占用线程”的话,那应该说明的是,之前“接续模式”的程序本身的IO方式有些问题
: 因为SSL_do_handshake本身支持以非阻塞方式进行,主要是适当的处理SL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE;
: 登录认证就更不用说了,肯定只是一些应用层的消息交互,如果设计没问题,这必然不可能造成IO长时间占用线程
: ...................
--
FROM 221.218.61.*
关于协程栈的管理,我等会儿回答。
【 在 wallyz 的大作中提到: 】
: 如果说"密钥协商,和登录认证"造成了“IO长时间占用线程”的话,那应该说明的是,之前“接续模式”的程序本身的IO方式有些问题
: 因为SSL_do_handshake本身支持以非阻塞方式进行,主要是适当的处理SL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE;
: 登录认证就更不用说了,肯定只是一些应用层的消息交互,如果设计没问题,这必然不可能造成IO长时间占用线程
: ...................
--
FROM 221.218.61.*
例如:int RecvNet(int socket,char *buf,int n,int timeout)
接收,整个系统,包括客户端,管理器,服务器,及各种模式(多进程,多线程,线程池),都是通过这个函数进行的。
它基本就是read。多了个TIMEOUT。它原来是阻塞模式循环接收。
改成协程模式后,要与原来的兼容。
解决办法是,里面加一个:
static T_YIELD yield=NULL;
T_YIELD get_yield()
{
return yield;
}
T_YIELD set_yield(T_YIELD new_yield)
{
T_YIELD oldyield=yield;
yield=new_yield;
return oldyield;
}
如果在非协程环境,yield=NULL,就进行原来的IO.
协程环境设置yield函数,它就会变成非阻塞模式。
if(socket<0) return SYSERR;
if(!buf && n<0) return 0;
if(yield) {
fflag=fcntl(socket,F_GETFL,0);
if(fflag!=-1) fcntl(socket,F_SETFL,fflag|O_ASYNC|O_NONBLOCK); //异步操作
}
如果异步操作出现任何异常,就立即转换为同步模式,不会耽误事。
while(bcount<n){
if((br=read(socket,buf,n-bcount))>0){//如果能收到,就收,无需转入异步模式。
bcount+=br;
buf+=br;
repeat=0;
continue;
}
if(fflag==-1 && errno==EAGAIN) return TIMEOUTERR;
if(br<=0 && errno && errno != EAGAIN){
if(errno!=ECONNRESET)
ShowLog(1,"%s:br=%d,err=%d,%s",__FUNCTION__,br,errno,strerror(errno));
break;
}
if(bcount < n && fflag!=-1) { //切换任务
if(repeat++>3) return -errno;
ShowLog(5,"%s:tid=%lX,socket=%d,yield to schedle bcount=%d/%d",__FUNCTION__,pthread_self(),socket,bcount,n);
i=yield(socket,0,timeout);
if(i<0) {
if(timeout>0) {
struct timeval tmout;
tmout.tv_sec=timeout;
tmout.tv_usec=0;
ret=setsockopt(socket,SOL_SOCKET,SO_RCVTIMEO,(char *)&tmout,sizeof(tmout));
}
fcntl(socket,F_SETFL,fflag);
fflag=-1;
【 在 wallyz 的大作中提到: 】
: 我有点纳闷,如果“IO会长时间占用线程”,这里我理解的IO肯定是非阻塞式的IO,那非阻塞IO怎么会长时间占用线程呢?
: 即使要发的数据包特别长,在socket buffer未满的情况下,本质上也还是内存拷贝(甚至零拷贝),也是不会长时间占用线程的
: 而如果socket buffer满了,遇见EAGAIN,这时候不应该就地重试send或者sleep或者就地poll,而是应该放弃这次send,转而处理别的连接,等待之前的socket的EPOLLOUT事件之后再重新send,所以没有道理会有一个连接长时间占用线程
: ...................
--
修改:ylh0315 FROM 221.218.61.*
FROM 221.218.61.*