在所有的預處理指令中,#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標記注釋,你可以在“工具->選項->環境->任務列表”找到并且編輯/添加/刪除標記注釋。下面是圖示:


3.創建區域(#region和#endregion)
當代碼越來越多的時候,你會期望可以隱藏一些代碼,而#region 和#endregion 就是這樣的功能。你可以在任何位置隱藏任何代碼。即使是隱藏的內容不屬于同一個函數。你可以點擊#region旁邊的+/-,展開/隱藏代碼。在隱藏的時 候,當你的光標放放置在備注上面的時候,VS會顯示出隱藏的代碼內容。

4.選擇一個單詞/選擇一個字符串
如你所知雙擊一個單詞的時候會選擇整個單詞。按住Ctrl鍵單擊單詞的任意位置同樣可以選中單詞。
雙擊字符串第一個引號的左側可以選中整個字符串。按住Ctrl鍵單擊第一個引號的前面同樣可以選中整個字符串。
5.將代碼放入工具箱
工具箱是拿來放控件的地方。我們在使用控件的時候,只需要從控件當中拖動控件到代碼就可以了,這樣可以省去大量代碼的編輯工作。既然工具箱如此方便,那么是否可以將一段重用性很高的代碼放入工具箱呢。答案當然是可以的。
你可以選中你的代碼,然后拖入工具箱的空白處,你的代碼就保存到工具箱了。就像你將控件拖 入代碼頁面一樣,也可以將代碼拖入工具箱中。以后你就可以像使用控件一樣使用重用的代碼。這是非常方便的。而且工具箱的內容不會因為你關閉VS而消失,在 你下次打開VS的時候工具箱同樣保存了你的代碼。如果你需要查看工具箱保存的代碼而又不想拖到代碼頁面中,你只需要將光標停留在工具箱的代碼圖標上面。如 圖所示:

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調出窗體。當然,還可以從菜單欄點擊“編輯->跳轉..”使用這個功能。

10.快速查找
光標停留在需要查找的詞上面,使用快捷鍵Ctrl+F3可以跳轉到下一個相同的詞。按Shift+F3可以往上查找。
11.查找“{/}”
查找:你是否很煩惱有些對應的標記找的到頭找不到尾,找得到尾不知道那個是頭。當你把光標放在“{”的前面,VS會將相對應的”}“標記起來。你也可以將光標停留在“}”的后面,可以達到同樣的效果。

12.查找和替換
當你想查找/替換掉某個字符串的時候,你可以按快捷鍵“Ctrl+F”或者“Ctrl+H”,進行這一操作。另外VS支持正則表達式和通配符。
如果你想從整個項目進行查找/替換,你需要使用快捷鍵“Ctrl+Shift+F”或者“Ctrl+Shift+H”。當然這一切都可以在菜單欄找到。“編輯->查找和替換”。當你想中止全局替換的時候,你可以使用快捷鍵“Ctrl+Pause Break”。

13. 書簽
書簽是很有用的功能,用過Chrome的都知道。在VS當中,書簽同樣適用。它可以幫你保存位置,以便你寫代碼。
放置書簽:Ctrl+B,T。
上一個書簽:Ctrl+B,P。
下一個書簽:Ctrl+B,N。
刪除所有書簽:Ctrl+K,C。
除此之外,VS還提供了其它的書簽操作。

14.跳轉到定義
當你查看代碼的時候,往往需要去查看原函數,這是難免的。但是千萬不要去手動尋找函數。這效率往往是很低的。你可以右鍵該函數,選擇跳轉到定義即可。當然你也可以使用快捷鍵F12。

15.以文本形式插入外部文本
“菜單->編輯->將文件作為文本插入”
好處是,你不需要打開文件去復制粘貼。

或許這些小技巧你早就知道了。亦或是,你覺得這些技巧根本沒啥用。當然,我們最主要的任務還是去編碼而已。沒有必要將心思花在這上面。但是,當你習慣使用這些小技巧的時候,這為你帶來的收益覺得不會讓你有所失望的。有些技巧,個人認為還是很有必要掌握的。
總之,撿你想撿的吧,讓其他人折騰去吧。
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修飾符。