http://blog.csdn.net/lfhfut/article/details/1128759
不错的博客,服务器开发的重点
http://www.cnblogs.com/my_life/articles/5389009.html
在ACE的sample中,对数据包的处理都是在接收到数据后立即进行,而在实际的网络应用中,由于某些逻辑处理可能会占用比较长的时间,因此有可能会阻塞网络数据包的接收,导致对方发送数据失败。
解决的方法是将网络IO与逻辑处理相分离,分别在独立的线程中运行,使用消息队列来进行数据缓冲。这样即使某个数据包的处理时间过长,也不会影响到IO线程的数据接收。
当然,如果逻辑处理的效率一直低于IO,问题将会越来越严重,此时可以通过为消息队列设置高水位来阻塞IO线程的数据接收,或者使用线程池的方式进行并发的逻辑处理,提高逻辑处理的效率。
【线程池:如何保证同一个客户端的消息的顺序处理? 把不同的client hash到不同的逻辑线程上,今后来自该客户端的所有的消息都由该逻辑线程来处理,这就保证了顺序】
网络IO与逻辑处理分离的实现比较简单,从ACE_Task分别派生两个线程,一个作为网络IO,一个作为逻辑处理。从ACE_Task派生的对象自动拥有了ACE_Message_Queue消息队列,网络IO线程其实不需要消息队列,可以从ACE_Task_Base派生。
IO线程接收到的数据都放到逻辑线程的消息队列中,当然,头部要加上一个标识,用于指出是哪个客户端发来的数据包。
在逻辑线程中使用一个映射表来保存每个Socket Handler到其处理器的对应。逻辑线程从消息队列中取出数据包后,根据其头部的标识找出对应的处理器来处理该消息。
一个可能的网络线程类申明为:
class RealmNetwork_Thread : public ACE_Task_Base
{
public:
RealmNetwork_Thread();
virtual ~RealmNetwork_Thread();
virtual int svc();
int open();
int close();
private:
bool running_;
ACE_Proactor * proactor_;
};
逻辑处理线程类申明为:
class RealmLogic_Thread : public ACE_Task<ACE_MT_SYNCH>
{
public:
RealmLogic_Thread();
virtual ~RealmLogic_Thread();
virtual int svc();
int open();
int close();
private:
bool running_;
typedef std::map<unsigned int, Realm_Client_Service *> ClientServiceMap;
ClientServiceMap client_service_map_;
};
网络IO线程的实现使用 创建一个可正常结束的Proactor服务器 中的方法,只是在handle_read_stream()中收到完整的数据包后要使用putq()方法将该数据包发送到逻辑线程的消息队列中,另外在连接建立和连接关闭时也要通知逻辑线程,以进行相应的映射建立和删除操作。
逻辑线程的实现就是循环地从消息队列中取数据包并执行相应的处理即可。
一个应用的实例见 WOW用户认证的方式-SRP 中的服务器。
=========================================================
http://blog.csdn.net/blade2001/article/details/4370823
双缓冲消息队列-减少锁竞争
在网络应用服务器端, 为了性能和防止阻塞, 经常会把逻辑处理和I/O处理分离:
I/O网络线程处理I/O事件: 数据包的接收和发送, 连接的建立和维护等.
逻辑线程要对收到的数据包进行逻辑处理.
通常网络线程和逻辑线程之间是通过数据包队列来交换信息, 简单来说就是一个生产者-消费者模式.
这个队列是多个线程在共享访问必须加锁, 意味着每次访问都要加锁。如何更好的如何减少锁竞争次数呢 ?
方案一 双缓冲消息队列:
两个队列,一个给逻辑线程读,一个给IO线程用来写,当逻辑线程读完队列后会将自己的队列与IO线程的队列相调换。
IO线程每次写队列时都要加锁,逻辑线程在调换队列时也需要加锁,但逻辑线程在读队列时是不需要加锁的.
队列缓冲区的大小要根据数据量的大小进行调整的,如果缓冲区很小,就能更及时的处理数据,但吞吐量以及出现资源竞争的几率大多了。
可以给缓冲队列设置最大上限,超过上限的数量之后,将包丢弃不插入队列。
另外,双缓冲的实现也有不同策略的,
一是读操作优先,就是生产者只要发现空闲缓冲,马上swap,
二是写线程只有在当前的缓冲区写满了,才进行swap操作。
三是上层逻辑按照帧率来处理,每一帧的时候将双层缓冲队列调换一下,取一个队列来处理即可.
方案二 提供一个队列容器:
提供一个队列容器,里面有多个队列,每个队列都可固定存放一定数量的消息。网络IO线程要给逻辑线程投递消息时,会从队列容器中取一个空队列来使用,
直到将该队列填满后再放回容器中换另一个空队列。而逻辑线程取消息时是从队列容器中取一个有消息的队列来读取,处理完后清空队列再放回到容器中。
这样便使得只有在对队列容器进行操作时才需要加锁,而IO线程和逻辑线程在操作自己当前使用的队列时都不需要加锁,所以锁竞争的机会大大减少了。
这里为每个队列设了个最大消息数,看来好像是打算只有当IO线程写满队列时才会将其放回到容器中换另一个队列。那这样有时也会出现IO线程未写满一
个队列,而逻辑线程又没有数据可处理的情况,特别是当数据量很少时可能会很容易出现
[这个可以通过设置超时来处理, 如果当前时间-向队列放入第一个包的时间> 50 ms, 就将其放回到容器中换另一个队列]。
方案3: Netty或muduo的方式,每个客户端维护自己的发送接收队列 ------------------我添加的
https://www.cnblogs.com/my_life/articles/10937669.html
有数据要发送,注册可写事件
通常我们逻辑服务器会以场景来划分线程,不同线程执行不同场景.一个线程可以执行多个场景.因为玩家属于场景,我们会把玩家数据,包括其缓冲池丢给场景 去处理.
Ref link: http://groups.google.com/group/dev4server/browse_thread/thread/4655f8ab1248347a?hl=zh-CN
这一段在做网关的设计,有一个相信初学者容易疑惑的地方就是多工作线程下是否存在数据包乱序问题.查阅了一下文档资料.有这么一句话: The CreateIoCompletionPort function associates an I/O completion port with one or more file handles. When an asynchronous I/O operation started on a file handle associated with a completion port is completed, an I/O completion packet is queued to the port. CreateIoCompletionPort函数会使一个I/O完成端口与一个或多个文件句柄发生关联。当与一个完成端口相关的文件句柄上启动的异步 I/O操作完成时,一个I/O完成包就会进入到该完成端口的队列中。 比如说接收数据,当我们未曾调用WSARecv()这样的接收数据之前,不会发生完成操作.这样就保证了我们数据在接收完成前的序列化,这样我们可以为 每一个连接定义相应的收发缓冲区保证序列的严格执行,在进行接收操作前对该缓冲区进行一些,比如说移动缓冲区指针,统计流量等操作.最后一步才进行 read操作. 对于IOCP我们会定义足够多的工作线程去处理IO完成事件,这就会涉及执行逻辑与工作线程之间的资源竞争.逻辑线程要对数据包进行逻辑处理.如何更好 的如何减少锁竞争次数,我参考了一下网上的一些方法.下面这些引自网络: "对于如何减少锁竞争次数的优化方案,Ghost Cheng提出了一种。提供一个队列容器,里面有多个队列,每个队列都可固定存放一定数量的消息。网 络IO线程要给逻辑线程投递消息时,会从队列容器中取一个空队列来使用,直到将该队列填满后再放回容器中换另一个空队列。而逻辑线程取消息时是从队列容 器中取一个有消息的队列来读取,处理完后清空队列再放回到容器中。 这样便使得只有在对队列容器进行操作时才需要加锁,而IO线程和逻辑线程在操作自己当前使用的队列时都不需要加锁,所以锁竞争的机会大大减少了。 这里为每个队列设了个最大消息数,看来好像是打算只有当IO线程写满队列时才会将其放回到容器中换另一个队列。那这样有时也会出现IO线程未写满一 个队列,而逻辑线程又没有数据可处理的情况,特别是当数据量很少时可能会很容易出现。Ghost Cheng在他的描述中没有讲到如何解决这种问题,但 我们可以先来看看另一个方案。 这个方案与上一个方案基本类似,只是不再提供队列容器,因为在这个方案中只使用了两个队列,arthur在他的一封邮件中描述了这个方案的实现及部 分代码。两个队列,一个给逻辑线程读,一个给IO线程用来写,当逻辑线程读完队列后会将自己的队列与IO线程的队列相调换。所以,这种方案下加锁的次数 会比较多一些,IO线程每次写队列时都要加锁,逻辑线程在调换队列时也需要加锁,但逻辑线程在读队列时是不需要加锁的。" 思考了一下,对于IOCP我们应该尽量避免的是阻塞发生在工作线程,当我们所有工作线程对同一资源操作的时候会带来更多工作线程的等待.我提出下面一种 方案,是基于1个原则,就是CPU的处理速度远远大于网络IO速度.这样我倾向于为每一个客户端的连接建立一个自己的队列,暂时定义为.128个数据包 上限.工作线程处理的时候仅仅会锁住该连接的接收队列(发送下面会讲到),逻辑处理线程会对每个连接的接收队列轮循.每个连接队列每次最大处理12个数 据包. 当缓冲区将要溢出或队列满的时候我会认为该客户端连接非法 对于发送数据,会在IOCP上屏蔽发送完成包.使其不响应发送完成事件. 不知各位大侠有没有认同的. :) Kouga 09/7/19 嘛~那种队列和咱马蜂窝似的水桶异曲同工啊~喵~ 既然用了IOCP,就不要阻塞!那样效率会高很多的~ 2009/7/18 liam <liam...@hotmail.com> - 显示引用文字 - -- 签名是什么东西?? 寒冰千醉 09/7/19 不阻塞当然是最理想的, 我见到过一种设计是: 一个IOCP端口,一个工作者线程方式,就不用加锁,但这只是工作线程之间。 但接收数据时,把IOCP的工作线程与应用层线程分开(比如查数据库等等), IOCP工作线程相当于生产者,向队列里放接收到的数据包。 有一个专门的检查线程,向队列里取数据。 这里的队例是多个线程在共享访问必须加锁。 Kouga 09/7/19 恩,被访问的队列必须要上锁,但是可以使用几个队列循环使用,接收线程和工作线程轮流去锁队列即可。 简单的就比如swap下的双队列,由工作线程进行安全的翻转即可。 2009/7/19 张晓衡 <zxh1...@gmail.com> - 显示引用文字 - -- 签名是什么东西?? 寒冰千醉 09/7/19 我有一个问题,就是从队列从取消息时,我只使用了一个线程。 有没有必要使用多个线程呢? 有时候上层业务很复杂,怕一个线程忙不过来。 如果是多个线程,怎么处理消息顺序问题呢?因为有些线程会执行的快些,有些会慢些,并不是先从队列中取出消息的线程先完成任务。 我当时为了完成任务没有去细想,不知道大家有什么好方法,使用多个线程来取数据,又要保证处理消息顺序不乱。 z_kris 09/7/20 第二种方法实际上就是双缓冲,操作系统底层也有使用,实际上如果实现得不错的话,是一种比较好比较成熟的方案了。 缓冲区的大小要根据数据量的大小进行调整的,如果缓冲区很小,就能更及时的处理数据,但吞吐量以及出现资源竞争的几率大多了。另外,双缓冲的实现也有不 同策略的,一是读操作优先,就是生产者只要发现空闲缓冲,马上swap,二是你说的,写线程只有在当前的缓冲区写满了,才进行swap操作。两种策略的 优劣需要自己仔细权衡。 - 显示引用文字 - Nicolas Tian 09/7/20 对于双队列调换(双缓冲)方案,可不可以这样理解:如果认为网络IO的速度大于逻辑处理的速度,在调换时机上应该读操作优先,即当逻辑线程读完自己的队 列后就会将自己的队列与IO线程的队列相调换;如果认为逻辑处理的速度大于网络IO的速度,在调换机制上应该是写操作优先,即当IO线程将自己的队列写 满了后才会将自己的队列与逻辑线程的队列相调换。简单来说,就是“谁慢谁是大爷”。 另外,楼主想讨论的其实是自己的方案的可行性吧: “对于IOCP我们应该尽量避免的是阻塞发生在工作线程,当我们所有工作线程对同一资源操作的时候会带来更多工作线程的等待。我提出下面一种方案,是基 于一个原则,就是CPU的处理速度远远大于网络IO速度。这样我倾向于为每一个客户端的连接建立一个自己的队列(暂时定义为128个数据包上限)工作线 程处理的时候仅仅会锁住该连接的接收队列(发送下面会讲到),逻辑处理线程会对每个连接的接收队列轮循。每个连接队列每次最大处理12个数据包。当缓冲 区将要溢出或队列满的时候我会认为该客户端连接非法。对于发送数据,会在IOCP上屏蔽发送完成包,使其不响应发送完成事件。” 没有经验,不敢乱放,顶出来等高手解答。 - 显示引用文字 - 关中刀客 09/7/20 上层逻辑按照帧率来处理,每一帧的时候将双层缓冲队列调换一下,取一个队列来处理即可。一般不会出现逻辑慢过io吧,可以给缓冲队列设置最大上限,超过 上限的数量之后,将包丢弃不插入队列。 - 显示引用文字 - > > > 不知各位大侠有没有认同的. :)- 隐藏被引用文字 - > > - 显示引用的文字 - liam 09/7/20 呵呵,自己想的方案.其实对于上面每一种都能成为一个成功案例,只不过是承载人数多少问题.想找到一个最优解.我期望能支持到8K以上的并发连接量. 前面做的服务器是没有网关.在尝试. On 7月20日, 下午4时39分, Nicolas Tian <pillgr...@vip.sina.com> wrote: - 显示引用文字 - > > > 不知各位大侠有没有认同的. :)- 隐藏被引用文字 - > > - 显示引用的文字 - liam 09/7/20 对于MMorpg类型来讲,我们针对每一个客户端数据池单线程执行.对于所有连接来讲由多个线程处理.这里不用考虑真正意义上的数据包顺序,因为网络也 是未定的 通常我们逻辑服务器会以场景来划分线程,不同线程执行不同场景.一个线程可以执行多个场景.因为玩家属于场景,我们会把玩家数据,包括其缓冲池丢给场景 去 处理. - 显示引用文字 - avalon 09/7/21 其实异步io模型就是普通的windows编程模型,用一个thread来getmessage ,然后dispatch出去,至于handler,可以用多线程,也可以一个线程,甚至多进程,这取决于逻辑需要了。 Michael 09/8/11 我的想法和Nicolas相反,是"谁快谁是大爷"。如果逻辑处理速度大于网络IO速度,当逻辑线程处理完自己的队列后同IO的队列调换;如果网络IO 速度大于逻辑处理速度,当IO队列满之后同逻辑线程队列调换。
===========================================================
http://www.cnblogs.com/my_life/articles/5340365.html
http://yaocoder.blog.51cto.com/2668309/1374280 很好的博客,可按部就班的学习
===============================
http://www.10tiao.com/html/335/201411/201788569/1.html
http://justdo2008.iteye.com/blog/1936795
http://www.cnblogs.com/tianzhiliang/archive/2010/10/28/1863684.html Socket服务器整体架构概述
https://my.oschina.net/u/1859679/blog/1438724 王者荣耀架构
https://it.zuocheng.net/tier-architecture-summary-zh
http://www.cnblogs.com/meibenjin/p/3604389.html
google 搜索: 网络io 逻辑 分离
网络io 业务逻辑 分离
http://www.parallellabs.com/2010/10/25/practical-concurrent-queue-algorithm/
多线程队列(Concurrent Queue)的使用场合非常多,高性能服务器中的消息队列,并行算法中的Work Stealing等都离不开它。对于一个队列来说有两个最主要的动作:添加(enqueue)和删除(dequeue)节点。在一个(或多个)线程在对一个队列进行enqueue操作的同时可能会有一个(或多个)线程对这个队列进行dequeue操作。因为enqueue和dequeue都是对同一个队列里的节点进行操作,为了保证线程安全,一般在实现中都会在队列的结构体中加入一个队列锁(典型的如pthread_mutex_t q_lock),在进行enqueue和dequeue时都会先锁住这个锁以锁住整个队列然后再进行相关的操作。这样的设计如果实现的好的话一般性能就会很不错了。以链表实现的队列的结构体一般是这样的:
01
02
03
04
05
|
struct queue_t { node_t *head; node_t *tail; pthread_mutex_t q_lock; }; |
但是,这其中其实有一个潜在的性能瓶颈:enqueue和dequeue操作都要锁住整个队列,这在线程少的时候可能没什么问题,但是只要线程数一多,这个锁竞争所产生的性能瓶颈就会越来越严重。那么我们可不可以想办法优化一下这个算法呢?当然可以!如果我们仔细想一想enqueue和dequeue的具体操作就会发现他们的操作其实不一定是冲突的。例如:如果所有的enqueue操作都是往队列的尾部插入新节点,而所有的dequeue操作都是从队列的头部删除节点,那么enqueue和dequeue大部分时候都是相互独立的,我们大部分时候根本不需要锁住整个队列,白白损失性能!那么一个很自然就能想到的算法优化方案就呼之欲出了:我们可以把那个队列锁拆成两个:一个队列头部锁(head lock)和一个队列尾部锁(tail lock)。这样这样的设计思路是对了,但是如果再仔细思考一下它的实现的话我们会发现其实不太容易,因为有两个特殊情况非常的tricky(难搞):第一种就是往空队列里插入第一个节点的时候,第二种就是从只剩最后一个节点的队列中删除那个“最后的果实”的时候。
为什么难搞呢?当我们向空队列中插入第一个节点的时候,我们需要同时修改队列的head和tail指针,使他们同时指向这个新插入的节点,换句话说,我们此时即需要拿到head lock又需要拿到tail lock。而另一种情况是对只剩一个节点的队列进行dequeue的时候,我们也是需要同时修改head和tail指针使他们指向NULL,亦即我们需要同时获得head和tail lock。有经验的同学会立刻发现我们进入危险区了!是什么危险呢?死锁!多线程编程中最臭名昭著的一种bug就是死锁了。例如,如果线程A在锁住了资源1后还想要获取资源2,而线程B在锁住了资源2后还想要获取资源1,这时两个线程谁都不能获得自己想要的那个资源,两个线程就死锁了。所以我们要小心奕奕的设计这个算法以避免死锁,例如保证enqueue和dequeue对head lock和tail lock的请求顺序(lock ordering)是一致的等等。但是这样设计出来的算法很容易就会包含多次的加锁/解锁操作,这些都会造成不必要的开销,尤其是在线程数很多的情况下反而可能导致性能的下降。我的亲身经历就是在32线程时这个思路设计出来的算法性能反而下降了10%左右,原因就是加锁/解锁的开销增加了。
好在有聪明人早在96年就想到了一个更妙的算法。这个算法也是用了head和tail两个锁,但是它有一个关键的地方是它在队列初始化的时候head和tail指针不为空,而是指向一个空节点。在enqueue的时候只要向队列尾部添加新节点就好了。而dequeue的情况稍微复杂点,它要返回的不是头节点,而是head->next,即头节点的下一个节点。先来看伪代码:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
typedef struct node_t { TYPE value; node_t *next } NODE; typedef struct queue_t { NODE *head; NODE *tail; LOCK q_h_lock; LOCK q_t_lock; } Q; initialize(Q *q) { node = new_node() // Allocate a free node node->next = NULL // Make it the only node in the linked list q->head = q->tail = node // Both head and tail point to it q->q_h_lock = q->q_t_lock = FREE // Locks are initially free } enqueue(Q *q, TYPE value) { node = new_node() // Allocate a new node from the free list node->value = value // Copy enqueued value into node node->next = NULL // Set next pointer of node to NULL lock(&q->q_t_lock) // Acquire t_lock in order to access Tail q->tail->next = node // Link node at the end of the queue q->tail = node // Swing Tail to node unlock(&q->q_t_lock) // Release t_lock } dequeue(Q *q, TYPE *pvalue) { lock(&q->q_h_lock) // Acquire h_lock in order to access Head node = q->head // Read Head new_head = node->next // Read next pointer if new_head == NULL // Is queue empty? unlock(&q->q_h_lock) // Release h_lock before return return FALSE // Queue was empty endif *pvalue = new_head->value // Queue not empty, read value q->head = new_head // Swing Head to next node unlock(&q->q_h_lock) // Release h_lock free (node) // Free node return TRUE // Queue was not empty, dequeue succeeded } |
发现玄机了么?是的,这个算法中队列总会包含至少一个节点。dequeue每次返回的不是头节点,而是头节点的下一个节点中的数据:如果head->next不为空的话就把这个节点的数据取出来作为返回值,同时再把head指针指向这个节点,此时旧的头节点就可以被free掉了。这个在队列初始化时插入空节点的技巧使得enqueue和dequeue彻底相互独立了。但是,还有一个小地方在实现的时候需要注意:对第一个空节点的next指针的读写。想象一下,当一个线程对一个空队列进行第一次enqueue操作时刚刚运行完第25行的代码(对该空节点的next指针进行写操作);而此时另一个线程对这个队列进行第一次dequeue操作时恰好运行到第33行(对该空节点的next指针进行读操作),它们其实还是有冲突!不过,好在一般来讲next指针是32位数据,而现代的CPU已经能保证多线程程序中内存对齐了的32位数据读写操作的原子性,而一般来讲编译器会自动帮你对齐32位数据,所以这个不是问题。唯一需要注意的是我们要确保enqueue线程是先让要添加的新节点包含好数据再把新节点插入链表(也就是不能先插入空节点,再往节点中填入数据),那么dequeue线程就不会拿到空的节点。其实我们也可以把q_t_lock理解成生产者的锁,q_h_lock理解成消费者的锁,这样生产者(们)和消费者(们)的操作就相互独立了,只有在多个生产者对同一队列进行添加操作时,以及多个消费者对同一队列进行删除操作时才需要加锁以使访问互斥。
通过使用这个算法,我成功的把一个32线程程序的性能提升了11%!可见多线程中的锁竞争对性能影响之大!此算法出自一篇著名的论文:M. Michael and M. Scott. Simple, Fast, and Practical Non-Blocking and Blocking Concurren Queue Algorithms. 如果还想做更多优化的话可以参考这篇论文实现相应的Non Blocking版本的算法,性能还能有更多提升。当然了,这个算法早已被集成到java.util.concurrent里了(即LinkedBlockingQueue),其他的并行库例如Intel的TBB多半也有类似的算法,如果大家能用上现成的库的话就不要再重复造轮子了。为什么别造并行算法的轮子呢?因为高性能的并行算法实在太难正确地实现了,尤其是Non Blocking,Lock Free之类的“火箭工程”。有多难呢?Doug Lea提到java.util.concurrent中一个Non Blocking的算法的实现大概需要1年的时间,总共约500行代码。所以,对最广大的程序员来说,别去写Non Blocking, Lock Free的代码,只管用就行了,我看见网上很多的Non Blocking阿,无锁编程的算法实现啊什么的都非常地害怕,谁敢去用他们贴出来的这些代码啊?我之所以推荐这个two lock的算法是因为它的实现相对Non Blocking之类的来说容易多了,非常具备实用价值。虽然这篇论文出现的很早,但是我在看了几个开源软件中多线程队列的实现之后发现他们很多还是用的本文最开始提到的那种一个锁的算法。如果你想要实现更高性能的多线程队列的话,试试这个算法吧!
Update: 多线程队列算法有很多种,大家应根据不同的应用场合选取最优算法(例如是CPU密集型还是IO密集型)。本文所列的算法应用在这样一个多线程程序中:每个线程都拥有一个队列,每个队列可能被本线程进行dequeue操作,也可以被其他线程进行dequeue(即work stealing),线程数不超过CPU核心数,是一个典型的CPU/MEM密集型客户端单写者多读者场景。