在所有的预处理指令中,#pragma指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作。#pragma指令对每个 编译器给出了一个方法,在保持与C和C++语言完全兼容的情况下,给出主机或操作系统专有的特征。依据定义,编译指示是机器或操作系统专有的,且对于每个 编译器都是不同的。其格式一般为:#pragma para,其中para为参数,下面来看一些常用的参数。
(Each implementation of C and C++ supports some features unique to its host machine or operating system. Some programs, for instance, need to exercise precise control over the memory areas where data is placed or to control the way certain functions receive parameters. The #pragma directives offer a way for each compiler to offer machine- and operating-system-specific features while retaining overall compatibility with the C and C++ languages. Pragmas are machine- or operating-system-specific by definition, and are usually different for every compiler. )
(1)message参数。Message参数是我最喜欢的一个参数,它能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:
#pragma message(“消息文本”),当编译器遇到这条指令时就在编译输出窗口中将消
息文本打印出来。
当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有正确的设置这些宏,此时我们可以用这条指令在编译的时候就进行检查。假设我们希望判断自己有没有在源代码的什么地方定义了_X86这个宏可以用下面的方法:
#ifdef _X86
#pragma message(“_X86 macro activated!”)
#endif
当我们定义了_X86这个宏以后,应用程序在编译时就会在编译输出窗口里显示
“_X86 macro activated!”。我们就不会因为不记得自己定义的一些特定的宏而抓耳
挠腮了。
(2)另一个使用得比较多的pragma参数是code_seg。格式如:
#pragma code_seg( ["section-name"[,"section-class"] ] )
它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它。
(3)#pragma once (比较常用)。只要在头文件的最开始加入这条指令就能够保证
头文件被编译一次,这条指令实际上在VC6中就已经有了,但是考虑到兼容性并没有太多的使用它。
(4)#pragma hdrstop表示预编译头文件到此为止,后面的头文件不进行预编译。BCB
可以预编译头文件以加快链接的速度,但如果所有头文件都进行预编译又可能占太多磁盘空间,所以使用这个选项排除一些头文件。有时单元之间有依赖关系,比如单元A依赖单元B,所以单元B要先于单元A编译。你可以用#pragma startup指定编
译优先级,如果使用了#pragma package(smart_init),BCB就会根据优先级的大小先
后编译。
(5)#pragma resource "*.dfm"表示把*.dfm文件中的资源加入工程。*.dfm中包括窗
体外观的定义。
(6)#pragma warning(disable : 4507 34; once : 4385; error : 164 ) 等价于:
#pragma warning(disable:4507 34) // 不显示4507和34号警告信息
#pragma warning(once:4385) // 4385号警告信息仅报告一次
#pragma warning(error:164) // 把164号警告信息作为一个错误。
同时这个pragma warning 也支持如下格式:
#pragma warning( push [ ,n ] )
#pragma warning( pop )
这里n代表一个警告等级(1---4)。
#pragma warning( push )保存所有警告信息的现有的警告状态。
#pragma warning( push, n)保存所有警告信息的现有的警告状态,并且全局警告
等级设定为n。
#pragma warning( pop )向栈中弹出最后一个警告信息,在入栈和出栈之间所作
的一切改动取消。例如:
#pragma warning( push )
#pragma warning( disable : 4705 )
#pragma warning( disable : 4706 )
#pragma warning( disable : 4707 )
//.......
#pragma warning( pop )
在这段代码的最后,重新保存所有的警告信息(包括4705,4706和4707)。
(7)#pragma comment(...) 该指令将一个注释记录放入一个对象文件或可执行
文件中。常用的lib关键字,可以帮我们连入一个库文件。
(8)#pragma pack() 我们知道在VC中,对于想结构体Struct这样的类型,VC采
用8字节对齐的方式,如果我们不想使用8字节对齐(在网络变成中经常需要这样),我们可以在结构体前面加上
#pragma pack(1)
struct
{
......
}
#pragma pack()
二.#if _MSC_VER > 1000 #pragma once #endif
(1)_MSC_VER。 Defines the compiler version. Defined as 1200 for Microsoft Visual C++ 6.0. Always defined.
(2)#if _MSC_VER > 1000的意思是指如果vc编译器的版本大于1000则这个语句
被编译!大概小于1000的版本不支持#pragma once这个语句。
(3)#pragma once 。Specifies that the file, in which the pragma resides,
will be included (opened) only once by the compiler in a build. A common use for this pragma is the following:
//header.h
#pragma once
// Your C or C++ code would follow:
#pragma once 加入头文件的第一行 指示这个文件在编译时只被编译器文件编译
(打开)一次!一般用到.h中防止文件被重复包括!
三.#pragma once 与 #ifndef #define #endif
(1)从定义上即可看出,pragmas指令是某种机器或者操作系统独有的,并且不同编译器也常常有别。#pragma once这个是编译器相关指令,就是说在这个编译系统
上能用,但是在其他编译系统 不一定型,也就是说移植型差。不过现在基本上
已经是每个编译器都有这个定义了。
#ifndef #define #endif这个是语言支持指令,这是C/C++语言中的宏定义,通过
宏定义避免文件多次编译。所以在所有支持C++语言的编译器上都是有效的。如果写的程序要 跨平台,最好使用这种方式。
(2)#ifndef #define #endif #ifndef 还有其它作用,防止头文件重复引用只是
其中一个应用而已。#pragma只有微软支持。
(3)#ifndef #define #endif 他读到#ifndef之后,如果已经定义过了,就会跳过
这一大片,一直到#endif为止。这将增加build时间,因为每次compiler都会打开这个文件,然后搜索全文件一遍。而如果碰到了#pragma once,他就会立刻停止,
关闭打开的这个文件。在某种程度上减少 了build时间。一般用法:
#ifndef
#define
#pragma once
.....
#endif
四. #pragma data_seg(".mdata").....#pragma data_seg()可以让编译器把两者之间
的所有已初始化变量放入一个新的.mdata段中。应用之一是单应用程序。
有的时候我们可能想让一个应用程序只启动一次,就像单件模式(singleton)一样,实现的方法可能有多种,这里说说用#pragma data_seg的实现,很是简洁便利。
应用程序的入口文件前面加上:
#pragma data_seg("flag_data")
int app_count = 0;
#pragma data_seg()
#pragma comment(linker,"/SECTION:flag_data,RWS")
然后程序启动的地方加上
if(app_count>0) // 如果计数大于0,则退出应用程序。
{
//MessageBox(NULL, "已经启动一个应用程序", "Warning", MB_OK);
//printf("no%d application", app_count);
return FALSE;
} app_count++;
总结:
1. #ifndef 由语言支持所以移植性好,#pragma 可以避免名字冲突
2. 调查一下<stdlib.h>和<iostream>等标准库, 用得都是#ifndef, 我个人推荐这种方式.
涉及到事件分享器的两种模式称为:Reactor and Proactor [1]. Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的. 在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离者就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
而在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称有overlapped的技术),事件分离者等IOCompletion事件完成[1]. 这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。
=======================================================================
这篇文章探讨并比较两种用于TCP服务器的高性能设计模式. 除了介绍现有的解决方案, 还提出了一种更具伸缩性,只需要维护一份代码并且跨平台的解决方案(含代码示例), 以及其在不同平台上的微调. 此文还比较了java,c#,c++对各自现有以及提到的解决方案的实现性能.
系统I/O 可分为阻塞型, 非阻塞同步型以及非阻塞异步型[1, 2]. 阻塞型I/O意味着控制权只到调用操作结束了才会回到调用者手里. 结果调用者被阻塞了, 这段时间了做不了任何其它事情. 更郁闷的是,在等待IO结果的时间里,调用者所在线程此时无法腾出手来去响应其它的请求,这真是太浪费资源了。拿read()操作来说吧, 调用此函数的代码会一直僵在此处直至它所读的socket缓存中有数据到来.
相比之下,非阻塞同步是会立即返回控制权给调用者的。调用者不需要等等,它从调用的函数获取两种结果:要么此次调用成功进行了;要么系统返回错误标识告诉调用者当前资源不可用,你再等等或者再试度看吧。比如read()操作, 如果当前socket无数据可读,则立即返回EWOULBLOCK/EAGAIN,告诉调用read()者"数据还没准备好,你稍后再试".
在非阻塞异步调用中,稍有不同。调用函数在立即返回时,还告诉调用者,这次请求已经开始了。系统会使用另外的资源或者线程来完成这次调用操作,并在完成的时候知会调用者(比如通过回调函数)。拿Windows的ReadFile()或者POSIX的aio_read()来说,调用它之后,函数立即返回,操作系统在后台同时开始读操作。
在以上三种IO形式中,非阻塞异步是性能最高、伸缩性最好的。
这篇文章探讨不同的I/O利用机制并提供一种跨平台的设计模式(解决方案). 希望此文可以给于TCP高性能服务器开发者一些帮助,选择最佳的设计方案。下面我们会比较 Java, c#, C++各自对探讨方案的实现以及性能. 我们在文章的后面就不再提及阻塞式的方案了,因为阻塞式I/O实在是缺少可伸缩性,性能也达不到高性能服务器的要求。
两种IO多路复用方案:Reactor and Proactor
一般情况下,I/O 复用机制需要事件分享器(event demultiplexor [1, 3]). 事件分享器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁的什么东西送了, 快来拿吧。开发人员在开始的时候需要在分享器那里注册感兴趣的事件,并提供相应的处理者(event handlers),或者是回调函数; 事件分享器在适当的时候会将请求的事件分发给这些handler或者回调函数.
涉及到事件分享器的两种模式称为:Reactor and Proactor [1]. Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的. 在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离者就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
而在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称有overlapped的技术),事件分离者等IOCompletion事件完成[1]. 这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。
举另外个例子来更好地理解Reactor与Proactor两种模式的区别。这里我们只关注read操作,因为write操作也是差不多的。下面是Reactor的做法:
- 某个事件处理者宣称它对某个socket上的读事件很感兴趣;
- 事件分离者等着这个事件的发生;
- 当事件发生了,事件分离器被唤醒,这负责通知先前那个事件处理者;
- 事件处理者收到消息,于是去那个socket上读数据了. 如果需要,它再次宣称对这个socket上的读事件感兴趣,一直重复上面的步骤;
下面再来看看真正意义的异步模式Proactor是如何做的:
- 事件处理者直接投递发一个写操作(当然,操作系统必须支持这个异步操作). 这个时候,事件处理者根本不关心读事件,它只管发这么个请求,它魂牵梦萦的是这个写操作的完成事件。这个处理者很拽,发个命令就不管具体的事情了,只等着别人(系统)帮他搞定的时候给他回个话。
- 事件分离者等着这个读事件的完成(比较下与Reactor的不同);
- 当事件分离者默默等待完成事情到来的同时,操作系统已经在一边开始干活了,它从目标读取数据,放入用户提供的缓存区中,最后通知事件分离者,这个事情我搞完了;
- 事件分享者通知之前的事件处理者: 你吩咐的事情搞定了;
- 事件处理者这时会发现想要读的数据已经乖乖地放在他提供的缓存区中,想怎么处理都行了。如果有需要,事件处理者还像之前一样发起另外一个写操作,和上面的几个步骤一样。
现行做法
开源C++开发框架 ACE[1, 3](Douglas Schmidt, et al.开发) 提供了大量平台独立的底层并发支持类(线程、互斥量等). 同时在更高一层它也提供了独立的几组C++类,用于实现Reactor及Proactor模式。 尽管它们都是平台独立的单元,但他们都提供了不同的接口.
ACE Proactor在MS-Windows上无论是性能还在健壮性都更胜一筹,这主要是由于Windows提供了一系列高效的底层异步API. [4, 5].
(这段可能过时了点吧) 不幸的是,并不是所有操作系统都为底层异步提供健壮的支持。举例来说, 许多Unix系统就有麻烦.因此, ACE Reactor可能是Unix系统上更合适的解决方案. 正因为系统底层的支持力度不一,为了在各系统上有更好的性能,开发者不得不维护独立的好几份代码: 为Windows准备的ACE Proactor以及为Unix系列提供的ACE Reactor.
就像我们提到过的,真正的异步模式需要操作系统级别的支持。由于事件处理者及操作系统交互的差异,为Reactor和Proactor设计一种通用统一的外部接口是非常困难的。这也是设计通行开发框架的难点所在。
更好的解决方案
在文章这一段时,我们将尝试提供一种融合了Proactor和Reactor两种模式的解决方案. 为了演示这个方案,我们将Reactor稍做调整,模拟成异步的Proactor模型(主要是在事件分离器里完成本该事件处理者做的实际读写工作,我们称这种方法为"模拟异步")。 下面的示例可以看看read操作是如何完成的:
- 事件处理者宣称对读事件感兴趣,并提供了用于存储结果的缓存区、读数据长度等参数;
- 调试者等待(比如通过select());
- 当有事件到来(即可读),调试者被唤醒, 调试者去执行非阻塞的读操作(前面事件处理者已经给了足够的信息了)。读完后,它去通知事件处理者。
- 事件处理者这时被知会读操作已完成,它拥有完整的原先想要获取的数据了.
我们看到,通过为分离者(也就上面的调试者)添加一些功能,可以让Reactor模式转换为Proactor模式。所有这些被执行的操作,其实是和Reactor模型应用时完全一致的。我们只是把工作打散分配给不同的角色去完成而已。这样并不会有额外的开销,也不会有性能上的的损失,我们可以再仔细看看下面的两个过程,他们实际上完成了一样的事情:
标准的经典的 Reactor模式:
- 步骤 1) 等待事件 (Reactor 的工作)
- 步骤 2) 发"已经可读"事件发给事先注册的事件处理者或者回调 ( Reactor 要做的)
- 步骤 3) 读数据 (用户代码要做的)
- 步骤 4) 处理数据 (用户代码要做的)
模拟的Proactor模式:
- 步骤 1) 等待事件 (Proactor 的工作)
- 步骤 2) 读数据(看,这里变成成了让 Proactor 做这个事情)
- 步骤 3) 把数据已经准备好的消息给用户处理函数,即事件处理者(Proactor 要做的)
- 步骤 4) 处理数据 (用户代码要做的)
在没有底层异步I/O API支持的操作系统,这种方法可以帮我们隐藏掉socket接口的差异(无论是性能还是其它), 提供一个完全可用的统一"异步接口"。这样我们就可以开发真正平台独立的通用接口了。
TProactor
我们提出的TProactor方案已经由TerabitP/L [6]公司实现了. 它有两种实现: C++的和Java的.C++版本使用了ACE平台独立的底层元件,最终在所有操作系统上提供了统一的异步接口。
TProactor中最重要的组件要数Engine和WaitStrategy了. Engine用于维护异步操作的生命周期;而WaitStrategy用于管理并发策略. WaitStrategy和Engine一般是成对出现的, 两者间提供了良好的匹配接口.
Engines和等待策略被设计成高度可组合的(完整的实现列表请参照附录1)。TProactor是高度可配置的方案,通过使用异步内核API和同步Unix API(select(), poll(), /dev/poll (Solaris 5.8+), port_get (Solaris 5.10),RealTime (RT) signals (Linux 2.4+), epoll (Linux 2.6), k-queue (FreeBSD) ),它内部实现了三种引擎(POSIX AIO, SUN AIO and Emulated AIO)并隐藏了六类等待策略。TProactor实现了和标准的 ACE Proactor一样的接口。这样一来,为不同平台提供通用统一的只有一份代码的跨平台解决方案成为可能。
Engines和WaitStrategies可以像乐高积木一样自由地组合,开发者可以在运行时通过配置参数来选择合适的内部机制(引擎和等待策略)。可以根据需求设定配置,比如连接数,系统伸缩性,以及运行的操作系统等。如果系统支持相应的异步底层API,开发人员可以选择真正的异步策略,否则用户也可以选择使用模拟出来的异步模式。所有这一切策略上的实现细节都不太需要关注,我们看到的是一个可用的异步模型。
举例来说,对于运行在Sun Solaris上的HTTP服务器,如果需要支持大量的连接数,/dev/poll或者port_get()之类的引擎是比较合适的选择;如果需要高吞吐量,那使用基本select()的引擎会更好。由于不同选择策略内在算法的问题,像这样的弹性选择是标准ACE Reactor/Proactor模式所无法提供的(见附录2)。
在性能方面,我们的测试显示,模拟异步模式并未造成任何开销,没有变慢,反倒是性能有所提升。根据我们的测试结果,TProactor相较标签的ACE Reactor在Unix/Linux系统上有大约10-35%性能提升,而在Windows上差不多(测试了吞吐量及响应时间)。
性能比较 (JAVA / C++ / C#).
除了C++,我们也在Java中实现了TProactor. JDK1.4中, Java仅提供了同步方法, 像C中的select() [7, 8]. Java TProactor基于Java的非阻塞功能(java.nio包),类似于C++的TProactor使用了select()引擎.
图1、2显示了以 bits/sec为单位的传输速度以及相应的连接数。这些图比较了以下三种方式实现的echo服务器:标准ACE Reactor实现(基于RedHat Linux9.0)、TProactor C++/Java实现(Microsoft Windows平台及RedHat v9.0), 以及C#实现。测试的时候,三种服务器使用相同的客户端疯狂地连接,不间断地发送固定大小的数据包。
这几组测试是在相同的硬件上做的,在不同硬件上做的相对结果对比也是类似。
图 1. Windows XP/P4 2.6GHz HyperThreading/512 MB RAM.
图 2. Linux RedHat 2.4.20-smp/P4 2.6GHz HyperThreading/512 MB RAM.
用户代码示例
下面是TProactor Java实现的echo服务器代码框架。总的来说,开发者只需要实现两个接口:一是OpRead,提供存放读结果的缓存;二是OpWrite,提供存储待写数据的缓存区。同时,开发者需要通过回调onReadComplated()和onWriteCompleted()实现协议相关的业务代码。这些回调会在合适的时候被调用.
class EchoServerProtocol implements AsynchHandler
{
AsynchChannel achannel = null;
EchoServerProtocol( Demultiplexor m, SelectableChannel channel )
throws Exception
{
this.achannel = new AsynchChannel( m, this, channel );
}
public void start() throws Exception
{
// called after construction
System.out.println( Thread.currentThread().getName() +
": EchoServer protocol started" );
achannel.read( buffer);
}
public void onReadCompleted( OpRead opRead ) throws Exception
{
if ( opRead.getError() != null )
{
// handle error, do clean-up if needed
System.out.println( "EchoServer::readCompleted: " +
opRead.getError().toString());
achannel.close();
return;
}
if ( opRead.getBytesCompleted () <= 0)
{
System.out.println("EchoServer::readCompleted: Peer closed "
+ opRead.getBytesCompleted();
achannel.close();
return;
}
ByteBuffer buffer = opRead.getBuffer();
achannel.write(buffer);
}
public void onWriteCompleted(OpWrite opWrite)
throws Exception
{
// logically similar to onReadCompleted
...
}
}
结束语
TProactor为多个平台提供了一个通用、弹性、可配置的高性能通讯组件,所有那些在附录2中提到的问题都被很好地隐藏在内部实现中了。
从上面的图中我们可以看出C++仍旧是编写高性能服务器最佳选择,虽然Java已紧随其后。然而因为Java本身实现上的问题,其在Windows上表现不佳(这已经应该成为历史了吧)。
需要注意的是,以上针对Java的测试,都是以裸数据的形式测试的,未涉及到数据的处理(影响性能)。
纵观AIO在Linux上的快速发展[9], 我们可以预计Linux内核API将会提供大量更加强健的异步API, 如此一来以后基于此而实现的新的Engine/等待策略将能轻松地解决能用性方面的问题,并且这也能让标准ACE Proactor接口受益。
附录 I
TProactor中实现的Engines 和 等待策略
引擎类型 |
等待策略 |
操作系统 |
POSIX_AIO (true async)
aio_read() /aio_write() |
aio_suspend() Waiting for RT signal Callback function |
POSIX complained UNIX (not robust) POSIX (not robust) SGI IRIX, LINUX (not robust) |
SUN_AIO (true async)
aio_read() /aio_write() |
aio_wait() |
SUN (not robust) |
Emulated Async Non-blocking read() /write() |
select()
poll() /dev/poll Linux RT signals Kqueue |
generic POSIX Mostly all POSIX implementations SUN Linux FreeBSD
|
附录 II
所有同步等待策略可划分为两组:
- edge-triggered (e.g. Linux实时信号) - signal readiness only when socket became ready (changes state);
- level-triggered (e.g.
select()
, poll()
, /dev/poll) - readiness at any time.
让我们看看这两组的一些普遍的逻辑问题:
- edge-triggered group: after executing I/O operation, the demultiplexing loop can lose the state of socket readiness. Example: the "read" handler did not read whole chunk of data, so the socket remains still ready for read. But the demultiplexor loop will not receive next notification.
- level-triggered group: when demultiplexor loop detects readiness, it starts the write/read user defined handler. But before the start, it should remove socket descriptior from theset of monitored descriptors. Otherwise, the same event can be dispatched twice.
- Obviously, solving these problems adds extra complexities to development. All these problems were resolved internally within TProactor and the developer should not worry about those details, while in the synch approach one needs to apply extra effort to resolve them.
在网络应用服务器端, 为了性能和防止阻塞, 经常会把逻辑处理和I/O处理分离:
I/O网络线程处理I/O事件: 数据包的接收和发送, 连接的建立和维护等.
逻辑线程要对收到的数据包进行逻辑处理.
通常网络线程和逻辑线程之间是通过数据包队列来交换信息, 简单来说就是一个生产者-消费者模式.
这个队列是多个线程在共享访问必须加锁, 意味着每次访问都要加锁。如何更好的如何减少锁竞争次数呢 ?
方案一 双缓冲消息队列:
两个队列,一个给逻辑线程读,一个给IO线程用来写,当逻辑线程读完队列后会将自己的队列与IO线程的队列相调换。
IO线程每次写队列时都要加锁,逻辑线程在调换队列时也需要加锁,但逻辑线程在读队列时是不需要加锁的.
队列缓冲区的大小要根据数据量的大小进行调整的,如果缓冲区很小,就能更及时的处理数据,但吞吐量以及出现资源竞争的几率大多了。
可以给缓冲队列设置最大上限,超过上限的数量之后,将包丢弃不插入队列。
另外,双缓冲的实现也有不同策略的,
一是读操作优先,就是生产者只要发现空闲缓冲,马上swap,
二是写线程只有在当前的缓冲区写满了,才进行swap操作。
三是上层逻辑按照帧率来处理,每一帧的时候将双层缓冲队列调换一下,取一个队列来处理即可
方案二 提供一个队列容器:
提供一个队列容器,里面有多个队列,每个队列都可固定存放一定数量的消息。网络IO线程要给逻辑线程投递消息时,会从队列容器中取一个空队列来使用,直到将该队列填满后再放回容器中换另一个空队列。而逻辑线程取消息时是从队列容器中取一个有消息的队列来读取,处理完后清空队列再放回到容器中。
这样便使得只有在对队列容器进行操作时才需要加锁,而IO线程和逻辑线程在操作自己当前使用的队列时都不需要加锁,所以锁竞争的机会大大减少了。
这里为每个队列设了个最大消息数,看来好像是打算只有当IO线程写满队列时才会将其放回到容器中换另一个队列。那这样有时也会出现IO线程未写满一个队列,而逻辑线程又没有数据可处理的情况,特别是当数据量很少时可能会很容易出现[这个可以通过设置超时来处理, 如果当前时间-向队列放入第一个包的时间 > 50 ms, 就将其放回到容器中换另一个队列]。
通常我们逻辑服务器会以场景来划分线程,不同线程执行不同场景.一个线程可以执行多个场景.因为玩家属于场景,我们会把玩家数据,包括其缓冲池丢给场景 去处理.
相信做开发的没有不重视效率的。开发C#,VB的都知道,我们很依赖VS,或者说,我们很感谢VS。能够对一个IDE产生依赖,说明这个IDE确实 有它的独特之处。无容置疑,VS是一个非常强大的IDE,它支持多语言编辑。支持C#,VB,C/C++,HTML......它拥有强大的调试编译功 能。它让我们不用去记住那些安装,环境变量设置,服务器设置,编译的繁琐过程。高度集成化。凡事有利有弊,在敏捷开发盛行的时代,VS是否值得我们使用是 无容置疑的。但是强大的VS也拥有众多的设置,众多的技巧。记住某些小技巧可以让我们更加方便,快捷地使用VS。这是很有必要的。每个人或多或少记住了一 些小技巧。但是不可能全部都记住,我们按照我们自己的编程习惯记住一些自己比较常用的就好。
下面是鄙人在编码过程中发现而且比较经常使用的一些小技巧,希望对你有所帮助。
1.行编辑(复制,剪切,删除,交换)
当你在光标停留行使用快捷键Ctrl+C,X,L时,可以复制,剪切,删除整行内容。当然,右键也是可以的。跟平时的复制,剪切,删除就是选中和没选中代码的区别而已。
如果你想交换上下两行,你可以使用快捷键(Shift+Alt+T),前提是光标要停留在上面那一行。替换之后,光标会一直跟随原本的那一行。
2.注释(//TODO:...)
看标题的话,你可能想打我。那个程序员不知道注释啊,不就//或者/*.....*/亦或者(HTML/XML注释)。但是使用过
的,估计是少数吧。如果你喜欢用“任务列表”记录一些要做的事情,这个小功能最适合你了。你可以再VS 2010的菜单上找到任务列表窗,点击“菜单->视图->任务列表”,你也可以点击快捷键“Ctrl+W,T”。VS还提供了,HACK,UNTODU,UnresolvedMergeConflict标记注释,你可以在“工具->选项->环境->任务列表”找到并且编辑/添加/删除标记注释。下面是图示:
![标记注释和任务列表 提高你开发效率的十五个 Visual Studio 使用技巧](http://www.open-open.com/lib/uploadImg/20120305/20120305193014_747.jpg)
![标记注释设置 提高你开发效率的十五个 Visual Studio 使用技巧](http://www.open-open.com/lib/uploadImg/20120305/20120305193014_895.jpg)
3.创建区域(#region和#endregion)
当代码越来越多的时候,你会期望可以隐藏一些代码,而#region 和#endregion 就是这样的功能。你可以在任何位置隐藏任何代码。即使是隐藏的内容不属于同一个函数。你可以点击#region旁边的+/-,展开/隐藏代码。在隐藏的时 候,当你的光标放放置在备注上面的时候,VS会显示出隐藏的代码内容。
![隐藏代码 提高你开发效率的十五个 Visual Studio 使用技巧](http://www.open-open.com/lib/uploadImg/20120305/20120305193015_346.jpg)
4.选择一个单词/选择一个字符串
如你所知双击一个单词的时候会选择整个单词。按住Ctrl键单击单词的任意位置同样可以选中单词。
双击字符串第一个引号的左侧可以选中整个字符串。按住Ctrl键单击第一个引号的前面同样可以选中整个字符串。
5.将代码放入工具箱
工具箱是拿来放控件的地方。我们在使用控件的时候,只需要从控件当中拖动控件到代码就可以了,这样可以省去大量代码的编辑工作。既然工具箱如此方便,那么是否可以将一段重用性很高的代码放入工具箱呢。答案当然是可以的。
你可以选中你的代码,然后拖入工具箱的空白处,你的代码就保存到工具箱了。就像你将控件拖 入代码页面一样,也可以将代码拖入工具箱中。以后你就可以像使用控件一样使用重用的代码。这是非常方便的。而且工具箱的内容不会因为你关闭VS而消失,在 你下次打开VS的时候工具箱同样保存了你的代码。如果你需要查看工具箱保存的代码而又不想拖到代码页面中,你只需要将光标停留在工具箱的代码图标上面。如 图所示:
![将代码放入工具箱 提高你开发效率的十五个 Visual Studio 使用技巧](http://www.open-open.com/lib/uploadImg/20120305/20120305193015_657.jpg)
6.格式化代码
这个很重要,即使VS在你每次打完“;”回车之后会自动格式化代码。但是难免代码的格式会有所变化,譬如粘贴一段代码之后,代码的格式往往会受到影响。所以,这个还是很有必要知道的。
格式化部分代码:选中代码->Ctrl+K,F。或者Ctrl+E,F。
格式化整个文档:编辑->高级->设置文档的格式。或者 Ctrl+K,D。或者Ctrl+E,D。
7.切换设计/代码图示
在ASP.NET页面切换(HTML): Ctrl+PgUp/Ctrl+PgDn
在windows窗体切换:F7/Shift+F7 (代码/设计)
8.查找错误代码。
当错误列表有错误或者警告提示时,你可以双击这个错误或提示,就可以跳转到错误或警告的语句前。
9.跳转到指定行号
如果代码很多的时候,这是很有用的。在ASP.NET编程的时候,很多错误只有在运行网站的时候才能发现,而这个错误又没被在错误列表提示的时候,你就可以使用这个小技巧跳到错误的代码前面。
双击右下角状态栏的行号,会跳出一个行号跳转窗体。或者快捷键Ctrl+G调出窗体。当然,还可以从菜单栏点击“编辑->跳转..”使用这个功能。
![跳转到指定行号 提高你开发效率的十五个 Visual Studio 使用技巧](http://www.open-open.com/lib/uploadImg/20120305/20120305193015_761.jpg)
10.快速查找
光标停留在需要查找的词上面,使用快捷键Ctrl+F3可以跳转到下一个相同的词。按Shift+F3可以往上查找。
11.查找“{/}”
查找:你是否很烦恼有些对应的标记找的到头找不到尾,找得到尾不知道那个是头。当你把光标放在“{”的前面,VS会将相对应的”}“标记起来。你也可以将光标停留在“}”的后面,可以达到同样的效果。
![查找“{/}” 提高你开发效率的十五个 Visual Studio 使用技巧](http://www.open-open.com/lib/uploadImg/20120305/20120305193015_642.jpg)
12.查找和替换
当你想查找/替换掉某个字符串的时候,你可以按快捷键“Ctrl+F”或者“Ctrl+H”,进行这一操作。另外VS支持正则表达式和通配符。
如果你想从整个项目进行查找/替换,你需要使用快捷键“Ctrl+Shift+F”或者“Ctrl+Shift+H”。当然这一切都可以在菜单栏找到。“编辑->查找和替换”。当你想中止全局替换的时候,你可以使用快捷键“Ctrl+Pause Break”。
![查找和替换 提高你开发效率的十五个 Visual Studio 使用技巧](http://www.open-open.com/lib/uploadImg/20120305/20120305193015_320.jpg)
13. 书签
书签是很有用的功能,用过Chrome的都知道。在VS当中,书签同样适用。它可以帮你保存位置,以便你写代码。
放置书签:Ctrl+B,T。
上一个书签:Ctrl+B,P。
下一个书签:Ctrl+B,N。
删除所有书签:Ctrl+K,C。
除此之外,VS还提供了其它的书签操作。
![书签 提高你开发效率的十五个 Visual Studio 使用技巧](http://www.open-open.com/lib/uploadImg/20120305/20120305193016_26.jpg)
14.跳转到定义
当你查看代码的时候,往往需要去查看原函数,这是难免的。但是千万不要去手动寻找函数。这效率往往是很低的。你可以右键该函数,选择跳转到定义即可。当然你也可以使用快捷键F12。
![跳转到定义 提高你开发效率的十五个 Visual Studio 使用技巧](http://www.open-open.com/lib/uploadImg/20120305/20120305193016_276.jpg)
15.以文本形式插入外部文本
“菜单->编辑->将文件作为文本插入”
好处是,你不需要打开文件去复制粘贴。
![将文件以文本的形式插入 提高你开发效率的十五个 Visual Studio 使用技巧](http://www.open-open.com/lib/uploadImg/20120305/20120305193016_161.jpg)
或许这些小技巧你早就知道了。亦或是,你觉得这些技巧根本没啥用。当然,我们最主要的任务还是去编码而已。没有必要将心思花在这上面。但是,当你习惯使用这些小技巧的时候,这为你带来的收益觉得不会让你有所失望的。有些技巧,个人认为还是很有必要掌握的。
总之,捡你想捡的吧,让其他人折腾去吧。
C++提供了
关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。
C++中, 一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数), 承担了两个角色。 1 是个构造器 2 是个默认且隐含的类型转换操作符。
所以, 有时候在我们写下如 AAA = XXX, 这样的代码, 且恰好XXX的类型正好是AAA单参数构造器的参数类型, 这时候编译器就自动调用这个构造器, 创建一个AAA的对象。
这样看起来好象很酷, 很方便。 但在某些情况下(见下面权威的例子), 却违背了我们(程序员)的本意。 这时候就要在这个构造器前面加上explicit修饰, 指定这个构造器只能被明确的调用,使用, 不能作为类型转换操作符被隐含的使用。 呵呵, 看来还是光明正大些比较好。
explicit构造函数的作用
解析:
explicit构造函数是用来防止隐式转换的。请看下面的代码:
class Test1
{
public:
Test1(int n) { num = n; } //普通构造函数
private:
int num;
};
class Test2
{
public:
explicit Test2(int n) { num = n; } //explicit(显式)构造函数
private:
int num;
};
int main()
{
Test1 t1 = 12; //隐式调用其构造函数, 成功
Test2 t2 = 12; //编译错误,不能隐式调用其构造函数
Test2 t3(12); //显示调用成功
return 0;
}
Test1的构造函数带一个int型的参数,代码19行会隐式转换成调用Test1的这个构造函数。而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数,因此代码20行会出现编译错误。
普通构造函数能够被隐式调用。而explicit构造函数只能被显示调用。
接下来是定义message属性,一个message是包含了各种类型字段的聚集。有很多标准的变量类型可以使用,包 括:bool,int32,float,double和string。你也可以使用其他的message作为字段类型。正像例子中的Person包含了 PhoneNumber,而AddressBook包含了Persion。甚至可以在message内部定义message,例 如:PhoneNumber就是在Persion里面定义的。你还可以定义enum类型,正像指定电话号码类型的MOBILE、HOME、WORK。
其中“=1”,“=2”表示每个元素的标识号,它会用在二进制编码中对域的标识。标识号1-15由于使用时会比那些高的标识号少一个字节,从最优化 角度考虑,可以将其使用在一些较常用的或repeated元素上,对于16以上的则使用在不常用的或optional的元素上。对于repeated的每 个元素都需要重复编码该标识号,所以repeated的域进行优化来说是最显示的。
每个字段必须提供一个修饰词:
Ø required:表示字段必须提供,不能为空。否则message会被认为是未初始化的,试图build未初始化的message会抛出 RuntimeException。解析未初始化的message会抛出IOException。除此之外,一个required字段与optional 字段完全相同。
Ø optional:可选字段,可以设置也可以不设置。如果没有设置,会设置一个缺省值。可以指定一个缺省值,正像电话号码的type字段。否则,使用系统 的缺省值:数字类型缺省为0;字符类型缺省为空串;逻辑类型缺省为false;对于嵌入的message,缺省值通常是message的实例或原型。
Ø repeated:字段可以被重复(包括0),可等同于动态数组或列表。其中存储的值列表的顺序是被保留的。
Required修饰的字段是永久性的,在使用该修饰符时一定要特别小心。如果在以后想要修改required域为optional域时会出现问 题。对于访问旧接口的用户来说没有该字段时,将会认为是不合法的访问,将会被拒绝或丢弃。其中google的一些工程师给出的建议是如果不是必须,就尽量 少用required修饰符。