服务端编程中多线程的应用

本文探讨了多线程在服务端编程中的应用,分析了单线程和多线程的优缺点。重点介绍了单线程服务器的Reactor模式,以及多线程服务器的常见编程模型,如线程池和one loop per thread模式。文章强调了多线程主要是为了发挥多核处理器的效能,同时提到了线程安全和进程间通信的问题,指出多线程适用于提高响应速度和降低延迟的场景。最后,文章讨论了线程分类和线程池的使用策略,并给出了多线程在并发和吞吐量上的考量。
摘要由CSDN通过智能技术生成

         本文是陈硕的《Linux多线程服务端编程  使用muduo C++网络库》一书中,第三章的读书笔记。其中暗红颜色的文字是自己的理解,鲜红颜色的文字表示原书中需要注意的地方。

 

一:进程和线程

         每个进程有自己独立的地址空间。“在同一个进程”还是“不在同一个进程”是系统功能划分的重要决策点。《Erlang程序设计》[ERL]把进程比喻为人:

         每个人有自己的记忆(内存),人与人通过谈话(消息传递)来交流,谈话既可以是面谈(同一台服务器),也可以在电话里谈(不同的服务器,有网络通信)。面谈和电话谈的区别在于,面谈可以立即知道对方是否死了(crash,SIGCHLD),而电话谈只能通过周期性的心跳来判断对方是否还活着。

         有了这些比喻,设计分布式系统时可以采取“角色扮演”,团队里的几个人各自扮演一个进程,人的角色由进程的代码决定(管登录的、管消息分发的、管买卖的等等)。每个人有自己的记忆,但不知道别人的记忆,要想知道别人的看法,只能通过交谈(暂不考虑共享内存这种IPC)。然后就可以思考:

         ·容错:万一有人突然死了

         ·扩容:新人中途加进来

         ·负载均衡:把甲的活儿挪给乙做

         ·退休:甲要修复bug,先别派新任务,等他做完手上的事情就把他重启

等等各种场景,十分便利。

 

         线程的特点是共享地址空间,从而可以高效地共享数据。一台机器上的多个进程能高效地共享代码段(操作系统可以映射为同样的物理内存),但不能共享数据。如果多个进程大量共享内存,等于是把多进程程序当成多线程来写,掩耳盗铃。

         “多线程”的价值,我认为是为了更好地发挥多核处理器(multi-cores)的效能。在单核时代,多线程没有多大价值(个人想法:如果要完成的任务是CPU密集型的,那多线程没有优势,甚至因为线程切换的开销,多线程反而更慢;如果要完成的任务既有CPU计算,又有磁盘或网络IO,则使用多线程的好处是,当某个线程因为IO而阻塞时,OS可以调度其他线程执行,虽然效率确实要比任务的顺序执行效率要高,然而,这种类型的任务,可以通过单线程的”non-blocking IO+IO multiplexing”的模型(事件驱动)来提高效率,采用多线程的方式,带来的可能仅仅是编程上的简单而已)。Alan Cox说过:”A computer is a state machine.Threads are for people who can’t program state machines.”(计算机是一台状态机。线程是给那些不能编写状态机程序的人准备的)如果只有一块CPU、一个执行单元,那么确实如Alan Cox所说,按状态机的思路去写程序是最高效的。

 

二:单线程服务器的常用编程模型

         据我了解,在高性能的网络程序中,使用得最为广泛的恐怕要数”non-blocking IO + IO multiplexing”这种模型,即Reactor模式。

         在”non-blocking IO + IO multiplexing”这种模型中,程序的基本结构是一个事件循环(event loop),以事件驱动(event-driven)和事件回调的方式实现业务逻辑:

//代码仅为示意,没有完整考虑各种情况
while(!done)
{
    int timeout_ms = max(1000, getNextTimedCallback());
    int retval = poll(fds, nfds, timeout_ms);
    if (retval<0){
        处理错误,回调用户的error handler
    }else{
        处理到期的timers,回调用户的timer handler
        if(retval>0){
        处理IO事件,回调用户的IO event handler
        }
    }
}

         这里select(2)/poll(2)有伸缩性方面的不足(描述符过多时,效率较低),Linux下可替换为epoll(4),其他操作系统也有对应的高性能替代品。

         Reactor模型的优点很明显,编程不难,效率也不错。不仅可以用于读写socket,连接的建立(connect(2)/accept(2)),甚至DNS解析都可以用非阻塞方式进行,以提高并发度和吞吐量(throughput),对于IO密集的应用是个不错的选择。lighttpd就是这样,它内部的fdevent结构十分精妙,值得学习。

         基于事件驱动的编程模型也有其本质的缺点,它要求事件回调函数必须是非阻塞的。对于涉及网络IO的请求响应式协议,它容易割裂业务逻辑,使其散布于多个回调函数之中,相对不容易理解和维护。

 

三:多线程服务器的常用编程模型

         大概有这么几种:

         a:每个请求创建一个线程,使用阻塞式IO操作。在Java 1.4引人NIO之前,这是Java网络编程的推荐做法。可惜伸缩性不佳(请求太多时,操作系统创建不了这许多线程)。

         b:使用线程池,同样使用阻塞式IO操作。与第1种相比,这是提高性能的措施。

         c:使用non-blocking IO + IO multiplexing。即Java NIO的方式。

         d:Leader/Follower等高级模式。

         在默认情况下,我会使用第3种,即non-blocking IO + one loop per thread模式来编写多线程C++网络服务程序。

 

1:one loop per thread

         此种模型下,程序里的每个IO线程有一个event  loop,用于处理读写和定时事件(无论周期性的还是单次的)。代码框架跟“单线程服务器的常用编程模型”一节中的一样。

         libev的作者说:

         One loop per thread is usually a good model. Doing this is almost never wrong, some times a better-performance model exists, but it is always a good start.

 

         这种方式的好处是:

         a:线程数目基本固定,可以在程序启动的时候设置,不会频繁创建与销毁。

         b:可以很方便地在线程间调配负载。

         c:IO事件发生的线程是固定的,同一个TCP连接不必考虑事件并发。

 

         Event loop代表了线程的主循环,需要让哪个线程干活,就把timer或IO channel(如TCP连接)注册到哪个线程的loop里即可:对实时性有要求的connection可以单独用一个线程;数据量大的connection可以独占一个线程,并把数据处理任务分摊到另几个计算线程中(用线程池);其他次要的辅助性connections可以共享一个线程。

         比如,在dbproxy中,一个线程用于专门处理客户端发来的管理命令;一个线程用于处理客户端发来的mysql命令,而与后端数据库通信执行该命令时,是将该任务分配给所有事件线程处理的。

 

         

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值