完成端口通讯服务器(IOCP Socket Server)设计
(三)不要迷信API(单链表的另一种算法)
Copyright © 2009 代码客(卢益贵)版权所有
QQ:48092788 源码博客:http://blog.csdn.net/guestcode
用这个标题可能会牵强了点。只是因为在性能优化中遇到这样的事情,因此用来做标题而已,由此通过一个小事抛出本文介绍的内容:单链表的另一种算法。我本人也做了个ARM的OS内核,虽然及其简单,但是OS的机理大相径庭。操作系统要做到线程同步,需要进入中断(应用程序同步一般是软件中断)才能做到,在进入和退出这两个环节付出的代价相当昂贵的(事实上操作系统的分时处理机制(时钟中断)也在付出昂贵的代价,但那是不可避免的)。
有时候了解的越多,考虑的因素越复杂,做出的结果会越错误。正如上面所说的,线程同步需要使用临界会消耗巨大时间(比起非使用临界而言)。在做内存管理的释放函数的时候,有牛人告诉我,可以使用独立线程来处理释放工作,在释放函数里面给它发个消息就得了,比如使用PostThreadMessage一个函数就搞定了,又避免了使用临界。刚开始我一听,也感觉真的是很简单,避免了释放的时候释放函数做太多工作而造成堵塞。但随后一想,我向他提出了几个问题:“线程什么时候读取消息?隔1毫秒?隔10毫秒?会不会因为延时造成内存耗尽的错象?”他又给我推荐了SetEvent,用它及时告知Thread去处理释放工作。感觉好像“非常有道理”。
不过我没有立即去这么做,而是做了几个代码段去测试(实际对于一个资深的程序员来说根本不需要测试他们是否使用同步机制,本人只想:既然都是使用临界如果那种方式效率高的话还是可取的):
第一种情形:
dwTickCount = GetTickCount();
for(i = 0; i < 10000000; i++)
{
EnterCriticalSection(&csSection);
//在这里处理释放工作
LeaveCriticalSection(&csSection);
}
第二种情形:
dwTickCount = GetTickCount();
for(i = 0; i < 10000000; i++)
{
EnterCriticalSection(&csSection);
//在这里把释放的地址放入处理队列
LeaveCriticalSection(&csSection);
//告诉处理线程,你该工作了
SetEvent(hEvent);
}
dwTickCount = GetTickCount() - dwTickCount;
第三种情形:
dwTickCount = GetTickCount();
for(i = 0; i < 10000000; i++)
{
PostThreadMessage(dwThreadID, WM_USER, 0, 0);
//告诉处理线程,你该工作了
SetEvent(hEvent);
}
dwTickCount = GetTickCount() - dwTickCount;
MSG Msg;
while(PeekMessage(&Msg, 0, 0, 0, PM_REMOVE));
上面3种情形都有两个线程互相作用,测试结果是:
1、dwTickCount = 4281
2、dwTickCount = 17563
4、dwTickCount = 37297
尽管我的机器主板修了几次,巨慢无比,但在环境一样的前提下得出这么悬殊得结果,已经可以排除2、3种情形了(当然如果第1种情形中的释放处理工作消耗的时间远大于“2、3种情形减轻1种情形”的话就另当别论了;另外第3种情形连续投递这么多消息或多或少内核处理也会耗时)。与其这么折腾,还不如在自己的释放代码上下功能呢(源码可以看上一篇文章)。
通过这个小事,说明了一个问题,有时候在开发过程种,我们已经无意中过多的信任了API,总以为看到的代码仅是一行API而已,比起(显示式)使用临界来说效率是优秀的(这个错误在以往本人也曾犯过,是在编码的时候一时兴起导致“一念之差”的大意行为)。在多线程环境下,部分API是需要进入“临界”这样的机制来同步的,诸如:SendMessage和PostMessage,GetMessge和PeekMessage。
我们都希望想系统能够提供效率更高的API,来满足我们对服务器性能的需要,比如Windows提供了完成端口功能,是不是系统还有更有效的方式?!更有人想直接操作物理内存,甚至有人说:如果在驱动层上下功夫是不是效率更高?毫不隐讳的讲:我也有过这样大胆的想法。但在现在的条件下,我们指望API似乎已经没有多大希望。与其……不如优化我们的算法。下面我就介绍一个单向链表的另一种提高效率的算法。(由于本人信息闭塞,这个算法不知道是否已经有人发布过目前我尚未得知。)
在做IOCP服务器的时候,为了提高效率,我们采用内存池和连接池的方法以避免频繁向系统索要和归还内存造成系统内存更多的碎片(这种方式效率也是低的)。这个方法好是好,但内存池和连接池一般也都使用了临界来同步,同时对于一个数据区一般也习惯使用一个临界变量来达到同步目的,如果并发性高的话这种同步就会造成堵塞。能不能再提高一点效率以此来降低堵塞的可能性?
假如我们使用两个临界变量来同步一个单向链表会不会把堵塞几率降低一半呢?方法是这样的:单向链表采用后进后出的方式,一个临界负责链表头的同步,另一个临界负责链表尾的同步,但这样的前提是要保证这个链表不为空。
下面是根据上面设想以后的优化算法(后进后出):
PGIO_BUF GIodt_AllocGBuf(void)
/*说明:分配一个内存块GBuf,提供给业务层
**输入:无
**输出:内存块GIoData地址*/
{
//锁链表头
EnterCriticalSection(&GIoDataPoolHeadSection);
//确保链表至少有一个节点,pGIoDataPoolHead不为空,除非不初始化
//假如一个设计者考虑正常和异常情况能保证内存用之不尽的话,下面这个判断是多余,
//在以往的设计中本人就曾这么大胆过。
if(pGIoDataPoolHead->pNext)//和常规算法if(pGIoDataPoolHead)相比并没有多大开支
{
PGIO_BUF Result;
Result = (PGIO_BUF)pGIoDataPoolHead;
pGIoDataPoolHead = pGIoDataPoolHead->pNext;
dwGIoDataPoolUsedCount++;
LeaveCriticalSection(&GIoDataPoolHeadSection);
//为什么会这样返回:(char *)Result + sizeof(GIO_DATA_INFO),
//这也是优化算法的一种,将在以后介绍
return((char *)Result + sizeof(GIO_DATA_INFO));
}else
{
LeaveCriticalSection(&GIoDataPoolHeadSection);
return(NULL);
}
}
void GIodt_FreeGBuf(PGIO_BUF pGIoBuf)
/*说明:业务层归还一个内存块GBuf
**输入:内存块GIoData地址
**输出:无*/
{
//锁链表尾
EnterCriticalSection(&GIoDataPoolTailSection);
pGIoBuf = (char *)pGIoBuf - sizeof(GIO_DATA_INFO);
((PGIO_DATA)pGIoBuf)->pNext = NULL;
pGIoDataPoolTail->pNext = (PGIO_DATA)pGIoBuf;
pGIoDataPoolTail = (PGIO_DATA)pGIoBuf;
dwGIoDataPoolUsedCount--;
LeaveCriticalSection(&GIoDataPoolTailSection);
}
以下是常规的算法(先进先出):
PGIO_BUF GIodt_AllocGBuf(void)
/*说明:分配一个内存块GIoBuf,提供业务层
**输入:无
**输出:内存块GIoData地址*/
{
PGIO_BUF Result;
//锁链表
EnterCriticalSection(&GIoDataPoolSection);
Result = (PGIO_BUF)pGIoDataPoolHead;
if(pGIoDataPoolHead)
{
pGIoDataPoolHead = pGIoDataPoolHead->pNext;
dwGIoDataPoolUsedCount++;
}
LeaveCriticalSection(&GIoDataPoolSection);
return((char *)Result + sizeof(GIO_DATA_INFO));
}
void GIodt_FreeGBuf(PGIO_BUF pGIoBuf)
/*说明:业务层归还一个内存块GIoBuf
**输入:内存块GIoData地址
**输出:无*/
{
//锁链表
EnterCriticalSection(&GIoDataPoolSection);
pGIoBuf = (char *)pGIoBuf - sizeof(GIO_DATA_INFO);
((PGIO_DATA)pGIoBuf)->pNext = pGIoDataPoolHead;
pGIoDataPoolHead = (PGIO_DATA)pGIoBuf;
dwGIoDataPoolUsedCount--;
LeaveCriticalSection(&GIoDataPoolSection);
}
细心的读者应该会发现,为什么优化算法是这样:
if(pGIoDataPoolHead->pNext)
{
PGIO_BUF Result;
…
LeaveCriticalSection(&GIoDataPoolHeadSection);
return((char *)Result + sizeof(GIO_DATA_INFO));
}else
{
LeaveCriticalSection(&GIoDataPoolHeadSection);
return(NULL);
}
和这样的算法有什么区别(这样代码量会更少而又简洁):
PGIO_BUF Result;
if(pGIoDataPoolHead->pNext)
{
…
}else
Result = NULL;
LeaveCriticalSection(&GIoDataPoolHeadSection);
return(NULL);
这个疑问,只有看了汇编后的代码才能解决了:前面的方法少执行了一两句汇编代码(现在仅谈算法效率,以后有时间再谈代码效率)。
上述单向链表的算法仅为个人搓见,希望能得到牛人指点迷津,使得算法更加有效率。
.....