也来说说并发,使用状态机或轻量级线程

515 views
Skip to first unread message

lijie

unread,
Dec 4, 2007, 11:08:02 PM12/4/07
to pon...@googlegroups.com
并发这东西接触时间并不长,不过几乎让我完全推倒过去的编程方法。

并发系统的典型特点是大量同时存在的无规律活动,最具代表性的是网络应用,同时保持大量网络连接。过去我们常使用多进程、多线程方式来处理类似任务,优点是编程容易,对多CPU的应用比较充分(虽然不一定最高效),缺点是并发能力有限,线程不是个可无限分配的资源。在过去相当长时间内多线程方式(主要指每连接一线程方式)没有出现问题,我想原因是过去网络应用主要是以WEB为主,都是短连接,应用也不像今天这么广泛。

网络游戏服务器据我了解通常采用双进程方式,一个进程负责和其它服务器、客户端交互,另一进程负责游戏逻辑,这么设计的好处是把并发和游戏逻辑隔离开来,这对于计算相对密集的应用是比较合适的。 网络游戏服务器由于并发部分和逻辑隔离得比较远,而且是以计算为主,所以我认为它不是并发应用的典型代表,我这里要讨论的并发应用是以并发IO操作为主要特征的应用。

考虑另一种应用,网络硬盘、在线视频播放等应用,典型特点是长连接,低流量(相对服务器来说),大量并发活动。apache这样的架构支撑这种应用是很费力的,慢客户占用大量进程,服务器负载可能并不很高但却无法处理更多并发连接。这种应用似乎可以使用epoll来单线程处理(Reactor模式),不过对于网络硬盘这种和存储打交道的应用来说,fopen/fread/fwrite/fclose都是耗时操作,把这种操作放在一个线程里,硬盘繁忙时会怠慢所有连接。

完成端口/AIO或其它混合模式(比如IO操作丢进线程池,连接部分由主线程处理)也可以解决这种问题,不过问题是通常这种程序并不好写,根本原因是异步处理程序本来就很难写,而且不符合思维习惯。如果每个请求是由多步操作组成的,中间就需要状态机来确定下一个异步操作该做什么。

所以异步程序通常就是在处理状态机。在复杂的应用里面,不但编写维护很困难,开发周期长,而且BUG最容易从这里滋生。

erlang 给了我很多启发,同样是异步操作,它的语法就不会改变思考方式,原因就是它把状态放在轻量级线程里面,我们可以像写单线程同步操作一样来写异步操作,实际执行的动作是由底层平台调度完成的。这和多线程写法和功能上很像似,但它可以减少IO等待,比如可以让send操作注册一个事件,当socket可写时把数据写过去,并在完成时切换到当前"线程",在调用者看来如同一个阻塞操作。当然这并不是erlang的专利,windows上的fiber和unix/linux上的ucontext也可以做类似的工作,如果我们能实现一整套基于伪线程的IO库。但并非没有缺点,使用C++实现时你得考虑fiber栈开多大,太大了伪线程数量受限,太小了不能处理某些应用,这在某些动态语言里可以通过栈伸缩来实现,C/C++里实现这东西还是很困难。

[或许有人认为写个网络硬盘程序不算什么,很多人都写过。不过我要提醒的是写个支持高并发、分段式操作的程序来说,erlang可能只要几十到几百行,c++我没统计过,不过目前已经有个应用处理IO这部分至少有一千行。]

在C++里面如何实现高并发编程框架?这个我考虑过很长时间,感觉轻量级线程是唯一的出路,实际上就是erlang的实现原理。每个线程被创建出来就开始运行,直到它调用yield主动切换出去。由于大量线程都处于非活动状态,可能正在等待READ事件,或者等待某个计时器超时,所以实际要调度的线程是很少的,这也是并发系统的特点。调度部分只需要一个支持IO事件和计时器的东西就行,比如epoll,或者直接使用libevent。

我假想了这个框架的代码,是从erlang里面翻译过来的,框架正在实现过程中。

---------- CODE ---------
void WINAPI loop(void* arg)
{
    Process::Self()->ControlResource((Socket*)arg);
    Socket sock = *(Socket*)arg;
    while(true)
    {
        char lenstr[5];
        Must<int>(4) = sock.Recv(lenstr, 4, 0);
        lenstr[4] = '\0';

        int len = atoi(lenstr);
        char* p = new char[len + 1];
        Must<int>(len) = sock.Recv(p, len, 0);
        p[len] = '\0';
        std::cout << p << std::endl;
        delete[] p;
    }
}

void WINAPI server(void*)
{
    Socket sock;
    Must<int>(0) = sock.Create(AF_INET, SOCK_STREAM, 0);
    Must<int>(0) = sock.Bind(2345, "0.0.0.0");
    Must<int>(0) = sock.Listen(1024);
    while(true)
    {
        Socket client = sock.Accept();
        spawn(&loop, &client);
    }
}

void WINAPI fmain(void*)
{
    spawn(&server, Process::Self());
    receive();
}

int _tmain(int argc, _TCHAR* argv[])
{
    spawn(&fmain, NULL);
    ProcessManager::Run();

    system("PAUSE");
    return 0;
}
---------- CODE ---------

我自己感觉和erlang确实很像,细节上有些差别。

spawn创建一个轻量级并切换过去执行。这里fmain作为这个框架的入口,上面的例子里,它创建了另一个线程server,并进入receive,这让它只有收到消息才会退出。

server线程打开一个socket,注意这是在栈上打开的,离开栈就会被destroy掉,不过放心,这里正是要利用RAII实现资源管理。它是一个死循环,不断accept连接并创建出loop线程来处理。

loop线程要做的第一件事就是把socket控制权拿到当前线程,这样如果这个线程异常退出时,可以自动关闭socket,以免资源泄漏。

socket的各种耗时操作都只是注册事件并yield把当前线程切出去,当事件触发时它继续执行,实际上就是把状态机转换成轻量级线程了,因为栈上本来就可以维持状态。

上面代码中的Must<int>(0)是模拟erlang中的match语法,如果返回值不匹配就抛出异常。

关于WINAPI,由于我目前是使用windows里的fiber来实现的,暂时直接使用fiber的入口函数。

暂时先想这么多了,有兴趣的不妨讨论下。

pongba

unread,
Dec 5, 2007, 1:00:48 AM12/5/07
to pon...@googlegroups.com


On Dec 5, 2007 12:08 PM, lijie <cpu...@gmail.com> wrote:
erlang 给了我很多启发,同样是异步操作,它的语法就不会改变思考方式,原因就是它把状态放在轻量级线程里面,我们可以像写单线程同步操作一样来写异步操作,实际执行的动作是由底层平台调度完成的。这和多线程写法和功能上很像似,但它可以减少IO等待,比如可以让send操作注册一个事件,当socket可写时把数据写过去,并在完成时切换到当前"线程",在调用者看来如同一个阻塞操作。当然这并不是erlang的专利,windows上的fiber和unix/linux上的ucontext也可以做类似的工作,如果我们能实现一整套基于伪线程的IO库。但并非没有缺点,使用C++实现时你得考虑fiber栈开多大,太大了伪线程数量受限,太小了不能处理某些应用,这在某些动态语言里可以通过栈伸缩来实现,C/C++里实现这东西还是很困难。

我对erlang不熟悉,这一段需要更多解释:-) 或者指向erlang书的哪章哪节也好。

btw. redsea肯定对此有兴趣 :P

--
刘未鹏(pongba)|C++的罗浮宫
http://blog.csdn.net/pongba
TopLanguage
http://groups.google.com/group/pongba

Atry

unread,
Dec 5, 2007, 1:02:33 AM12/5/07
to pon...@googlegroups.com
我已经有一部分代码是用 Boost.Asio + Boost.Coroutine 来做的。对于比较复杂的网络协议,解析的过程需要保留中间状态的,这种编码有好处。

在07-12-5,lijie <cpu...@gmail.com > 写道:

Atry

unread,
Dec 5, 2007, 1:05:05 AM12/5/07
to pon...@googlegroups.com
对于 64 位机,栈内存空间还是够用的。
当然,理想情况下,应该是像 lua 的栈那样动态增长,这就需要依赖于虚拟机。

在07-12-5,Atry <pop....@gmail.com> 写道:

pongba

unread,
Dec 5, 2007, 1:06:03 AM12/5/07
to pon...@googlegroups.com
On Dec 5, 2007 2:02 PM, Atry <pop....@gmail.com> wrote:
我已经有一部分代码是用 Boost.Asio + Boost.Coroutine 来做的。对于比较复杂的网络协议,解析的过程需要保留中间状态的,这种编码有好处。
能具体说说怎么做的吗?

lijie

unread,
Dec 5, 2007, 1:10:18 AM12/5/07
to pon...@googlegroups.com
On Dec 5, 2007 2:00 PM, pongba <pon...@gmail.com> wrote:


On Dec 5, 2007 12:08 PM, lijie <cpu...@gmail.com> wrote:
erlang 给了我很多启发,同样是异步操作,它的语法就不会改变思考方式,原因就是它把状态放在轻量级线程里面,我们可以像写单线程同步操作一样来写异步操作,实际执行的动作是由底层平台调度完成的。这和多线程写法和功能上很像似,但它可以减少IO等待,比如可以让send操作注册一个事件,当socket可写时把数据写过去,并在完成时切换到当前"线程",在调用者看来如同一个阻塞操作。当然这并不是erlang的专利,windows上的fiber和unix/linux上的ucontext也可以做类似的工作,如果我们能实现一整套基于伪线程的IO库。但并非没有缺点,使用C++实现时你得考虑fiber栈开多大,太大了伪线程数量受限,太小了不能处理某些应用,这在某些动态语言里可以通过栈伸缩来实现,C/C++里实现这东西还是很困难。

我对erlang不熟悉,这一段需要更多解释:-) 或者指向erlang书的哪章哪节也好。

erlang的原理我很少看到,大部分是猜想、strace、读代码这样推断出来的。从我做过的测试来看,它的轻量级线程比ucontext/fiber高得多。。

http://qiezi.javaeye.com/blog/123977
这里我用D语言简单模拟了erlang的消息机制,如果加上超时和IO事件就完整了。发上来的这个性能比较差,我后来改进了一个版本,性能和多线程相当,比上面这个提高了60倍左右。不过装mac os x把文件弄坏掉了:-( tango里面这个fiber多半性能也不是很高,它还要修改gc来处理每个fiber的栈,如果有大量的fiber存在,反而不如C++高效了。

lijie

unread,
Dec 5, 2007, 1:12:58 AM12/5/07
to pon...@googlegroups.com
栈空间够用,内存却一定够。。如果没有栈的动态伸缩,无法创建大量轻量级线程。erlang里面"进程"数量最大限制是1亿多呢。。

Atry

unread,
Dec 5, 2007, 1:13:48 AM12/5/07
to pon...@googlegroups.com
Boost.Coroutine 的作者说 coroutine 切换的开销和一次间接函数调用差不多。

在07-12-5,Atry <pop....@gmail.com> 写道:
是个类似网络游戏的东西。TCP 连接,需要切包,但是包里面并没有长度,也就是说需要解析包的内容才知道长度。我就把收包的代码放到一个 coroutine 里面,这个 coroutine 是运行在主线程的。数据来了就切到解析协议的 coroutine ,在解析协议的 coroutine 里面需要读数据,就切到主循环(主循环挂在 io_service::run 上)。这样,解析协议的代码可以写成同步的,但实际上却是异步 IO 。这就是所谓"化异步为同步"。

在07-12-5,pongba <pon...@gmail.com> 写道:

Atry

unread,
Dec 5, 2007, 1:12:21 AM12/5/07
to pon...@googlegroups.com
是个类似网络游戏的东西。TCP 连接,需要切包,但是包里面并没有长度,也就是说需要解析包的内容才知道长度。我就把收包的代码放到一个 coroutine 里面,这个 coroutine 是运行在主线程的。数据来了就切到解析协议的 coroutine ,在解析协议的 coroutine 里面需要读数据,就切到主循环(主循环挂在 io_service::run 上)。这样,解析协议的代码可以写成同步的,但实际上却是异步 IO 。这就是所谓"化异步为同步"。

在07-12-5,pongba <pon...@gmail.com> 写道:


pi1ot

unread,
Dec 5, 2007, 1:38:49 AM12/5/07
to TopLanguage
高IO高并发的系统只能靠异步操作来缓解阻塞,erlang在这方面主要靠纯消息通信来做到的,没有ipc,没有rpc,没有lock,一切都是
message。用c++来模仿,兴许到最后你会发现自己也搞出一个简版erl vm。
何不直接扑向erlang呢,hehe。

On 12月5日, 下午12时08分, lijie <cpun...@gmail.com> wrote:
> 并发这东西接触时间并不长,不过几乎让我完全推倒过去的编程方法。
>
> 并发系统的典型特点是大量同时存在的无规律活动,最具代表性的是网络应用,同时保持大量网络连接。过去我们常使用多进程、多线程方式来处理类似任务,优点是编程-容易,对多CPU的应用比较充分(虽然不一定最高效),缺点是并发能力有限,线程不是个可无限分配的资源。在过去相当长时间内多线程方式(主要指每连接一线程方-式)没有出现问题,我想原因是过去网络应用主要是以WEB为主,都是短连接,应用也不像今天这么广泛。
>
> 网络游戏服务器据我了解通常采用双进程方式,一个进程负责和其它服务器、客户端交互,另一进程负责游戏逻辑,这么设计的好处是把并发和游戏逻辑隔离开来,这对于-计算相对密集的应用是比较合适的。
> 网络游戏服务器由于并发部分和逻辑隔离得比较远,而且是以计算为主,所以我认为它不是并发应用的典型代表,*
> 我这里要讨论的并发应用是以并发IO操作为主要特征的应用。*
>
> 考虑另一种应用,网络硬盘、在线视频播放等应用,典型特点是长连接,低流量(相对服务器来说),大量并发活动。apache这样的架构支撑这种应用是很费力的,-慢客户占用大量进程,服务器负载可能并不很高但却无法处理更多并发连接。这种应用似乎可以使用epoll来单线程处理(Reactor模式),不过对于网络硬盘-这种和存储打交道的应用来说,fopen/fread/fwrite/fclose都是耗时操作,把这种操作放在一个线程里,硬盘繁忙时会怠慢所有连接。
>
> 完成端口/AIO或其它混合模式(比如IO操作丢进线程池,连接部分由主线程处理)也可以解决这种问题,不过问题是通常这种程序并不好写,根本原因是异步处理程-序本来就很难写,而且不符合思维习惯。如果每个请求是由多步操作组成的,中间就需要状态机来确定下一个异步操作该做什么。
>
> 所以异步程序通常就是在处理状态机。在复杂的应用里面,不但编写维护很困难,开发周期长,而且BUG最容易从这里滋生。
>
> *erlang*给了我很多启发,同样是异步操作,它的语法就不会改变思考方式,原因就是它把状态放在轻量级线程里面,我们可以*
> 像写单线程同步操作一样来写异步操作*
> ,实际执行的动作是由底层平台调度完成的。这和多线程写法和功能上很像似,但它可以减少IO等待,比如可以让send操作注册一个事件,当socket可写时把-数据写过去,并在完成时切换到当前"线程",在调用者看来如同一个阻塞操作。当然这并不是erlang的专利,windows上的fiber和unix/lin-ux上的ucontext也可以做类似的工作,如果我们能实现一整套基于伪线程的IO库。但并非没有缺点,使用C++实现时你得考虑fiber栈开多大,太大了-伪线程数量受限,太小了不能处理某些应用,这在某些动态语言里可以通过栈伸缩来实现,C/C++里实现这东西还是很困难。
>
> [或许有人认为写个网络硬盘程序不算什么,很多人都写过。不过我要提醒的是写个支持高并发、分段式操作的程序来说,erlang可能只要几十到几百行,c++我-没统计过,不过目前已经有个应用处理IO这部分至少有一千行。]
>
> 在C++里面如何实现高并发编程框架?这个我考虑过很长时间,感觉轻量级线程是唯一的出路,实际上就是erlang的实现原理。每个线程被创建出来就开始运行,-直到它调用yield主动切换出去。由于大量线程都处于非活动状态,可能正在等待READ事件,或者等待某个计时器超时,所以实际要调度的线程是很少的,这也是-并发系统的特点。调度部分只需要一个支持IO事件和计时器的东西就行,比如epoll,或者直接使用libevent。
> spawn创建一个轻量级并切换过去执行。这里fmain作为这个框架的入口,上面的例子里,它创建了另一个线程server,并进入receive,这让它只-有收到消息才会退出。
>
> server线程打开一个socket,注意这是在栈上打开的,离开栈就会被destroy掉,不过放心,这里正是要利用RAII实现资源管理。它是一个死循环-,不断accept连接并创建出loop线程来处理。
>
> loop线程要做的第一件事就是把socket控制权拿到当前线程,这样如果这个线程异常退出时,可以自动关闭socket,以免资源泄漏。
>
> socket的各种耗时操作都只是注册事件并yield把当前线程切出去,当事件触发时它继续执行,实际上就是把状态机转换成轻量级线程了,因为栈上本来就可以-维持状态。

Atry

unread,
Dec 5, 2007, 1:44:22 AM12/5/07
to pon...@googlegroups.com
写异步代码,需要保存中间状态的。想要写得舒服,我觉得有两个不同的方向。
第一,用 coroutine 化异步为同步。
第二,语言支持 closure 。注意,必须是 closure 而不是 D 那样的 lambda
用 coroutine 或者 closure 两者任意一个都可以写出很舒服的异步代码。

在07-12-5, pi1ot <pilo...@gmail.com> 写道:
>     Must<int>(0) = sock.Bind (2345, "0.0.0.0");

lijie

unread,
Dec 5, 2007, 1:51:45 AM12/5/07
to pon...@googlegroups.com
语言不支持closure,可以自己用对象模拟,不过经过一段时间发现它不能解决问题,原因是每个closure都自己操作还是很复杂,每个操作还是要分解成多步。最终还是coroutine能简化问题。。

pi1ot

unread,
Dec 5, 2007, 1:52:12 AM12/5/07
to TopLanguage
最近写了些前端呈现的js代码,大量互相依赖状态的异步xmlhttp调用,没用现成的库从var XHR=new XMLHttpRequest()
写起,到后期慢慢形成一些异步调用的框架。
结论是,下次打死我也不再造轮子了,实在是有点自虐嫌疑。

On 12月5日, 下午2时44分, Atry <pop.a...@gmail.com> wrote:
> 写异步代码,需要保存中间状态的。想要写得舒服,我觉得有两个不同的方向。
> 第一,用 coroutine 化异步为同步。
> 第二,语言支持 closure 。注意,必须是 closure 而不是 D 那样的 lambda
> 用 coroutine 或者 closure 两者任意一个都可以写出很舒服的异步代码。
>
> 在07-12-5,pi1ot <pilot...@gmail.com> 写道:
>
>
>
>
>
> > 高IO高并发的系统只能靠异步操作来缓解阻塞,erlang在这方面主要靠纯消息通信来做到的,没有ipc,没有rpc,没有lock,一切都是
> > message。用c++来模仿,兴许到最后你会发现自己也搞出一个简版erl vm。
> > 何不直接扑向erlang呢,hehe。
>
> > On 12月5日, 下午12时08分, lijie <cpun...@gmail.com> wrote:
> > > 并发这东西接触时间并不长,不过几乎让我完全推倒过去的编程方法。
>
> > 并发系统的典型特点是大量同时存在的无规律活动,最具代表性的是网络应用,同时保持大量网络连接。过去我们常使用多进程、多线程方式来处理类似任务,优点是编程--容易,对多CPU的应用比较充分(虽然不一定最高效),缺点是并发能力有限,线程不是个可无限分配的资源。在过去相当长时间内多线程方式(主要指每连接一线程-方-式)没有出现问题,我想原因是过去网络应用主要是以WEB为主,都是短连接,应用也不像今天这么广泛。
>
> > 网络游戏服务器据我了解通常采用双进程方式,一个进程负责和其它服务器、客户端交互,另一进程负责游戏逻辑,这么设计的好处是把并发和游戏逻辑隔离开来,这对于--计算相对密集的应用是比较合适的。
> > > 网络游戏服务器由于并发部分和逻辑隔离得比较远,而且是以计算为主,所以我认为它不是并发应用的典型代表,*
> > > 我这里要讨论的并发应用是以并发IO操作为主要特征的应用。*
>
> > 考虑另一种应用,网络硬盘、在线视频播放等应用,典型特点是长连接,低流量(相对服务器来说),大量并发活动。apache这样的架构支撑这种应用是很费力的,--慢客户占用大量进程,服务器负载可能并不很高但却无法处理更多并发连接。这种应用似乎可以使用epoll来单线程处理(Reactor模式),不过对于网络硬-盘-这种和存储打交道的应用来说,fopen/fread/fwrite/fclose都是耗时操作,把这种操作放在一个线程里,硬盘繁忙时会怠慢所有连接。
>
> > 完成端口/AIO或其它混合模式(比如IO操作丢进线程池,连接部分由主线程处理)也可以解决这种问题,不过问题是通常这种程序并不好写,根本原因是异步处理程--序本来就很难写,而且不符合思维习惯。如果每个请求是由多步操作组成的,中间就需要状态机来确定下一个异步操作该做什么。
>
> > > 所以异步程序通常就是在处理状态机。在复杂的应用里面,不但编写维护很困难,开发周期长,而且BUG最容易从这里滋生。
>
> > > *erlang*给了我很多启发,同样是异步操作,它的语法就不会改变思考方式,原因就是它把状态放在轻量级线程里面,我们可以*
> > > 像写单线程同步操作一样来写异步操作*
>
> > ,实际执行的动作是由底层平台调度完成的。这和多线程写法和功能上很像似,但它可以减少IO等待,比如可以让send操作注册一个事件,当socket可写时把--数据写过去,并在完成时切换到当前"线程",在调用者看来如同一个阻塞操作。当然这并不是erlang的专利,windows上的fiber和unix/li-n-ux上的ucontext也可以做类似的工作,如果我们能实现一整套基于伪线程的IO库。但并非没有缺点,使用C++实现时你得考虑fiber栈开多大,太-大了-伪线程数量受限,太小了不能处理某些应用,这在某些动态语言里可以通过栈伸缩来实现,C/C++里实现这东西还是很困难。
>
> > [或许有人认为写个网络硬盘程序不算什么,很多人都写过。不过我要提醒的是写个支持高并发、分段式操作的程序来说,erlang可能只要几十到几百行,c++我--没统计过,不过目前已经有个应用处理IO这部分至少有一千行。]
>
> > 在C++里面如何实现高并发编程框架?这个我考虑过很长时间,感觉轻量级线程是唯一的出路,实际上就是erlang的实现原理。每个线程被创建出来就开始运行,--直到它调用yield主动切换出去。由于大量线程都处于非活动状态,可能正在等待READ事件,或者等待某个计时器超时,所以实际要调度的线程是很少的,这也-是-并发系统的特点。调度部分只需要一个支持IO事件和计时器的东西就行,比如epoll,或者直接使用libevent。
>
> > > 我假想了这个框架的代码,是从erlang里面翻译过来的,框架正在实现过程中。
>
> > > ---------- CODE ---------
> > > void WINAPI loop(void* arg)
> > > {
> > > Process::Self()->ControlResource((Socket*)arg);
> > > Socket sock = *(Socket*)arg;
> > > while(true)
> > > {
> > > char lenstr[5];
> > > Must<int>(4) = sock.Recv(lenstr, 4, 0);
> > > lenstr[4] = '\0';
>
> > > int len = atoi(lenstr);
> > > char* p = new char[len + 1];
> > > Must<int>(len) = sock.Recv(p, len, 0);
> > > p[len] = '\0';
> > > std::cout << p << std::endl;
> > > delete[] p;
> > > }
>
> > > }
>
> > > void WINAPI server(void*)
> > > {
> > > Socket sock;
> > > Must<int>(0) = sock.Create(AF_INET, SOCK_STREAM, 0);
> > > Must<int>(0) = sock.Bind(2345, "0.0.0.0");
> > > Must<int>(0) = sock.Listen(1024);
> > > while(true)
> > > {
> > > Socket client = sock.Accept();
> > > spawn(&loop, &client);
> > > }
>
> > > }
>
> > > void WINAPI fmain(void*)
> > > {
> > > spawn(&server, Process::Self());
> > > receive();
>
> > > }
>
> > > int _tmain(int argc, _TCHAR* argv[])
> > > {
> > > spawn(&fmain, NULL);
> > > ProcessManager::Run();
>
> > > system("PAUSE");
> > > return 0;}
>
> > > ---------- CODE ---------
>
> > > 我自己感觉和erlang确实很像,细节上有些差别。
>
> > spawn创建一个轻量级并切换过去执行。这里fmain作为这个框架的入口,上面的例子里,它创建了另一个线程server,并进入receive,这让它只--有收到消息才会退出。
>
> > server线程打开一个socket,注意这是在栈上打开的,离开栈就会被destroy掉,不过放心,这里正是要利用RAII实现资源管理。它是一个死循环--,不断accept连接并创建出loop线程来处理。
>
> > > loop线程要做的第一件事就是把socket控制权拿到当前线程,这样如果这个线程异常退出时,可以自动关闭socket,以免资源泄漏。
>
> > socket的各种耗时操作都只是注册事件并yield把当前线程切出去,当事件触发时它继续执行,实际上就是把状态机转换成轻量级线程了,因为栈上本来就可以--维持状态。
>
> > > 上面代码中的Must<int>(0)是模拟erlang中的match语法,如果返回值不匹配就抛出异常。
>
> > > 关于WINAPI,由于我目前是使用windows里的fiber来实现的,暂时直接使用fiber的入口函数。
>
> > > 暂时先想这么多了,有兴趣的不妨讨论下。- 隐藏被引用文字 -
>
> - 显示引用的文字 -

lijie

unread,
Dec 5, 2007, 1:54:55 AM12/5/07
to pon...@googlegroups.com
使用erlang阻力很大的,开发人员不好找。而且很多东西没办法只用erlang完成,port又据说不是很稳定,而且总隔着一层语言不是很方便。我们用python/ruby时会因为效率而使用C写扩展,效率足够的情况下不需要写C代码。使用erlang却是因为某些东西必须用C写port。。。

pi1ot

unread,
Dec 5, 2007, 2:20:52 AM12/5/07
to TopLanguage
说起来本人心底也隐隐有拿cpp搞个erlang like之类东西的想法,函数性的语法就不用了,能搞出可靠的message和node机制,还有
link,再加上gen_server,貌似也能挺像回事的。

On 12月5日, 下午2时54分, lijie <cpun...@gmail.com> wrote:
> 使用erlang阻力很大的,开发人员不好找。而且很多东西没办法只用erlang完成,port又据说不是很稳定,而且总隔着一层语言不是很方便。我们用py-thon/ruby时会因为效率而使用C写扩展,效率足够的情况下不需要写C代码。使用erlang却是因为某些东西必须用C写port。。。
>
> On Dec 5, 2007 2:38 PM, pi1ot <pilot...@gmail.com> wrote:
>
>
>
> > 高IO高并发的系统只能靠异步操作来缓解阻塞,erlang在这方面主要靠纯消息通信来做到的,没有ipc,没有rpc,没有lock,一切都是
> > message。用c++来模仿,兴许到最后你会发现自己也搞出一个简版erl vm。
> > 何不直接扑向erlang呢,hehe。- 隐藏被引用文字 -
>
> - 显示引用的文字 -

pi1ot

unread,
Dec 5, 2007, 2:27:07 AM12/5/07
to TopLanguage
这不就是个ICE么,写完了自己才反应过来。

On 12月5日, 下午3时20分, pi1ot <pilot...@gmail.com> wrote:
> 说起来本人心底也隐隐有拿cpp搞个erlang like之类东西的想法,函数性的语法就不用了,能搞出可靠的message和node机制,还有
> link,再加上gen_server,貌似也能挺像回事的。
>
> On 12月5日, 下午2时54分, lijie <cpun...@gmail.com> wrote:
>
>
>
> > 使用erlang阻力很大的,开发人员不好找。而且很多东西没办法只用erlang完成,port又据说不是很稳定,而且总隔着一层语言不是很方便。我们用py--thon/ruby时会因为效率而使用C写扩展,效率足够的情况下不需要写C代码。使用erlang却是因为某些东西必须用C写port。。。
>
> > On Dec 5, 2007 2:38 PM, pi1ot <pilot...@gmail.com> wrote:
>
> > > 高IO高并发的系统只能靠异步操作来缓解阻塞,erlang在这方面主要靠纯消息通信来做到的,没有ipc,没有rpc,没有lock,一切都是
> > > message。用c++来模仿,兴许到最后你会发现自己也搞出一个简版erl vm。
> > > 何不直接扑向erlang呢,hehe。- 隐藏被引用文字 -
>
> > - 显示引用的文字 -- 隐藏被引用文字 -
>
> - 显示引用的文字 -

SpitFire

unread,
Dec 5, 2007, 2:32:02 AM12/5/07
to pon...@googlegroups.com
用 coroutine 化异步为同步是如何做的,有啥paper可以看看
closure有没有paper

在07-12-5,Atry <pop....@gmail.com> 写道:



--
SpitFire

lijie

unread,
Dec 5, 2007, 2:54:59 AM12/5/07
to pon...@googlegroups.com
呵呵。我这里的函数主要是给spawn使用的,用类还真是有些罗嗦。

里面加了点像erlang一样的match语法(我代码里这个是没法工作的),和D里面 Andrei Alexandrescu 做的enforce有点相似,其实就是把反回值转成错误码。不喜欢enforce这样套一层函数,括号也很讨厌不是。

lijie

unread,
Dec 5, 2007, 3:30:26 AM12/5/07
to pon...@googlegroups.com
boost里面有啊?一直不知道。不过boost想起来就有些发抖。

要的就是"化异步为同步",等于把编程难度降了一个维度。用在协议解析也是用对地方了,用轻量级线程代替状态机。不过轻量级线程可以做得更多,可以用它来做并发处理。

大家对reactor都比较熟,我拿它举个例子和轻量级线程比较一下。

很多人是这么用的:

class MyHandler : public EventHandler
{
  int status;
public:
  virtual int HandleInput(..)
  {
    recv(xxxx)
    switch(status)
      ....
  }
};

还有一种用法:
struct ReadRequest
{
  int bytes;
  CALLBACK_FUNC func;
  void* arg;
};

class MyHandler : public EventHandler
{
  std::list<ReadRequest> requests;

public:
  int Open()
  {
    // ......
    RequestRead(4, &MyHandler::PackLengthRead, this);
  }

  static void PackLengthRead(const char* data, size_t len, void* arg)
  {
    MyHandler* pthis = static_cast<MyHandler*>(arg);
    int packlen = convert_int_from_data(data, len);
    pthis->RequestRead(packlen, &MyHandler::PackRead, this);
  }

  static void PackRead(const char* data, size_t len, void* arg)
  {
     // ...
  }

  virtual int HandleInput(...)
  {
    // process requests
  }
};

这种方式是把状态分解成多步操作,比状态机稍稍容易编写。如果是D语言由于有委托,就不需要static了。

看看erlang:

receive_text_package(Socket, Timeout) ->
    {ok, Packet} = gen_tcp:recv(Socket, 4, Timeout),
    Len = erlang:list_to_integer(binary_to_list(Packet)),
    {ok, Data} = gen_tcp:recv(Socket, Len, Timeout),
    io:format("Received package: ~p~n", [Data]).

完全像是同步操作。实现原理并不复杂,它会在Recv的时候挂一个READ事件,再把当前"线程"的时间片交出去。可读或超时的时候再切回来。我前面发的C++代码也是从erlang翻译的,目前正打算编写的一个框架就是这样子。对应的代码:

        char lenstr[5];
        Must<int>(4) = sock.Recv(lenstr, 4, 0);
        lenstr[4] = '\0';

        int len = atoi(lenstr);
        char* p = new char[len + 1];
        Must<int>(len) = sock.Recv(p, len, 0);
        p[len] = '\0';
        std::cout << "Received package: " << p << std::endl;
        delete[] p;

erlang还有个特色的地方,就是异步线程。比如file:write,不打开异步线程时,操作是在同一个线程里面的。如果设备比较慢,或者某些设置必须要多线程写才能提高效率,就可以用erl +A设置异步线程数,程序不用做任何修改。

最近做的项目在遇到头疼的地方时,通常都会strace看erlang是怎么解决的。。。

Atry

unread,
Dec 5, 2007, 3:44:46 AM12/5/07
to pon...@googlegroups.com
那个库已经停止开发了,估计也没希望加入 boost 了。是 GSoC 2006
的一个项目。我用的感觉,接口设计是有问题的,而且有一些陷阱,用得不对出错了很难调试。但若是正确的使用, bug 不多。

在 07-12-5,lijie<cpu...@gmail.com> 写道:

bipe...@gmail.com

unread,
Dec 5, 2007, 9:09:20 PM12/5/07
to TopLanguage
只用过reactor, 异步一直没有机会在真正的项目用过。希望以后能用一下。
> > On Dec 5, 2007 2:13 PM, Atry < pop.a...@gmail.com> wrote:
> > > Boost.Coroutine 的作者说 coroutine 切换的开销和一次间接函数调用差不多。
>
> > > 在07-12-5,Atry <pop.a...@gmail.com> 写道:
> > > > 是个类似网络游戏的东西。TCP
> > 连接,需要切包,但是包里面并没有长度,也就是说需要解析包的内容才知道长度。我就把收包的代码放到一个 coroutine
> > 里面,这个 coroutine 是运行在主线程的。数据来了就切到解析协议的 coroutine ,在解析协议的 coroutine
> > 里面需要读数据,就切到主循环(主循环挂在 io_service::run 上)。这样,解析协议的代码可以写成同步的,但实际上却是异步 IO
> > 。这就是所谓"化异步为同步"。
>
> > > > 在07-12-5,pongba <pon...@gmail.com> 写道:
>
> > > > > On Dec 5, 2007 2:02 PM, Atry <pop.a...@gmail.com> wrote:
>
> > > > > > 我已经有一部分代码是用 Boost.Asio + Boost.Coroutine
> > 来做的。对于比较复杂的网络协议,解析的过程需要保留中间状态的,这种编码有好处。
>
> > > > > 能具体说说怎么做的吗?
>
> > > > > > 在07-12-5,lijie <cpun...@gmail.com > 写道:
> ...
>
> 阅读更多 >>

lijie

unread,
Dec 5, 2007, 10:58:37 PM12/5/07
to pon...@googlegroups.com
找到一个东西,《A Language-based Approach to Unifying Events and Threads report.pdf》

一会上传上来。

它是用haskell描述的,monads这个东西我又从来没看懂过,所以不知道和它描述的是不是很相似,不过要解决的问题是一样的。

On Dec 5, 2007 2:00 PM, pongba < pon...@gmail.com> wrote:

red...@gmail.com

unread,
Dec 7, 2007, 1:13:14 AM12/7/07
to pon...@googlegroups.com
这些方面我算是有一些经验, 我来说说我对一些常见方案的看法吧:

1. process per connection / thread per connection
这在连接数不多的时候编码方便.
并且, 在连接数不多而要求网络吞吐量高的场合, 这种方式是最容易实现, 效果也好.

2. 单一 selector/preactor
需要编写大量的状态机代码, 比较麻烦.
配合 epoll/kqueue/ devpoll 等机制, 没有 io block 问题的时候, 这种模式可
以轻易处理大量连接, cpu 消耗少.

需要注意的是, 文件访问不一定会造成大的 io block, 看业务目标和设计. 例如,
发送文件内容的时候, 可以使用 sendfile 调用, 或者 (有人提出过, 我还没有研
究过) 将文件 mmap 到内存中, 然后用 zero copy api 去发送这段内存.

外界的数据库访问之类的, 则一定会造成 block.

3. 线程池

half-sync / half-async 方式
一个 selector 负责接收请求, 一堆线程处理请求
缺点是 切换的 thread context 太多, 对 cpu cache 也不利.

leader / follower 方式
优点是减少了 thread context 切换, 对 cpu cache 也更有利.
ACE 的书上写这个模式的缺点是, 编写程序更复杂.

但是根据我自己的实践, 这里面还有一个对性能很不利的点:
hs ha 方式, selector 可以有很多数据作为自己线程的私有数据, 访问的时候不
必加锁.

领导者跟随者模式中, 由于所有线程都可以访问所有的数据结构, 造成所有的数据
访问之前都需要加锁(可能有的数据结构: 发送队列, 全局连接表 或者 os 具体
poll 设施的数据结构), 这在多core 的情况下, 会引起很大的性能损失. 如果使
用小的加锁粒度, 那么要申请的锁数目众多, 做一件事情可能要加解多次锁; 如果
使用大粒度锁, 那么锁竞争冲突可能会严重.

我没有做实际的测试, 但是担心领导者追随者这种方式, 在大连接数, 多 cpu
core 的情况下表现会差, 所以放弃了这种方式.

我现在写的库, 实际上是使用 ha hs 方式, 但是, 多数简单的任务, 直接就在
selector 线程中处理了, 计算量大的任务, 或者是有 io block 的任务, 才交给
线程池进行处理, 对于这种复杂的任务来说, thread context 的切换开销, cpu
cache 的损失, 也就不算什么了.


4. erlang 的方式
我想过写状态机不方便, 如果能够象写顺序执行的代码一样写代码就好了, 但是再
想到要处理的种种问题, 以及状态机其实写熟了也就那样了, 就算了 :)


BTW: ACE 的selector 代码, 在事件发生的时候, 先将这个事件源上的事件关闭,
处理完了再打开, 这在大连接数的情况下会造成很大的开销, OS call 的开销不小.

asio 的我忘了.

Googol Lee

unread,
Dec 7, 2007, 2:37:01 AM12/7/07
to pon...@googlegroups.com
拜过把状态机写熟的大牛……

在 07-12-7,red...@gmail.com<red...@gmail.com> 写道:


--
新的理论从少数人的主张到一统天下,并不是因为这个理论说服了别人抛弃旧观点,而是因为一代人的逝去。

My blog: http://googollee.blog.163.com

lijie

unread,
Dec 7, 2007, 3:05:58 AM12/7/07
to pon...@googlegroups.com
On Dec 7, 2007 2:13 PM, <red...@gmail.com> wrote:

2. 单一 selector/preactor
需要编写大量的状态机代码, 比较麻烦.
配合 epoll/kqueue/ devpoll 等机制, 没有 io block 问题的时候, 这种模式可
以轻易处理大量连接, cpu 消耗少.
 
这个适合网络IO,文件IO应该还是不太适合。



需要注意的是, 文件访问不一定会造成大的 io block, 看业务目标和设计. 例如,
发送文件内容的时候, 可以使用 sendfile 调用, 或者 (有人提出过, 我还没有研
究过) 将文件 mmap 到内存中, 然后用 zero copy api 去发送这段内存.
 
并发的文件访问会造成 io等待,实际测试发现这和文件系统类型、文件数也有关系。文件数多了以后,通常IO操作比较耗时的是open/close,由于读写都有缓冲,反而很少出在读写上。我的测试环境环境是2T的磁盘空间写100k左右的文件,写到几百G就变慢很多了。



3. 线程池

half-sync / half-async 方式
一个 selector 负责接收请求, 一堆线程处理请求
缺点是 切换的 thread context 太多, 对 cpu cache 也不利.

处理网络请求的我都考虑单线程实现,大量并发使用epoll处理还算轻松。处理文件的使用AIO,实际测试的确比自己开线程池处理高效很多。很多应用都要混合这两种IO,所以只好都用上,中间加上个通知机制。
 


leader / follower 方式
优点是减少了 thread context 切换, 对 cpu cache 也更有利.
ACE 的书上写这个模式的缺点是, 编写程序更复杂.

但是根据我自己的实践, 这里面还有一个对性能很不利的点:
hs ha 方式, selector 可以有很多数据作为自己线程的私有数据, 访问的时候
必加锁
.

领导者跟随者模式中, 由于所有线程都可以访问所有的数据结构, 造成 所有的数据
访问之前都需要加锁
(可能有的数据结构: 发送队列, 全局连接表 或者 os 具体
poll 设施的数据结构), 这在多core 的情况下, 会引起很大的性能损失. 如果使
用小的加锁粒度, 那么要申请的锁数目众多, 做一件事情可能要加解多次锁; 如果
使用大粒度锁, 那么锁竞争冲突可能会严重.

也有另一种解决办法,就是开多个selector,不过调度也是个麻烦,而且要求各个连接之间没有交互,web服务器这样的倒是很适合。
 
状态机主要不是写起来麻烦,而是本来很顺畅的想法被拆成几段,看着有点恶心。。



说一个实际的应用,一个存储项目。由于要处理很多低流量的连接,所以连接那部分是epoll处理的。这个存储除了要保存本地文件以外,还要往另一个备份设备(同样的上传服务器)上写一份,而且要确保备份成功才返回。

第一个实现是收够包头后就交给线程池处理。这个实现是把文件上传成功后再写备份,后来发现这样不好,用户都上传完了还要等一会,这段时间备份设备可能流量突增,备份设备负载也可能一会大一会小。

第二个版本改成一边上传一边备份,收一次就写一次并同步一次。这也有问题,磁盘可能突然很忙,备份设备也会忙,不能因为这个就降低用户上传的流量。所以上传时先写到缓冲区,写文件和备份可以异步进行。另一个问题,文件可能很大,1000个同时上传的文件,每个10M内在都不够用了,又改成维护一个链表,写文件和备份都完成的部分就删掉以释放内存空间。

线程池来处理也很有问题,线程数量开大了很影响性能,开小了并发数又受限,这些低流量的连接又占着线程不拉XX。
所以第三个版本这部分改成aio了,其它部分都在主线程里面完成,主线程只是完成调度。接着发现aio不支持文件open/close,经过统计这两个操作又是最耗时的,主线程不能因为这个去阻塞,所以加个线程池来完成open/close。

结果性能还是很理想的,用aio换掉线程池就让IO性能提升了很多。问题是这几个版本一个比一个难写,第一个开发只要2-3天,第三个版本花掉2周时间才写完并排除大部分BUG,而且这部分代码看着就头大,每添加一个功能就要去修改状态机那部分,每个连接可是有好几个状态机分别标记socket/file和同步。

于是就有了这个帖子的想法。理想情况下,这个程序可以这么简单:
-define(TIMEOUT, 3000).

server(Port) ->
    {ok, Listen) = gen_tcp:listen(Port, [binary]),
    server_loop(Listen).

server_loop(Listen) ->
    {ok, Socket} = gen_tcp:accept(Listen),
    spawn(fun() -> process_upload(Socket) end),
    server_loop(Listen).

process_upload(Socket) ->
    %% Receive header length
    {ok, HeaderLenStr} = gen_tcp:recv(Socket, 4, ?TIMEOUT),
    HeaderLen = erlang:list_to_integer(binary_to_list(HeaderLenStr)),
    %% Receive header
    {ok, Header} = gen_tcp:recv(Socket, HeaderLen, ?TIMEOUT),
    %% unpack header
    {ok, FileLen, Signature} = unpack(Header),
    %% open file
    Filename = generate_filename(),
    File = file:open(Filename, write),
    FilePid = spawn(fun() -> file_writer(File) end),
    %% open replicate
    {ok, RepSock} = gen_tcp:connect(?REP_HOST, ?REP_PORT, [binary]),
    RepPid = spawn(fun() -> rep_writer(RepSock) end),
    do_upload(FileLen, Socket, FilePid, RepPid).

do_upload(FileLen, Socket, FilePid, RepPid) ->
    %% control socket, handle socket message, send to FilePid, send to RepPid

虽然没写出实现部分,但可以想象不是很复杂,这么复杂的逻辑用erlang不会超过200行。erlang做这个不一定高效,这和它的虚拟机平台效率有冯,但如果我们自己实现出这一套东西,可以想象效率肯定不会比现有的这一套复杂代码效率低,但代码却很简单。

red...@gmail.com

unread,
Dec 7, 2007, 4:06:14 AM12/7/07
to pon...@googlegroups.com

 
并发的文件访问会造成 io等待,实际测试发现这和文件系统类型、文件数也有关系。文件数多了以后,通常IO操作比较耗时的是open/close,由于读写都有缓冲,反而很少 出在读写上。我的测试环境环境是2T的磁盘空间写100k左右的文件,写到几百G就变慢很多了。
 
   open /close 速度有几个地方可以进行尝试的, 不知道你做了没有:
1. 如果是大量小文件, 那么文件系统类型是值得尝试的, 通常认为linux 下面 reiserfs 处理大量小文件性能较好, open close 性能较高

2. 采用多级目录的方法, 给最终用到的文件名做 hash, 对应到这些目录上, 这样每个目录下面的文件不会太多

3. 如果一个文件比较零碎, open 之后做 seek 也是比较慢的, linux 2.6 自某个版本之后, 具体忘了, 多了一个新 kernel api, 可以给文件保留空间, 这样就不会出现文件一直往后写, 越来越零碎了.




也有另一种解决办法,就是开多个selector,不过调度也是个麻烦,而且要求各个连接之间没有交互,web服务器这样的倒是很适合。
 
状态机主要不是写起来麻烦,而是本来很顺畅的想法被拆成几段,看着有点恶心。。
   这倒是, 状态机代码, 看起来讨厌.  所以我写过这样的状态机, 将实际工作的代码基本上可以集中在一起.

#define WAIT_STAGE(connection, newStage)  { connection->expectStage=newStage; return; }

   if ( processEvent(connection, & event) != NEED_MORE_INPUT )
   {
     // 这里是状态转移代码,  尽量用二维数据描述, 如果不行, 而必须用 switch, 那就比较讨厌了.
     switch(stage)
     {
     case stage1:
           switch(event)
           {
           case event1:
                  newStage = xxx;
                  break;
           ...
           }
     ...           
     }   

     // 检查目标状态是否是期望的
     if (connection->expectedStage != newStage)
        出错处理
     else
        connection->stage = newStage;


     // 这里主要是实际工作代码,
     // 也包含一些状态转移代码, 例如真实状态机可以 expect 多个后级状态的, 这里就只能先 except 到一个中间状态
     // 然后根据 event 再跳转到下级状态
     switch (newStage)
     {
     case stage1:
               start_do_someing;
               WAIT_STAGE(connection, stage2);

     case stage2:
               if (! try_do_soming_immdeiately() )
                    WAIT_STAGE(connection, stage3);
               connection->stage = stage3;
     case stage3:
               ...
     }
   }
  

   最后面那段, 也就基本上能够集中实际工作代码了, 看起来是恶心了一点, 吐啊吐啊习惯了就好了.
   因此, 也就懒得研究更深入的想法了.

   等lijie 兄辛苦完毕, 有成绩之后我再来偷师.
  
         



说一个实际的应用,一个存储项目。由于要处理很多低流量的连接,所以连接那部分是epoll处理的。这个存储除了要保存本地文件以外,还要往另一个备份设 备(同样的上传服务器)上写一份,而且要确保备份成功才返回。

第一个实现是收够包头后就交给线程池处理。这个实现是把文件上传成功后再写备份,后来发现这样不好,用户都上传完了还要等一会,这段时间备份设备可能流量 突增,备份设备负载也可能一会大一会小。

第二个版本改成一边上传一边备份,收一次就写一次并同步一次。这也有问题,磁盘可能突然很忙,备份设备也会忙,不能因为这个就降低用户上传的流量。所以上 传时先写到缓冲区,写文件和备份可以异步进行。另一个问题,文件可能很大,1000个同时上传的文件,每个10M内在都不够用了,又改成维护一个链表,写 文件和备份都完成的部分就删掉以释放内存空间。

线程池来处理也很有问题,线程数量开大了很影响性能,开小了并发数又受限,这些低流量的连接又占着线程不拉XX。
所以第三个版本这部分改成aio了,其它部分都在主线程里面完成,主线程只是完成调度。接着发现aio不支持文件open/close,经过统计这两个操 作又是最耗时的,主线程不能因为这个去阻塞,所以加个线程池来完成open/close。


结果性能还是很理想的,用aio换掉线程池就让IO性能提升了很多。问题是这几个版本一个比一个难写,第一个开发只要2-3天,第三个版本花掉2周时间才 写完并排除大部分BUG,而且这部分代码看着就头大,每添加一个功能就要去修改状态机那部分,每个连接可是有好几个状态机分别标记socket/file 和同步。
  哈哈, 对性能而言, 你这个方案确实没得说了, 所有的性能瓶颈都有好的对策, 就是架构复杂, 如果已经有了一个好的通信, 调度, AIO 框架还好办, 从头弄起, 还是有很多东西要写, 要 debug 的 :)

pi1ot

unread,
Dec 7, 2007, 4:23:48 AM12/7/07
to TopLanguage
refs辅以合适的目录hash算法,较ext3而言,可以极大地提高大量小文件的io性能,实际项目经验。

On 12月7日, 下午5时06分, red...@gmail.com wrote:
>
>
> 并发的文件访问会造成 io等待,实际测试发现这和文件系统类型、文件数也有关系。文件数多了以后,通常IO操作比较耗时的是open/close,由于读写都有缓冲,反而很少出在读写上。我的测试环境环境是2T的磁盘空间写100k左右的文件,写到几百G就变慢很多了。
> 说一个实际的应用,一个存储项目。由于要处理很多低流量的连接,所以连接那部分是epoll处理的。这个存储除了要保存本地文件以外,还要往另一个备份设备(同样的上传服务器)上写一份,而且要确保备份成功才返回。
> 第一个实现是收够包头后就交给线程池处理。这个实现是把文件上传成功后再写备份,后来发现这样不好,用户都上传完了还要等一会,这段时间备份设备可能流量突增,备份设备负载也可能一会大一会小。
> 第二个版本改成一边上传一边备份,收一次就写一次并同步一次。这也有问题,磁盘可能突然很忙,备份设备也会忙,不能因为这个就降低用户上传的流量。所以上传时先写到缓冲区,写文件和备份可以异步进行。另一个问题,文件可能很大,1000个同时上传的文件,每个10M内在都不够用了,又改成维护一个链表,写文件和备份都完成的部分就删掉以释放内存空间。
> 线程池来处理也很有问题,线程数量开大了很影响性能,开小了并发数又受限,这些低流量的连接又占着线程不拉XX。
> 所以第三个版本这部分改成aio了,其它部分都在主线程里面完成,主线程只是完成调度。接着发现aio不支持文件open/close,经过统计这两个操作又是最耗时的,主线程不能因为这个去阻塞,所以加个线程池来完成open/close。
> 结果性能还是很理想的,用aio换掉线程池就让IO性能提升了很多。问题是这几个版本一个比一个难写,第一个开发只要2-3天,第三个版本花掉2周时间才写完并排除大部分BUG,而且这部分代码看着就头大,每添加一个功能就要去修改状态机那部分,每个连接可是有好几个状态机分别标记socket/file 和同步。 哈哈, 对性能而言, 你这个方案确实没得说了, 所有的性能瓶颈都有好的对策, 就是架构复杂, 如果已经有了一个好的通信, 调度, AIO 框架还好办, 从头弄起, 还是有很多东西要写, 要 debug 的 :)
> 于是就有了这个帖子的想法。理想情况下,这个程序可以这么简单:
> -define(TIMEOUT, 3000).
> server(Port) ->
> {ok, Listen) = gen_tcp:listen(Port, [binary]),
> server_loop(Listen).
> server_loop(Listen) ->
> {ok, Socket} = gen_tcp:accept(Listen),
> spawn(fun() -> process_upload(Socket) end),
> server_loop(Listen).
> process_upload(Socket) ->
> %% Receive header length
> {ok, HeaderLenStr} = gen_tcp:recv(Socket, 4, ?TIMEOUT),
> HeaderLen = erlang:list_to_integer(binary_to_list(HeaderLenStr)),
> %% Receive header
> {ok, Header} = gen_tcp:recv(Socket, HeaderLen, ?TIMEOUT),
> %% unpack header
> {ok, FileLen, Signature} = unpack(Header),
> %% open file
> Filename = generate_filename(),
> File =file:open(Filename, write),

lijie

unread,
Dec 7, 2007, 4:27:43 AM12/7/07
to pon...@googlegroups.com
那我应该测试一下reiserfs了。目前文件名是随机产生的mds5,还是比较散的,分成256 * 256,不知道是否适合。

On Dec 7, 2007 5:23 PM, pi1ot <pilo...@gmail.com> wrote:
refs辅以合适的目录hash算法,较ext3而言,可以极大地提高大量小文件的io性能,实际项目经验。


lijie

unread,
Dec 7, 2007, 4:26:05 AM12/7/07
to pon...@googlegroups.com
On Dec 7, 2007 5:06 PM, <red...@gmail.com> wrote:

 
并发的文件访问会造成 io等待,实际测试发现这和文件系统类型、文件数也有关系。文件数多了以后,通常IO操作比较耗时的是open/close,由于读写都有缓冲,反而很少 出在读写上。我的测试环境环境是2T的磁盘空间写100k左右的文件,写到几百G就变慢很多了。
 
   open /close 速度有几个地方可以进行尝试的, 不知道你做了没有:
1. 如果是大量小文件, 那么文件系统类型是值得尝试的, 通常认为linux 下面 reiserfs 处理大量小文件性能较好, open close 性能较高

也搜索了一些别人做过的测试,结果都是xfs综合性能最高,所以目前使用的是xfs,reiserfs据说前景不妙,所以没考虑用呢,做个综合性的测试也比较费精力。
 


2. 采用多级目录的方法, 给最终用到的文件名做 hash, 对应到这些目录上, 这样每个目录下面的文件不会太多

原来也是这么想的,不过有同事做了几天测试后结论是:xfs几百万个文件放在同一个目录下比分级性能还高!有点出乎意料,不过也抽不出时间来验证了。不过为了便于管理,还是给它分级了。。



3. 如果一个文件比较零碎, open 之后做 seek 也是比较慢的, linux 2.6 自某个版本之后, 具体忘了, 多了一个新 kernel api, 可以给文件保留空间, 这样就不会出现文件一直往后写, 越来越零碎了.

这个倒是没关注,有时间研究下。
 

状态机也只能这么做了,要不然也不叫状态机。。还有另一种做法,就是把stage做成对象,各个stage按顺序做成一个链表,每次处理完了就取链表的next处理,结果和状态机是一样的,也算是一种分解吧,貌似ace里面也有类似的东西,以前大致看到一点,没使用过。


     
  哈哈, 对性能而言, 你这个方案确实没得说了, 所有的性能瓶颈都有好的对策, 就是架构复杂, 如果已经有了一个好的通信, 调度, AIO 框架还好办, 从头弄起, 还是有很多东西要写, 要 debug 的 :)

基本上是从头写,通讯框架倒是前两个月刚做完一套。AIO的例子网上也不多,所以都是从头摸索。架构、原理、性能上参考erlang比较多,到目前为止我认为erlang算是个完美平台,只是写port麻烦了点。erlang对操作系统效率最高的异步文件IO也基本上没用上,所以对于我这里的项目来说,性能上超过它还是有胜算的,没有胜算的部分是需要利用多CPU调度的。

red...@gmail.com

unread,
Dec 7, 2007, 5:08:04 AM12/7/07
to pon...@googlegroups.com

也搜索了一些别人做过的测试,结果都是xfs综合性能最高,所以目前使用的是xfs,reiserfs据说前景不妙,所以没考虑用呢,做个 综合性的测试也比较费精力。

   xfs 大文件性能高 (GB 级别吧), 小文件并无优势.
   reiserfs 前景不好是 reiser4, reiser3 早进了 kernel, 官方人员自然会维护.

   我们的测试, cf 卡上用 reiser 开机速度比用 xfs 可以快一倍, cf 卡的读写速度慢, 内部没有cache, reiser 的速度快, 应该证明 reiser造成的实际读写次数更少.  但是最终我们在 cf 卡上没有使用 reiserfs, 原因是, 异常关机的时候, cf 卡上 reiserfs 的文件系统检查速度很不理想, 比 xfs 慢几十倍.

 


2. 采用多级目录的方法, 给最终用到的文件名做 hash, 对应到这些目录上, 这样每个目录下面的文件不会太多

原来也是这么想的,不过有同事做了几天测试后结论是:xfs几百万个文件放在同一个目录下比分级性能还高!有点出乎意料,不过也抽不出时间来验证了。不过 为了便于管理,还是给它分级了。。
   这个结论有点奇怪, 朋友们处理大量文件都是用分级的, 因为都碰到过一个目录下面文件太多, open close 慢.




3. 如果一个文件比较零碎, open 之后做 seek 也是比较慢的, linux 2.6 自某个版本之后, 具体忘了, 多了一个新 kernel api, 可以给文件保留空间, 这样就不会出现文件一直往后写, 越来越零碎了.

这个倒是没关注,有时间研究下。
   你的应用如果不是大文件, 则不需要关注这个.

状态机也只能这么做了,要不然也不叫状态机。。还有另一种做法,就是把stage做成对象,各个stage按顺序做成一个链表,每次处理完了就取链表的 next处理,结果和状态机是一样的,也算是一种分解吧,貌似ace里面也有类似的东西,以前大致看到一点,没使用过。
   理论上没有问题, 代码看起来也可以更漂亮.

   但是考虑到实际 debug 需要, 调试或者加 log 信息时候, 用 switch 的代码反而更容易处理.

   我其实不太喜欢一大堆小对象, 或者调用一层嵌一层很深的, 架构可能很漂亮, 但是阅读这种代码和调试这些代码, 都很痛苦. 漂亮又不能当饭吃, 呵呵.

  

  


     
  哈哈, 对性能而言, 你这个方案确实没得说了, 所有的性能瓶颈都有好的对策, 就是架构复杂, 如果已经有了一个好的通信, 调度, AIO 框架还好办, 从头弄起, 还是有很多东西要写, 要 debug 的 :)

基本上是从头写,通讯框架倒是前两个月刚做完一套。AIO的例子网上也不多,所以都是从头摸索。架构、原理、性能上参考erlang比较多,到目前为止我 认为erlang算是个完美平台,只是写port麻烦了点。erlang对操作系统效率最高的异步文件IO也基本上没用上,所以对于我这里的项目来说,性 能上超过它还是有胜算的,没有胜算的部分是需要利用多CPU调度的。
  如果  erlang 可以轻松和某个编译语言结合起来, 岂不是比较完美 ?

pi1ot

unread,
Dec 7, 2007, 5:20:49 AM12/7/07
to TopLanguage
port很令人抓狂,传个数据而已,又是server又是monitor的。

red...@gmail.com

unread,
Dec 7, 2007, 6:16:35 AM12/7/07
to pon...@googlegroups.com
256 * 256 是什么意思?
还有, 目录做hash 的话, 层数不要太多了, 在目录无法全部被cache 的情况下, 多一层目录, 可能要多若干io.
一个目录大一点的话, 由于 block io prefetch 的存在, 还是很有可能一次 IO 就全部读到内存的.

如果要做深度优化, 那么要检查 fs 的目录项, 存一个文件entey 用多少字节, 硬盘的 io prefetch sector, fs driver 的prefetch 参数之类, 综合来做决定和设置.

lijie 写道:

pi1ot

unread,
Dec 7, 2007, 6:23:08 AM12/7/07
to TopLanguage
就是256x256级子目录的意思,还得看具体hash算法,避免分配不均。

pi1ot

unread,
Dec 7, 2007, 6:27:02 AM12/7/07
to TopLanguage
错了,两级目录,每级有256个。
google groups不能改帖子?这样倒是能优化一下论坛系统性能。

On 12月7日, 下午7时23分, pi1ot <pilot...@gmail.com> wrote:
> 就是256x256级子目录的意思,还得看具体hash算法,避免分配不均。
>
> On 12月7日, 下午7时16分, red...@gmail.com wrote:
>
> > 256 * 256 是什么意思?
> > 还有, 目录做hash 的话, 层数不要太多了, 在目录无法全部被cache 的情况下, 多一层目录, 可能要多若干io.
> > 一个目录大一点的话, 由于 block io prefetch 的存在, 还是很有可能一次 IO 就全部读到内存的.
> > 如果要做深度优化, 那么要检查 fs 的目录项, 存一个文件entey 用多少字节, 硬盘的 io prefetch sector, fs driver 的prefetch 参数之类, 综合来做决定和设置.
> > lijie 写道:那我应该测试一下reiserfs了。目前文件名是随机产生的mds5,还是比较散的,分成256 * 256,不知道是否适合。On Dec 7, 2007 5:23 PM, pi1ot <pilot...@gmail.com> wrote:refs 辅以合适的目录hash算法,较ext3而言,可以极大地提高大量小文件的io性能,实际项目经验。

lijie

unread,
Dec 7, 2007, 6:34:29 AM12/7/07
to pon...@googlegroups.com
/root/00/00/xxxxxxxx
这种形式。因为单台上文件就有几千万,这样每个目录下只有几百上千个文件了。如果是一级,单个目录下就是几万到十几万个文件。

现在没太多精力做更多研究,毕竟开发人手满少。。

lijie

unread,
Dec 7, 2007, 6:55:26 AM12/7/07
to pon...@googlegroups.com
我是实用派,语言方面还是以实用为主,有空余的时间则也喜欢玩玩各种语言,开阔下眼界,学习点思想也很重要不是。貌似我讨论的内容和这个论坛有点不太和谐呀,呵呵。

lijie

unread,
Dec 8, 2007, 11:13:06 AM12/8/07
to pon...@googlegroups.com
上传了一个文档:threads-hotos-2003.pdf

没细看,似乎是说线程方式比基于事件方式更好一些.不过前面举的例子有明显问题,文件是从内存cache读取的,每个请求8k左右,可以很容易地全部塞进socket缓冲区,这种方式自然是线程更容易处理,因为事件方式多一次系统调用。另外这个数据统计也有问题,并发系统的测试非常复杂,需要模拟随机的慢客户,突发的大并发。我在项目中就发现模拟压力测试测不出所有问题,挂到真实系统中就很容易找到真正的瓶颈,通常会得出和模拟压力测试不同的结论。

使用我前面描述的轻量级线程方式或者是基于事件方式,可以异步IO很好地配合,而操作系统实现的异步IO可以对读写请求进行重排以优化性能,虽然也听到说这个特性的效果不怎么样,但测试发现性能相差还是非常多的。

red...@gmail.com

unread,
Dec 8, 2007, 12:01:57 PM12/8/07
to pon...@googlegroups.com
lijie 写道:
上传了一个文档:threads-hotos-2003.pdf

没细看,似乎是说线程方式比基于事件方式更好一些.不过前面举的例子有明显问题,文件是从内存cache读取的,每个请求8k左右,
  没看文档, 我只是觉得, 不能简单说什么方式好, 这个和具体应用场合关系太大了.

  当然, 要求不高的代码, 例如, 不必处理大量网络连接, 不要求尽量短时延, 不要求尽量用完网络带宽, 或者尽量用完硬盘的IO 能力等的话, 那, 随便怎么写都可以, 甚至多进程方式也有机会 ok 的.

可以很容易地全部塞进socket缓冲区,这种方式自然是线程更容易处理,因为事件方式多一次系统调用。另外这个数据统计也有 问题,并发系统的测试非常复杂,需要模拟随机的慢客户,突发的大并发。我在项目中就发现模拟压力测试测不出所有问题,挂到真实系统中就很容易找到真正的瓶 颈,通常会得出和模拟压力测试不同的结论。
  对, 互联网上情况复杂, 如果通信程序的通信对方程序不是自己控制代码, 就更复杂, 例如, 会碰到慢速客户, 发了请求就不收应答的客户,  带宽巨大的客户, 短时间内产生大量新连接的客户等等, 用每连接一个线程方式, 其实很容易被弄出问题来.

  一个例子: 我们给客户做的一个应用中, 有一个 http 服务器, 平时就有近万个无效用户挂在上面. 曾经的代码是, 对无效用户, 关闭连接, 后来发现不对, 这些无效用户会立刻重连, 搞到每秒钟收到的 SYN 包非常多, 和 SYN flood 攻击效果一样, 最后, 就只好让它们挂着, 反正他们一秒钟也就发两三千个请求, 能够应付得过来.
 

使用我前面描述的轻量级线程方式或者是基于事件方式,可以异步IO很好地配合,而操作系统实现的异步IO可以对读写请求进行重排以优化性能,虽然也听到说 这个特性的效果不怎么样,但测试发现性能相差还是非常多的。


red...@gmail.com

unread,
Dec 8, 2007, 11:34:27 PM12/8/07
to pon...@googlegroups.com
快速浏览了一下网络上两个 thread & event 的文档, 似乎还是属于研究性质的, 不过这个方向不错, 结果可以对以后的程序很有帮助.

lijie 写道:
上传了一个文档:threads-hotos-2003.pdf

没细看,似乎是说线程方式比基于事件方式更好一些.不过前面举的例子有明显问题,文件是从内存cache读取的,每个请求8k左右,可以很容易地全部塞进 socket缓冲区,这种方式自然是线程更容易处理,因为事件方式多一次系统调用。另外这个数据统计也有问题,并发系统的测试非常复杂,需要模拟随机的慢 客户,突发的大并发。我在项目中就发现模拟压力测试测不出所有问题,挂到真实系统中就很容易找到真正的瓶颈,通常会得出和模拟压力测试不同的结论。

oldrev

unread,
Dec 9, 2007, 4:22:03 AM12/9/07
to pon...@googlegroups.com
完全同意,XFS是最快的文件系统而且很安全稳定,我一直都用她,唯一的缺点可
能就是 Grub 不支持了。


在 2007-12-07五的 17:26 +0800,lijie写道:

> stage做成对象,各个stage按顺序做成一个链表,每次处理完了就取链表的next
> 处理,结果和状态机是一样的,也算是一种分解吧,貌似ace里面也有类似的东

> 西,以前大致看到一点,没使用过。
>
>
>
>
> 哈哈, 对性能而言, 你这个方案确实没得说了, 所有的性能瓶颈都有
> 好的对策, 就是架构复杂, 如果已经有了一个好的通信, 调度, AIO 框
> 架还好办, 从头弄起, 还是有很多东西要写, 要 debug 的 :)
>
> 基本上是从头写,通讯框架倒是前两个月刚做完一套。AIO的例子网上也不多,
> 所以都是从头摸索。架构、原理、性能上参考erlang比较多,到目前为止我认为
> erlang算是个完美平台,只是写port麻烦了点。erlang对操作系统效率最高的异
> 步文件IO也基本上没用上,所以对于我这里的项目来说,性能上超过它还是有胜
> 算的,没有胜算的部分是需要利用多CPU调度的。
>
>
> >

--
"Live Long and Prosper"
- oldrev

oldrev

unread,
Dec 9, 2007, 4:24:08 AM12/9/07
to pon...@googlegroups.com
整版最精华的帖子了 :)

在 2007-12-07五的 14:13 +0800,red...@gmail.com写道:

red...@gmail.com

unread,
Dec 9, 2007, 4:34:20 AM12/9/07
to pon...@googlegroups.com
oldrev 写道:
> 整版最精华的帖子了 :)
>
岂敢岂敢.

前一阵以ponda 和 longshanksmo 为首讨论的深层内容, 技术含量才真是高啊.

lijie

unread,
Dec 9, 2007, 6:50:26 AM12/9/07
to pon...@googlegroups.com
哈哈,确实不错,很全面,看来都是POSA的读者吧。

补充点讨论。

目前应用主要是HS/HA方式,LF方式我总感觉缺点大于优点。cpu cache的影响不容易测试出来,大概实际应用很少在这上面出现性能瓶颈吧。

前段时间学习erlang,发现它在编写IO操作上的一些好处。

1、erlang的reactor/proactor(伪)模式代码只是编写风格上的不同。比如reactor,只需要把socket对象设成active,就可以在进程中不断接收数据包消息来处理。而proactor只需要调用gen_tcp:recv来设置需要收完多少字节,收完后才返回,当然这是伪proactor,本身也是reactor方式模拟的,但并不是说以后就没有可能使用一些异步IO来实现。它不像ACE一样编写reactor/proactor代码有巨大的差异。

2、send操作默认是直接在主线程中阻塞发送的,某些情况下这样效率比较高,因为直接发到缓冲区了。如果数据比较大,则可以设置发送选项为delay_send,这时候并不马上发送,而是注册OUT事件,有事件时才真正发送,缺点是对于短连接小数据包方式,多一次系统调用。erlang在这上面切换很灵活

3、(前面提过)文件IO操作在没有打开异步线程时,是直接在主线程中完成的,打开以后就会交给线程池完成,程序不用改变。

4、erlang调度线程和port线程池的通讯方式实现比较普通,说普通是因为大家都是这么做的。线程池那边阻塞等待信号,调度线程有任务就放队列并触发信号,线程池处理完以后放到队列并发送一字节到socket来唤醒调度线程。由于调度线程这边是不需要加锁的,所以它使用的肯定是个无锁队列,只需要在线程池那一端加锁。

有了这些特性,可以用它轻松编写各种模式的代码,除了LF模式和每连接一线程方式以外,这和它的内核实现有关,它选择了一种它认为最好的方式。erlang的smp方式我没研究过,不过通常HS/HA方式总是可以充分利用CPU,使用smp有可能效率反而降低了。

注:以上是使用erlang编写代码,strace跟踪的结果。由于实际测试过erlang的性能还是很高的,虽然语言性能稍低点,但目前我写过的所有的压力测试程序,用erlang写的那个是可以把程序负载压到最高的,所以在使用C++做项目时通常是尽可能向它学习。

2007/12/9 oldrev < old...@gmail.com>:

莫华枫

unread,
Dec 9, 2007, 8:10:48 AM12/9/07
to pon...@googlegroups.com
On Dec 9, 2007 5:34 PM, <red...@gmail.com> wrote:
oldrev 写道:
> 整版最精华的帖子了 :)
>
 岂敢岂敢.

 前一阵以ponda 和 longshanksmo 为首讨论的深层内容, 技术含量才真是高啊.
哎哟哟,可不能这么说。这是两个领域的东西啊。都是很重要的。我已经把它star了,我的"缺门",正要好好学习哦。:)
建议pongba把这两个帖子页顶置。:)



> 在 2007-12-07五的 14:13 +0800,red...@gmail.com写道:
>
>> 这些方面我算是有一些经验, 我来说说我对一些常见方案的看法吧:
>>


pongba

unread,
Dec 9, 2007, 10:47:10 PM12/9/07
to pon...@googlegroups.com


On Dec 9, 2007 9:10 PM, 莫华枫 <longsh...@gmail.com> wrote:

On Dec 9, 2007 5:34 PM, <red...@gmail.com> wrote:
oldrev 写道:
> 整版最精华的帖子了 :)
>
 岂敢岂敢.

 前一阵以ponda 和 longshanksmo 为首讨论的深层内容, 技术含量才真是高啊.
哎哟哟,可不能这么说。这是两个领域的东西啊。都是很重要的。我已经把它star了,我的"缺门",正要好好学习哦。:)
建议pongba把这两个帖子页顶置。:)

收到:-)

Daniel Lv

unread,
Dec 9, 2007, 11:03:44 PM12/9/07
to pon...@googlegroups.com
这个mail-list的讨论,质量实在是太高!
--
=====================================
Name : Daniel Lv
Email : lgn...@gmail.com
=====================================

red...@gmail.com

unread,
Dec 9, 2007, 11:17:39 PM12/9/07
to pon...@googlegroups.com
lijie 写道:

> 哈哈,确实不错,很全面,看来都是POSA的读者吧。
>
> 补充点讨论。
>
> 目前应用主要是HS/HA方式,LF方式我总感觉缺点大于优点。cpu cache的影响
> 不容易测试出来,大概实际应用很少在这上面出现性能瓶颈吧。
>
> 前段时间学习erlang,发现它在编写IO操作上的一些好处。
>
> 1、erlang的reactor/proactor(伪)模式代码只是编写风格上的不同。比如
> reactor,只需要把socket对象设成active,就可以在进程中不断接收数据包消
> 息来处理。而proactor只需要调用gen_tcp:recv来设置需要收完多少字节,收完
> 后才返回,当然这是伪proactor,本身也是reactor方式模拟的,但并不是说以
> 后就没有可能使用一些异步IO来实现。它不像ACE一样编写
> reactor/proactor代码有巨大的差异。
这是我对 ACE 不满的其中一点了, 非必要地搞出这些东西来.

>
> 2、send操作默认是直接在主线程中阻塞发送的,某些情况下这样效率比较高,
> 因为直接发到缓冲区了。如果数据比较大,则可以设置发送选项为delay_send,
> 这时候并不马上发送,而是注册OUT事件,有事件时才真正发送,缺点是对于
> 短连接小数据包方式,多一次系统调用。erlang在这上面切换很灵活
>
> 3、(前面提过)文件IO操作在没有打开异步线程时,是直接在主线程中完成
> 的,打开以后就会交给线程池完成,程序不用改变。
>
> 4、erlang调度线程和port线程池的通讯方式实现比较普通,说普通是因为大家
> 都是这么做的。线程池那边阻塞等待信号,调度线程有任务就放队列并触发信
> 号,线程池处理完以后放到队列并发送一字节到socket来唤醒调度线程。由于调
> 度线程这边是不需要加锁的,所以它使用的肯定是个无锁队列,只需要在线程池
> 那一端加锁。
>
> 有了这些特性,可以用它轻松编写各种模式的代码,除了LF模式和每连接一线
> 程方式以外,这和它的内核实现有关,它选择了一种它认为最好的方式。erlang
> 的smp方式我没研究过,不过通常HS/HA方式总是可以充分利用CPU,使用smp
> 有可能效率反而降低了。
>
> 注:以上是使用erlang编写代码,strace跟踪的结果。由于实际测试过erlang的
> 性能还是很高的,虽然语言性能稍低点,但目前我写过的所有的压力测试程序,
> 用erlang写的那个是可以把程序负载压到最高的,所以在使用C++做项目时通
> 常是尽可能向它学习。
>

好东西的成本就是使用代价高 :(

BTW:
ACE, asio, zeroc ice 的代码我都看过, 觉得ACE, asio可以适合那些要求不那
么非常高的代码, 例如连接数不要太多, 不要求将网络带宽尽量用掉, 网络延时尽
量减小等, 还是可以用的.
ice 优点是容易跨语言, 说到性能指标, 其实比较不行, 适合较轻的应用;
thread per connection 对于大连接数, 开销厉害; 没有必要的复杂逻辑, 对于要
求低延时等也不是很有利. 但是, 如果只是用于较轻的应用, 在程序已经集成脚本
语言的时候, 通常有非常容易的选择, 例如 python 的 pyro.

ACE_OS 里面的东西我倒是很喜欢, 处理平台差异做得很好, 如果是独立的就好了.

pi1ot

unread,
Dec 9, 2007, 11:29:22 PM12/9/07
to TopLanguage
不知道你描述的性能指标具体指什么,到什么量级,我一直在用ICE,不过因为项目开始得比较早,使用的特性也不多,所以一直停留在v1.5阶段没有升级
过。
在我这里是一个内部调用的后端server,每次请求都要读些几个文件并返回数据,大概每秒百十来次,高不过千,跑的有年头了至少ICE部分从来没出过
问题。
ICE也提供了AMI和AMD的异步调用和分派模式,但是编码方面比较复杂,性能需求也没到那个程度,所以我懒得再改了。
一般性不算太变态的server/client项目,ICE绝对是够用了。
> ACE_OS 里面的东西我倒是很喜欢, 处理平台差异做得很好, 如果是独立的就好了.- 隐藏被引用文字 -
>
> - 显示引用的文字 -

redsea

unread,
Dec 9, 2007, 11:56:04 PM12/9/07
to TopLanguage
恩, 对你这种级别的应用, ice 应该够了.

我们有一个服务器, 要能够处理至少 70K 个 TCP 连接, 每秒钟至少 10K 个请求;
另外一个过滤器, 延时指标不能超过 0.12 ms, 其中网卡收包发包和旁路保护装置大概占用了 50us, 也就是说, 剩下70us 的时间要
保证处理完毕一个包.
> > - 显示引用的文字 -- 隐藏被引用文字 -
>
> - 显示引用的文字 -

redsea

unread,
Dec 10, 2007, 12:04:36 AM12/10/07
to TopLanguage
程序开发中发现, 一个函数, 用ddr2 800 的内存, 多访问三个没有在cache 中的不连续内存, 会使得消耗的时间多0.4us 以
上, 而多执行很多条cpu 指令, 甚至包括条件预测missing, 影响都没有那么大.

粗略计算一下, ddr2 800 的内存的频率是 400M, 每个 clock 是2.5ns, 几个主要延时值记得好像是 5 5 5 16,
访问不连续内存, 似乎这些延时值都要消耗, 31 * 2.5 =77.5 ns, 77.5 * 3 = 232.5ns, 似乎离
0.4us 还有一些差距, 不知道还有一些在哪里消耗掉了.

pi1ot

unread,
Dec 10, 2007, 12:28:26 AM12/10/07
to TopLanguage
这样级别的按我的理解不会有任何现成框架可用了吧,从头造轮子比较靠谱。
顺便说说你们的轮子是怎么造的? :)

On 12月10日, 下午12时56分, redsea <red...@gmail.com> wrote:

redsea

unread,
Dec 10, 2007, 1:04:50 AM12/10/07
to TopLanguage
TCP 连接的有框架可以用的, 只要用单线程方式, epoll, kqueue, devpolll 之类的东西, 代码结构写好, 避免太
多的动态内存申请, 释放, 做起来不是什么很难的事情.

延时那个, 倒没有什么框架可以用, 主要是问题比较极端了:

之前还没有说得足够清楚, 上贴中说到网卡等硬件的延时, 指数据已经放到网卡的内部缓冲区, 它发到网上去的延时是这么多, 其实这里面主要的延时就
是从网卡存储的数据转换成 100M 以太网的信号这点了, 并行 --> 慢速串行. (一个 dlink 100M 交换机的延时大概是
30us的级别).

网卡之后的部分, 包括网卡准备好之后通过中断 cpu, cpu 进入内核态, 从网卡处拿数据, 等等, 一堆工作, 都会造成延时.
最后实际上, 我们主体函数的处理时间, 只剩下 5个 us, 所以多几个cache missing 的内存访问, 例如40个, 就全部时间都
没有了.

这里面倒是没有轮子可以用, 直接写 kernel 代码, 数据结构设计好, 让 cache 能够充分发挥作用, cpu 周期不是问题, 很富余
(例如 0.4us 在 2g 的core2 duo cpu 上, 应该够执行 400ns / 0.5 * 3 = 2400 条简单指令了, 但
这仅仅够三个内存 miss).

这些领域中, OO 在被处理的数据对象上其实不好用, 简单的POD好用. 对cache/内存而言, 数据结构经常就是pod数组最理想了, 即
使cache missing, 也是一步到位, 一次miss 之后可以访问到数据, 而不必通过n 多次指针跳来跳去.

无法使用数组, 也必须设法让数据紧凑, 例如, "对象"有可变长度数据, 那么将对象的基本数据放在前面, 可变数据放在紧接的后面, 让内存可以
连续访问 (c 的常用方法, c++ 要用自定义new); 多个"对象"不要独立new, 而是new 一大块内存之后, 将这些对象按照访问相关
性, 顺序排列好 ----- 似乎和数据库实现中的记录存放方案也差不多, 那里是硬盘访问速度很慢, 这里是内存比 cpu 慢很多.

为了cache 效果好, 有时候还要拆分属于一个 "对象" 的东西, 例如 a, b,c,d 四个属性, a,b 经常被访问, 而且是一起访
问, 那么 a,b 放一起弄一个 pod 数组, c,d 访问不多, 那另外放一个.

pi1ot

unread,
Dec 10, 2007, 1:21:17 AM12/10/07
to TopLanguage
说实话既然都到了us必扣的时候,没有框架适合你了,你做的东西也不能再抽象成框架贡献出来。只能是具体问题具体分析,具体问题具体解决了。
> > > 保证处理完毕一个包.- 隐藏被引用文字 -
>
> - 显示引用的文字 -

up duan

unread,
Dec 17, 2007, 2:17:36 AM12/17/07
to pon...@googlegroups.com
并发有两个考虑的角度,一个是实现,一个是接口。
到现在为止,实现可以分为共享内存和消息传递两大类,还有一些小类比如事务内存等。
接口上,可以分为同步式的接口和异步式的接口,同步式的接口一般表现为线程形式(或者其他类似的形式),异步式的接口表现为信号,异步io或者状态机【其实状态机不严格是异步接口,只是依附于异步接口的一种配对接口】。
共享内存的一般性能较高,尤其是大量的共享数据存在的时候,不过由于锁的使用,其性能会快速下降,导致现在无锁编程开始崭露头脚。消息传递一般并行度可以很高,但是如果没有更精致的细化其消息传递机制(主要是充分采用zero
copy),其性能一般不可接受。
同步式接口的优势是简单和直观,符合人类思维模式,但是由于大多数系统上线程实现比较低效(显著的异数是erlang或者别的任何基于stackless技术的vm),一般情况下给人低效的印象。而异步模式则需要充分深入到不同活动交互交错产生的行为序列里面去,对一般人来说是一个很大的头脑负担,但由于相当有成效的降低了一般来说很昂贵的资源——线程的数量,其性能一般能高很多。

Linker M Lin

unread,
Dec 19, 2007, 8:08:08 AM12/19/07
to TopLanguage
虽然多线程多进程模型被认为是不够高效的,但是像台湾ptt BBS, smth 的KBS 等都是 标准 fork() join()模型的.
而且,实际的情况是,PTT BBS长期支撑80K以上的用户并保持在可用的响应时间.
可以预计,在硬件线程越来越多的趋势下,(例如硬件线程数量超过32个的时候),多线程模型的效率也是可以接受的.


On 12月5日, 下午12时08分, lijie <cpun...@gmail.com> wrote:
> 并发这东西接触时间并不长,不过几乎让我完全推倒过去的编程方法。
>
> 并发系统的典型特点是大量同时存在的无规律活动,最具代表性的是网络应用,同时保持大量网络连接。过去我们常使用多进程、多线程方式来处理类似任务,优点是编程-容易,对多CPU的应用比较充分(虽然不一定最高效),缺点是并发能力有限,线程不是个可无限分配的资源。在过去相当长时间内多线程方式(主要指每连接一线程方-式)没有出现问题,我想原因是过去网络应用主要是以WEB为主,都是短连接,应用也不像今天这么广泛。
>
> 网络游戏服务器据我了解通常采用双进程方式,一个进程负责和其它服务器、客户端交互,另一进程负责游戏逻辑,这么设计的好处是把并发和游戏逻辑隔离开来,这对于-计算相对密集的应用是比较合适的。
> 网络游戏服务器由于并发部分和逻辑隔离得比较远,而且是以计算为主,所以我认为它不是并发应用的典型代表,*
> 我这里要讨论的并发应用是以并发IO操作为主要特征的应用。*
>
> 考虑另一种应用,网络硬盘、在线视频播放等应用,典型特点是长连接,低流量(相对服务器来说),大量并发活动。apache这样的架构支撑这种应用是很费力的,-慢客户占用大量进程,服务器负载可能并不很高但却无法处理更多并发连接。这种应用似乎可以使用epoll来单线程处理(Reactor模式),不过对于网络硬盘-这种和存储打交道的应用来说,fopen/fread/fwrite/fclose都是耗时操作,把这种操作放在一个线程里,硬盘繁忙时会怠慢所有连接。
>
> 完成端口/AIO或其它混合模式(比如IO操作丢进线程池,连接部分由主线程处理)也可以解决这种问题,不过问题是通常这种程序并不好写,根本原因是异步处理程-序本来就很难写,而且不符合思维习惯。如果每个请求是由多步操作组成的,中间就需要状态机来确定下一个异步操作该做什么。
>
> 所以异步程序通常就是在处理状态机。在复杂的应用里面,不但编写维护很困难,开发周期长,而且BUG最容易从这里滋生。
>
> *erlang*给了我很多启发,同样是异步操作,它的语法就不会改变思考方式,原因就是它把状态放在轻量级线程里面,我们可以*
> 像写单线程同步操作一样来写异步操作*
> ,实际执行的动作是由底层平台调度完成的。这和多线程写法和功能上很像似,但它可以减少IO等待,比如可以让send操作注册一个事件,当socket可写时把-数据写过去,并在完成时切换到当前"线程",在调用者看来如同一个阻塞操作。当然这并不是erlang的专利,windows上的fiber和unix/lin-ux上的ucontext也可以做类似的工作,如果我们能实现一整套基于伪线程的IO库。但并非没有缺点,使用C++实现时你得考虑fiber栈开多大,太大了-伪线程数量受限,太小了不能处理某些应用,这在某些动态语言里可以通过栈伸缩来实现,C/C++里实现这东西还是很困难。
>
> [或许有人认为写个网络硬盘程序不算什么,很多人都写过。不过我要提醒的是写个支持高并发、分段式操作的程序来说,erlang可能只要几十到几百行,c++我-没统计过,不过目前已经有个应用处理IO这部分至少有一千行。]
>
> 在C++里面如何实现高并发编程框架?这个我考虑过很长时间,感觉轻量级线程是唯一的出路,实际上就是erlang的实现原理。每个线程被创建出来就开始运行,-直到它调用yield主动切换出去。由于大量线程都处于非活动状态,可能正在等待READ事件,或者等待某个计时器超时,所以实际要调度的线程是很少的,这也是-并发系统的特点。调度部分只需要一个支持IO事件和计时器的东西就行,比如epoll,或者直接使用libevent。
>
> 我假想了这个框架的代码,是从erlang里面翻译过来的,框架正在实现过程中。
>
> ---------- CODE ---------
> void WINAPI loop(void* arg)
> {
> Process::Self()->ControlResource((Socket*)arg);
> Socket sock = *(Socket*)arg;
> while(true)
> {
> char lenstr[5];
> Must<int>(4) = sock.Recv(lenstr, 4, 0);
> lenstr[4] = '\0';
>
> int len = atoi(lenstr);
> char* p = new char[len + 1];
> Must<int>(len) = sock.Recv(p, len, 0);
> p[len] = '\0';
> std::cout << p << std::endl;
> delete[] p;
> }
>
> }
>
> void WINAPI server(void*)
> {
> Socket sock;
> Must<int>(0) = sock.Create(AF_INET, SOCK_STREAM, 0);
> Must<int>(0) = sock.Bind(2345, "0.0.0.0");
> Must<int>(0) = sock.Listen(1024);
> while(true)
> {
> Socket client = sock.Accept();
> spawn(&loop, &client);
> }
>
> }
>
> void WINAPI fmain(void*)
> {
> spawn(&server, Process::Self());
> receive();
>
> }
>
> int _tmain(int argc, _TCHAR* argv[])
> {
> spawn(&fmain, NULL);
> ProcessManager::Run();
>
> system("PAUSE");
> return 0;}
>
> ---------- CODE ---------
>
> 我自己感觉和erlang确实很像,细节上有些差别。
>
> spawn创建一个轻量级并切换过去执行。这里fmain作为这个框架的入口,上面的例子里,它创建了另一个线程server,并进入receive,这让它只-有收到消息才会退出。
>
> server线程打开一个socket,注意这是在栈上打开的,离开栈就会被destroy掉,不过放心,这里正是要利用RAII实现资源管理。它是一个死循环-,不断accept连接并创建出loop线程来处理。
>
> loop线程要做的第一件事就是把socket控制权拿到当前线程,这样如果这个线程异常退出时,可以自动关闭socket,以免资源泄漏。
>
> socket的各种耗时操作都只是注册事件并yield把当前线程切出去,当事件触发时它继续执行,实际上就是把状态机转换成轻量级线程了,因为栈上本来就可以-维持状态。
>
> 上面代码中的Must<int>(0)是模拟erlang中的match语法,如果返回值不匹配就抛出异常。
>
> 关于WINAPI,由于我目前是使用windows里的fiber来实现的,暂时直接使用fiber的入口函数。
>
> 暂时先想这么多了,有兴趣的不妨讨论下。

Alleluia

unread,
Jan 3, 2008, 12:53:13 AM1/3/08
to TopLanguage
我认为用异步方式编写网络程序的复杂性在于两点
1.程序员必须维护两种时序,一个是程序逻辑运行的时序,一个是网络消息收发的时序
2.不通过某种方法将其中一个时序规约掉,那么两种时序的叠加的后果是非线性的.

你一个程序拥有N个状态,这N个状态就代表了程序逻辑运行时序的N个时段.这时某一个网络消息到达,除非能够在逻辑上把这个消息限制在有限几个状态中,
否则你必须在所有状态上都处理这个消息.
程序逻辑时序和网络消息收发相比较,显然规约后者更简单.这就是Erlang在这个方面成功之处.
Reply all
Reply to author
Forward
0 new messages