1/06/2014

ODBC-API存取MS-SQL預存程序(Stored Procedure)的正確用法

回想之前...我曾被ODBC-API針對MS-SQL預存程序(Stored Procedure)的使用方式搞到頭快爆了~
在網路上搜尋到很多相關資料,但是都沒有正確的使用範例!?!
一堆人在討論,但是有解答的幾乎沒有,或是我沒找到!?!
只有那些微軟的MSDN英文說明有非常多的解釋!但是這麼多英文...慢慢看...實在會瘋掉!
而且重點是要有直接的程式範例參考啊!
不然MSDN寫再多文字也只是曲高和寡,實質意義不大!

而且書店的SQL電腦書堆裡竟然沒有一本有完整介紹ODBC-API!?!
難道這技術這麼差嗎?還是太舊了,過時了,沒人想使用!?!怪怪~
不過,因為我蠻堅持要使用ODBC-API來存取MS-SQL預存程序,
(因為我想將已學的ODBC繼續強化)

所以我足足花了四天時間待在外包公司內做研究!終於東湊西湊的,將正確的用法找出來了!
而且還有外包公司的韓國工程師來支援我,讓我更快瞭解預存程序的用法!
ps : 有趣的是... 小事情我們就用英文溝通,大事情才靠韓文翻譯人員幫忙!
  能用英文來溝通,工作,這點我倒是覺得很新鮮!

在此我就和大家分享我辛苦的小研究喔!
ODBC-API的初始化我就不列出了,這種基本資料網路上還是很多的!
我就直接進入主題來說明!

//-------------------------

// 以下是SQL中的sp_test預存程序的範例:(自己要去MS-SQL中定義sp_test喔)
CREATE Procedure sp_test
    @val1    As    int,
    @val2    As    int output,
    @uid1    As    varchar(12),
    @uid2    As    varchar(12) output

As

set @val2 = @val1;

set @uid2 = @uid1;

return 1
GO


//-------------------------

// 以下是用ODBC-API來呼叫SQL中的sp_test預存程序的範例

long MSSQLClass::vStoredProcedure_SelectData()
{
    SQLRETURN   rc = 0;
    char szQuery[ SQL_STRING_MAX ] = { 0 };

    rc = SQLAllocHandle( SQL_HANDLE_STMT, hdbc, &hstmt );
    ERROR_OUT( rc, "MSSQLClass::vStoredProcedure_SelectData   error : SQLAllocHandle" );

    // 下面的?號,是更下面的SQLBindParameter要綁定參數的順序
    // 而第一個?號是回傳值(如果需要有回傳的話)
    // 若是沒有回傳值的預存程序,則輸入的命令是 {call sp_test(?,?,?,?)}
    strcat_s( szQuery, SQL_STRING_MAX, "{? = call sp_test(?,?,?,?)}" );

    // 準備要下的SQL命令
    rc = ::SQLPrepare( hstmt, (SQLCHAR *)szQuery, SQL_NTS );
    ERROR_OUT( rc, "MSSQLClass::vStoredProcedure_SelectData   error : SQLPrepare" );

    SQLINTEGER receive_length1 = 0;
    SQLINTEGER receive_length2 = 0, receive_length3 = 0;
    SQLINTEGER receive_length4 = 0;

    // 這個SQL_NTS代號是SQL_PARAM_INPUT的字串時必須用的,
    // 否則預存程序會接收不到傳入的字串
    SQLINTEGER char_type_input_code = SQL_NTS;

    SQLINTEGER return_value = -99;

    SQLBIGINT val1 = 10, val2 = 0; // val2要先清為0,不然會收不到資訊!
    SQLCHAR uid1[ 20 ] = { "hello" }, uid2[ 20 ] = { 0 }; // uid2要先清為0,不然會收不到資訊!
      
    // 第一個SQLBindParameter是接收回傳值,所以要使用SQL_PARAM_OUTPUT
    rc = ::SQLBindParameter( hstmt, 1, SQL_PARAM_OUTPUT,
                                 SQL_C_SLONG, SQL_INTEGER, 0, 0,
                                 &return_value, 0,
                                 &receive_length1 );
    ERROR_OUT( rc, "MSSQLClass::vStoredProcedure_SelectData   error : SQLBindParameter" );

    // 第二個是只傳入val1數值,所以用SQL_PARAM_INPUT
    rc = ::SQLBindParameter( hstmt, 2, SQL_PARAM_INPUT,
                                 SQL_C_ULONG, SQL_INTEGER, 0, 0,
                                 &val1, 0,
                                 &receive_length2 );
    ERROR_OUT( rc, "MSSQLClass::vStoredProcedure_SelectData   error : SQLBindParameter" );

    // 第三個是預存程序內會改變val2數值,所以必須使用SQL_PARAM_INPUT_OUTPUT
    rc = ::SQLBindParameter( hstmt, 3, SQL_PARAM_INPUT_OUTPUT,
                                 SQL_C_ULONG, SQL_INTEGER, 0, 0,
                                 &val2, 0,
                                 &receive_length3 );
    ERROR_OUT( rc, "MSSQLClass::vStoredProcedure_SelectData   error : SQLBindParameter" );

    // 第四個是只傳入uid1字串,所以用SQL_PARAM_INPUT
    // 這裡有個重點,就是SQLBindParameter的最後一個參數必須填入SQL_NTS代號!
    // 但是因為他是只能傳入指標,所以必須將代號先放在char_type_input_code變數上
    // 如果不傳入SQL_NTS代號,則預存程序會接收不到傳入的字串喔!
    // 這點,我花了很多時間研究原因,終於讓我發現到這個設定上的小眉角!
    rc = ::SQLBindParameter( hstmt, 4, SQL_PARAM_INPUT,
                                 SQL_C_CHAR, SQL_VARCHAR, sizeof( uid1 ), 0,
                                 uid1, sizeof( uid1 ),
                                 &char_type_input_code ); // 這裡是傳入字串的重點設定喔
    ERROR_OUT( rc, "MSSQLClass::vStoredProcedure_SelectData   error : SQLBindParameter" );

    // 第五個是預存程序內會改變uid2字串,所以必須使用SQL_PARAM_INPUT_OUTPUT
    rc = ::SQLBindParameter( hstmt, 5, SQL_PARAM_INPUT_OUTPUT,
                                 SQL_C_CHAR, SQL_VARCHAR, sizeof( uid2 ), 0,
                                 uid2, sizeof( uid2 ),
                                 &receive_length4 );
    ERROR_OUT( rc, "MSSQLClass::vStoredProcedure_SelectData   error : SQLBindParameter" );
  
    // 執行準備好的命令
    rc = ::SQLExecute( hstmt );
    ERROR_OUT( rc, "MSSQLClass::vStoredProcedure_SelectData   error : SQLExecute" );

    // 如果預存程序內有多條SQL敘述,則需要再執行SQLMoreResults,依序將資料取出!
    while( 1 )
    {
        rc = ::SQLMoreResults( hstmt );
        if( rc == SQL_NO_DATA )
        {
            break;
        }
        else
            ERROR_OUT( rc, "MSSQLClass::vStoredProcedure_SelectData   error : SQLMoreResults" );
    }

    ::SQLFreeHandle( SQL_HANDLE_STMT, hstmt );
    hstmt = SQL_NULL_HSTMT;

    return return_value;
}


還有另一個用法,就是能藉由預存程序,取得一連串的資料!
例如:一次取出50筆朋友清單資料!
這用法其實也很簡單,等我有空時再發佈嚕~


最後要說的是,就將這很辛苦的小小研究成果獻給有緣人嚕!



ps : 我又多花了三天,才解決預存程序內若有多條SQL敘述...會發生取不到資料的問題,
  原來是需要使用SQLMoreResults來取出!找的我快瘋了!



1/03/2014

勇者大人的記憶體池(memory pool)使用方法簡介

我認為精通活用資料結構是非常重要的內功!(我也還在學...)

我設計的記憶體池管理類別,就是善用資料結構的產物。(哈~自賣自誇)

 任何種類的記憶體池管理類別都是為了解決某一種需求而設計出來的。
當然,我這東西仍在持續改進中...

我只是單純的分享一下我的設計方式,網友若有更好的設計方式,
也麻煩分享給我喔!


有網友就問我該如何使用這些記憶體池管理類別呢!?
那我就介紹一下一些我常用的記憶體池管理類別。

記憶體池管理類別  使用範例如下:
void Example()
{
    // AV_KernelMemoryPool 記憶體池管例類別用法簡介
 
    DWORD bytes_1MB = (1024 * 1024);
    DWORD memory_pool_bytes_max = bytes_1MB * 100;
    // 先配置100MB的總記憶體池
    AV_KernelMemoryPool::sMemoryPoolCreate( memory_pool_bytes_max );
 
    //#####################################################
    // AV_MemoryPool2HeadList 雙向鍊結串列用法簡介
 
    struct PlayerInfo // 玩家資訊結構- 假設有此種結構
    {
        DWORD Number;
        char  Name[ 16 ];
        DWORD Attack;
        DWORD Defend;
    };
   
    // 此PlayerTable表格實際上會是定義在例如GameManager這種主類別之中,
    // 現在這只是範例,所以就寫在這裡.
    AV_MemoryPool2HeadList< PlayerInfo > PlayerTable; // 玩家資訊串列表格
 
    // 初始化表格
    PlayerTable.Create( 100 ); // 從總記憶體池配置出100個PlayerInfo的空間
   
    // 如果當有需要使用新PlayerInfo節點時...
    // 例如: Client端要登入Server端時, Server端就需要配置該玩家資訊的節點。
    // 下面是取得一個新的PlayerInfo節點來用。
    PlayerInfo *new_player_node = PlayerTable.GetNode();

    // 將節點串連起來(也可以不用,看當時的需求)

    // 串起來的目的是...如果有尋訪所有使用節點的需求時, 則就會用到.
    PlayerTable.Link( new_player_node );
 
    // 當不需要用時...
    // 先解開連結
    PlayerTable.Unlink( new_player_node );
    // 再將此節點清除後釋放
    // ps: 這不是真正的記憶體釋放,而是將此節點先保留起來,
    // 等到下次要GetNode()時,則會優先分配出去使用.
    PlayerTable.ReleaseNode( new_player_node );
 
    //------------------

    // 還有一種AV_ MemoryPool2HeadArrayList用法,
    // 目的是...不需要向總記憶體池來索取記憶體,

    // 而是用自身的NodeBuffer記憶體來當作串列用的記憶體.
    // 我想...這是個很有趣的觀念和用法...
    // 舉例: 假設有這樣的SceneInfo結構...
    struct SceneInfo
    {
    // 設定NPCTable有100個PlayerInfo空間,但目前還未分配到記憶體.
        AV_MemoryPool2HeadArrayList< PlayerInfo, 100 > NPCTable;
    // 設定EnemyTable有200個PlayerInfo空間,但目前還未分配到記憶體.
        AV_MemoryPool2HeadArrayList< PlayerInfo, 200 > EnemyTable;
    };
    AV_MemoryPool2HeadList< SceneInfo > SceneTable;
    // 這個5是隨便打的.
    SceneTable.Create( 5 );
    SceneInfo *scene = SceneTable.GetNode(); // 取得新節點
    // 重點在這...    // Create內部會將自身的NodeBuffer和自身的    // pBufferList連接起來, 達到記憶體配置之自給自足喔!
    scene->NPCTable.Create();      // 分配記憶體,也就是做記憶體連結初始化
    scene->EnemyTable.Create(); // 分配記憶體,也就是做記憶體連結初始化
 
    // 之後呢...萬一有需要將scene資訊清除時,
    // 可以直接這樣清除...
    ZeroMemory( scene, sizeof( SceneInfo ) );
    // 或是這樣清除並釋放...
    SceneTable.ReleaseNode( scene );
    // 而不需要再顧慮NPCTable和EnemyTable的記憶體配置釋放問題.
    // 這種設計方式能達到記憶體不用再歸還作業系統.     //#####################################################
    // AV_SimpleMemoryPool 小型記憶體池用法簡介
 
    DWORD simple_memory_pool_bytes_max = bytes_1MB * 10;
    AV_SimpleMemoryPool  SimpleMemoryPool;
    // 這小型記憶體池是從主記憶體池分配出來的.
    SimpleMemoryPool.MemoryPoolCreate( simple_memory_pool_bytes_max );
 
    // 而這要如何應用呢!?
    // 就用上面的SceneInfo例子來應用.
    AV_MemoryPool2HeadList< SceneInfo > SceneTable2;
    DWORD scene_table_node_count = 5;
    DWORD scene_table_memory_bytes =
                sizeof( MPNode2< SceneInfo > ) * scene_table_node_count;

    // 重點來了, 從SimpleMemoryPool配置5個MPNode2< SceneInfo >節點的記憶體,
    // 然後傳給SceneTable2來作記憶體連結的初始化,
    // 這樣一來,SceneTable2萬一不需要再使用時,
    // 可以藉由SimpleMemoryPool來作全部的回收再利用 ,
    // 也就是同樣的記憶體,再給其他種的記憶體結構所使用,
    // 更能善加利用記憶體池喔.
    SceneTable2.CreateLink( scene_table_node_count,         (MPNode2< SceneInfo >*)SimpleMemoryPool.MemoryAllocate(
                scene_table_memory_bytes ) );

 
    //#####################################################
    // AV_SimplyMemoryPoolArray 陣列用法簡介
 
    struct RoomInfo
    {
        DWORD Number;
        char  Name[ 16 ];
    };
 
    // 其實這用法也很簡單,跟上面的都類似...
    // 我有盡量將相似的使用語法統一起來.
 
    AV_SimplyMemoryPoolArray< RoomInfo > RoomTable;
    // 配置100個RoomInfo的實體空間
    RoomTable.Create( 100 );
 
    RoomInfo *room = RoomTable.Get( 0 );
    room->Number = 100;
 
    // 下面這是指標陣列
    AV_MemoryPoolPointerArray< RoomInfo > RoomPointerTable;
    // 配置100個RoomInfo的指標陣列空間
    RoomPointerTable.Create( 100 );
    RoomPointerTable.Add( room );

    //#####################################################

    //其他種的記憶體池管理類別也都是大同小異的用法
    //看倌們就自行發揮嚕!
    //其實,最好是自己動手寫一組這種管理類別,
    //這樣自己才能夠真正瞭解到關鍵點, 也才能自己解決各種需求喔!

    //#####################################################

 
    // 當程式要結束時,就這樣將記憶體池釋放給作業系統吧.
    AV_KernelMemoryPool::sMemoryPoolRelease();
}



謝謝指教!

11/16/2013

多執行緒伺服器程式感想(多緒方案)

多執行緒真的是一門有趣又困難的技術。
而在多核心電腦當道的趨勢下,
程式設計師也務必將此技術觀念學好、學精。

我在寫伺服器程式時,常常被多緒之間互相存取公用資源而感到困擾。
若一不小心恍神,沒注意到,就可能埋下日後的隱藏性錯誤。

難道沒有一種較佳的多緒方案,能同時實現『撰寫容易』和『高效能』嗎?
這點我仍是在不斷思考中。。。

以下列出一些遊戲伺服器程式比較會用到多緒方案:

〔1〕winsock:簡單型(非封鎖模式)
一個send_thread(可以用main_thread代替),一個accept_thread,一個recv_and_logic_thread。
說明:
這是最簡單的多緒應用,也只用三個緒,『撰寫容易,效能適中』,適合初學者使用。
由於只有一個recv_and_logic_thread,所以多玩家間要互相存取資訊時,
不需要考慮lock/unlock問題,撰寫上就非常方便。
(recv_and_logic_thread的意思是接收封包和邏輯處理都是在同一個緒上完成)

〔2〕winsock:中階型(非封鎖模式)
一個send_thread(可以用main_thread代替),一個accept_thread,一堆recv_thread,一個logic_thread。
說明:
此多緒應用,也是『撰寫容易,效能適中』,send_thread、accept_thread和recv_thread這種緒是可以歸到
game engine層上,剩下的應用層處理就是logic_thread。
這是穩定、安全、簡單、容易分工的程式設計作法。
(我就有一個朋友是採用此方案來設計)

〔3〕winsock:複雜型(非封鎖模式)
一個send_thread(可以用main_thread代替),一個accept_thread,一堆recv_and_logic_thread。
說明:
由於使用了多個recv_and_logic_thread,所以多玩家間要互相存取資訊時,
開始需要考慮lock/unlock問題,撰寫上就變得困難許多。
例如:一堆玩家要同時查詢玩家列表資訊時,就需要互相存取,同時,部份玩家可能也會下線。
此方案適合經驗豐富者使用。

〔4〕IOCP:簡單型
一個send_thread(可以用main_thread代替),一個accept_thread,一堆recv_thread,一個logic_thread。
說明:
要使用IOCP,基本上就是一件複雜的工程,但撇開IOCP複雜的機制不談,
若是只使用一個logic_thread,則撰寫遊戲邏輯時,將會輕鬆許多。
因為不需要考慮玩家間的互相存取問題。可以用一般單緒的思考來撰寫。
(我就有一個朋友是採用此方案來設計)

〔5〕IOCP:複雜型
一個send_thread(可以用main_thread代替),一個accept_thread,一堆recv_and_logic_thread。
說明:
我想這是最複雜的應用了。運用得好的話,可以徹底發揮多核的功效,
但很難運用得好。。。呵呵~
我目前是採用此方案來設計,有時感到蠻困擾的。會有一些我無法理解的bug現象出現。
而程式中,到處都是lock/unlock,思維都得用多緒來思考,程式撰寫速度會較慢。
優點是...效能是最好的。


簡單說明了這些多緒方案,我還是在想我可以如何改進我的多緒設計。。。
以實現『撰寫容易』和『高效能』。
若有想到什麼時,再補充吧!


謝謝觀賞。

11/14/2013

多執行緒(multi-thread) - 避免發生死結(死鎖)(deadlock)的小技巧(二)

來發表一下thread經驗談...

據我使用thread多年的經驗發現,
要避免死結,除了我以前列出的重點之外,
最好的方式就是...

不要Lock多個物件後才處理邏輯

這個設計限制,將能杜絕死結的產生,
但寫起來會稍微麻煩點!


說明範例如下:

一般的Lock方式(多重lock互套,容易發生死結):
void PlayerAttackEnemy( char *player1_name, char *player2_name )
{
    PlayerInfo *player1_node = GetPlayer( player1_name );
    PlayerInfo *player2_node = GetPlayer( player2_name );

    player1_node->Lock();
    player2_node->Lock();

    player1_node->HP -= player2_node->Strength + player2_node->Equip;
    player2_node->AttackExp += 1;

    player2_node->Unlock();
    player1_node->Unlock();
}

改寫後(禁止多重lock互套,不會發生死結):
void PlayerAttackEnemyForThreadSafe( char *player1_name, char *player2_name )
{
    PlayerInfo *player1_node = GetPlayer( player1_name );
    PlayerInfo *player2_node = GetPlayer( player2_name );

    player2_node->Lock();

    DWORD attack = player2_node->Strength + player2_node->Equip;
    player2_node->AttackExp += 1;

    player2_node->Unock();

    //---------------------

    player1_node->Lock();

    player1_node->HP -= attack;

    player1_node->Unlock();
}

看出其中的差異了嗎?這非常重要喔!

範例寫法可以繼續衍生很多種,但觀念則是不變的。
請記住:不要Lock多個物件後才處理邏輯

謝謝觀賞~


延伸閱讀:
3D線上遊戲製作軟體 - 多執行緒(multi-thread) - 避免發生死結(死鎖)(deadlock)的小技巧!(一)

11/13/2013

多執行緒(multi-thread) - 避免發生死結(死鎖)(deadlock)的小技巧!(一)

多執行緒(multi-thread) !一個讓程式師又愛又恨的技術! 
愛的是....它能善用單CPU和人機介面操作時的閒置時間,藉此加快程式運作。也能利用現在最夯的趨勢:多核心CPU,大幅增進程式效能。
恨的是....用lock時,萬一發生死結(deadlock),可是會讓你除錯除到不知所云~(很容易發生)程式設計的不好時,還會發生多執行緒lock競爭問題(race conditions)。結果是效能不增反減!

死結(死鎖)!這是最常發生的問題!

研究這門技術時,我看書看資料看了好久好久,並且有加上實作測試和實務應用,最後才慢慢融會貫通,才較知道如何運用多執行緒時避免死結。
在此,將我的防死結研究心得和大家分享!

///////////////////////////////////////////////////////////////////////////


有一種常見的狀況,會發生死結:


struct PlayerNode

{
        long  Number;
        CRITICAL_SECTION  LockInfo;
        PlayerNode  *pNext;
};
PlayerNode *ActorNode;
PlayerNode *EnemyNode;

void gNodeDataSwapFunc( PlayerNode *node1, PlayerNode *node2 )

{
        ::EnterCriticalSection( &node1->LockInfo );
        ::EnterCriticalSection( &node2->LockInfo );
    
        long temp = node1->Number;
        node1->Number = node2->Number;
        node2->Number = temp;

        ::LeaveCriticalSection( &node2->LockInfo );

        ::LeaveCriticalSection( &node1->LockInfo );
}
gNodeDataSwapFunc這個函式看似沒有問題,其實用在多緒程式時,就會發生死結。

假設...有二個執行緒會去呼叫gNodeDataSwapFunc。

Thread-A執行如下:
    gNodeDataSwapFunc( ActorNode, EnemyNode );
Thread-B執行如下:
    gNodeDataSwapFunc( EnemyNode, ActorNode );

各位看出來嚕嗎?這樣就一定會發生死結。

原因是因為...當Thread-A的ActorNode和EnemyNode傳入到gNodeDataSwapFunc,
ActorNode給它lock住後,可能就會馬上發生執行緒切換(context switch),
然後切換到Thread-B來執行,
結果Thread-B呼叫的gListDataSwapFunc傳入的EnemyNode也頭一個就lock住,
然後要在lock這ActorNode時,會發現它已經被其他執行緒lock住了,
所以Thread-B就此進入等待...等待此lock解開後才能繼續往下執行。
接著,系統再度切換回Thread-A這邊,繼續往下執行要lock住EnemyNode,
當然發現它也被其他執行緒lock住了,所以Thread-A也進入等待...
這二個執行緒彼此互相等待對方解開鎖,才能往下執行,
但是都沒辦法,因為都已經進入等待...
此時,就是產生了死結了! 

什麼!還看不出來嗎?

就是說...
ActorNode在Thread-A中lock住了。
EnemyNode在Thread-B中lock住了。
二個執行緒彼此都在等待對方的鎖先解開。
Thread-A會一直等待EnemyNode解開lock後才能往下執行。
Thread-B會一直等待ActorNode解開lock後才能往下執行。
這就是死結嚕!雙方無止盡的互相等待下去!


好的,一個簡單有效的解決方案是...在外圍再加上一個lock.

程式碼示例如下:
void gNodeDataSwapFunc( PlayerNode *node1, PlayerNode *node2 )
{
        ::EnterCriticalSection( &SingleLockInfo );

        ::EnterCriticalSection( &node1->LockInfo );

        ::EnterCriticalSection( &node2->LockInfo );
    
        longtemp = node1->Number;
        node1->Number = node2->Number;
        node2->Number = temp;

        ::LeaveCriticalSection( &node2->LockInfo );

        ::LeaveCriticalSection( &node1->LockInfo );

        ::LeaveCriticalSection( &SingleLockInfo );

}

SingleLockInfo在此就很重要了。

在這種典型的巢狀lock狀況中,就是要規定,一次只能一個thread才能執行裡面的內容!
就能避免多緒時發生死結!

如果可以盡量不要用多重巢狀lock的話,就盡量避開此種寫法,

則能避開死結的發生喔!

///////////////////////////////////////////////////////////////////////////


另一種狀況就是,如果...有必要同時lock很多個鎖時,

而且lock的先後順序也不一致的話,就會產生死結。
下面是會產生死結的範例程式碼:
void g_A_WorkThreadFunc()
{
        ::EnterCriticalSection( &SingleLockInfo1 );
        ::EnterCriticalSection( &SingleLockInfo2 );

        // 其中執行很多程式碼...


        ::LeaveCriticalSection( &SingleLockInfo2 );

        ::LeaveCriticalSection( &SingleLockInfo1 );
}
void g_B_WorkThreadFunc()
{
        ::EnterCriticalSection( &SingleLockInfo2 );
        ::EnterCriticalSection( &SingleLockInfo1 );

        // 其中執行很多程式碼...


        ::LeaveCriticalSection( &SingleLockInfo2 );

        ::LeaveCriticalSection( &SingleLockInfo1 );
}上述會發生死結的原因也是跟之前說明的一樣.
肇因就是不同執行緒要用相同的一堆鎖時,他們的lock順序不一致的關係.


我自己的解法是...替每一組相關的鎖加上編號,要使用鎖時,

則要按照鎖的編號順序來lock,這樣就能容易的避開人為疏失!
所以記得使用時,不能任意顛倒編號來lock喔!

這樣用說的可能很難理解...直接用程式碼來解釋吧!

程式碼示例如下:
void g_A_WorkThreadFunc()
{
        // 要按照順序來lock (這邊用三個鎖)
        ::EnterCriticalSection( &SingleLockInfo1 );
        ::EnterCriticalSection( &SingleLockInfo2 );
        ::EnterCriticalSection( &SingleLockInfo3 );

        // 其中執行很多程式碼...


        // unlock的順序就無所謂,沒有那麼重要了

        ::LeaveCriticalSection( &SingleLockInfo3 );
        ::LeaveCriticalSection( &SingleLockInfo2 );
        ::LeaveCriticalSection( &SingleLockInfo1 );
}
void g_B_WorkThreadFunc()
{
        // 要按照順序來lock (這邊只用二個鎖)
        ::EnterCriticalSection( &SingleLockInfo2 );
        ::EnterCriticalSection( &SingleLockInfo3 );

        // 其中執行很多程式碼...


        // unlock的順序就無所謂,沒有那麼重要了

        ::LeaveCriticalSection( &SingleLockInfo3 );
        ::LeaveCriticalSection( &SingleLockInfo2 );
}



OK ! enjoy it.  


11/12/2013

多執行緒(multi-thread) - 發生死結(死鎖)(deadlock)時,該如何除錯?

相信用過多緒的人,一定對於發生死結後不知道卡在哪裡,無法除錯,感到很懊惱吧!
我有一個方式,有機會讓你抽絲剝繭的找出死結錯誤喔。

原理就是...先使用SetUnhandledExceptionFilter來設定當機例外處理,
之後創建一個定時執行緒,功用是定時檢查程式是否卡死,
若卡死,則故意讓程式當機,藉此觸發當機例外處理。
接著就能使用MiniDumpWriteDump來將所有記憶體都dump到檔案中。
最後就能用VC開啟此dump檔,慢慢的來觀察相關記憶體,找出死結所在。

使用場合:
1. client程式在玩家這邊執行時
2. server程式在機房執行時
就把dmp檔案拿回來後,趕快去熬夜修正死結吧!(悲慘ing)


〔1〕先將下述一大段程式碼,放置你的專案中

#define DUMP_TYPE (MINIDUMP_TYPE)(MiniDumpNormal | MiniDumpWithDataSegs | MiniDumpWithProcessThreadData | MiniDumpWithHandleData | MiniDumpWithUnloadedModules | MiniDumpScanMemory | MiniDumpWithIndirectlyReferencedMemory | MiniDumpWithFullMemory)

static void gDumpMiniDump( PEXCEPTION_POINTERS pExInfo )
{
    char buff_file_name[ MAX_PATH ] = {0};
    SYSTEMTIME time;
    ::GetLocalTime( &time );

#ifdef DEF_I_AM_CLIENT
    Tool::sSprintf( buff_file_name, MAX_PATH, "ClientDumpFile_%04d%02d%02d_%02d%02d%02d.dmp", time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute, time.wSecond );
#else
    Tool::sSprintf( buff_file_name, MAX_PATH, "ServerDumpFile_%04d%02d%02d_%02d%02d%02d.dmp", time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute, time.wSecond );
#endif

    HANDLE lhDumpFile = ::CreateFile( buff_file_name, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL ,NULL );
    if( lhDumpFile != INVALID_HANDLE_VALUE )
    {
        MINIDUMP_EXCEPTION_INFORMATION loExceptionInfo;
        loExceptionInfo.ExceptionPointers = pExInfo;
        loExceptionInfo.ThreadId = ::GetCurrentThreadId();
        loExceptionInfo.ClientPointers = TRUE;

        BOOL flag = ::MiniDumpWriteDump( ::GetCurrentProcess(),
                        ::GetCurrentProcessId(), lhDumpFile, DUMP_TYPE, &loExceptionInfo, NULL, NULL );
        if( !flag )
        {
        //    D_WARNING();
        }
        ::CloseHandle( lhDumpFile );
    }
    else
    {
    //    D_WARNING();
    }

//    GameEventInterface::sOnCrash();
}

static LONG WINAPI gCustomUnhandledExceptionFilter( PEXCEPTION_POINTERS pExInfo )
{
    gDumpMiniDump( pExInfo );
    return EXCEPTION_EXECUTE_HANDLER;
}

void gCreateDebugCrashDump()
{
    // 設定例外當機捕捉
    ::SetUnhandledExceptionFilter( gCustomUnhandledExceptionFilter );
}

〔2〕 然後,在程式一啟動時,立即呼叫gCreateDebugCrashDump();
作用是先設定好VC的當機例外處理。



〔3〕 創建一個定時執行緒(此是虛擬碼)
UINT WINAPI CasualServer::sCheckDeadlockThreadProc( LPVOID lpParam )
{
    CasualServer *pThis = (CasualServer*)lpParam;

    while( !gApp->IsShutdown() )
    {
        pThis->DebugSingleRun_Lock.Lock();
        BOOL flag = pThis->SendPocketBreak.DelayTimeFunc();
        pThis->DebugSingleRun_Lock.Unlock();
        if( flag )
        {
            Debug::SBox( "CasualServer::sCheckDeadlockThreadProc   timeout, prepare to dump file." );
            char *p = NULL;
            *p = 1; // 故意當機
        }
        ::Sleep( 50 );
    }
    return 0;
}

我的SendPocketBreak計時器是設定5分鐘會觸發,我會在主迴圈不斷的將計時器清為0,
當死結發生時,SendPocketBreak計時器就不會被清除。
(這個定時檢查的條件依據各自程式不同,而會改變,請自行決定定時的條件)
然後累積5分鐘到時,我就讓程式故意當機,就會觸發當機例外處理。
會產生一個蠻大的dmp檔案,產生的時間和大小依據使用的記憶體多寡而不同。
(約幾十MB到幾百MB)

有了dmp後,要如何使用呢?
把dmp搬移到你exe執行檔的位置,然後點擊dmp二下,會開啟VC,
然後選擇左方專案列表的功能,就能開始使用並定位到當機時的位置。
以我的話,就能靠gApp這個全域主體物件指標,來開始逐一檢查嚕。

PS:當你的程式發佈出去時,記得要將此專案的全部資料都備份起來(包含各種debug暫存檔),這樣到時dmp才能順利定位。
如果你exe重新編譯後,又使用舊版的dmp開啟的話,則無法定位喔!VC會找不到對應的位址。



thanks for reading. :D

9/10/2013

對於記憶體池(memory pool)的一些經驗

在我早期寫windows程式時,我總是喜歡使用『動態配置記憶體』。
需要記憶體的時候,就跟作業系統new一下即可,不用時則delete歸還系統。
後來發現,常常這樣搞的話,
會讓作業系統容易發生『記憶體破碎』(memory fragmentation)等相關問題。

『記憶體破碎』問題如下:

重複進行許多次不同容量記憶體區塊的配置(new)和釋放(delete)後,
會在堆積記憶體(heap memory)中留下許多零碎的、系統很難再去使用的小區塊,
這就是所謂的記憶體碎片問題。
此問題的發生取決於系統的分配機制。
碎片可能會增加配置時間,因而降低程式執行時的性能,
也可能會導致記憶體耗盡,造成程式可能發生當機。

『動態配置記憶體』缺點如下:

[1]隨著使用次數的增加,而開始發生記憶體破碎問題。
[2]容易發生人為疏忽的記憶體洩漏(memory leak)問題。
[3]每次配置新記憶體區塊時,都會再附加額外的檔頭資訊,用多了則記憶體消耗更大。
[4]配置和釋放動態記憶體的速度可能會越來越慢。
[5]最後有可能會發生記憶體配置失敗的問題,而導致程式當機。

但是我當時還是不管此問題,繼續這麼做,我只想求方便!

因為我認為我是寫Windows平台的單機遊戲,能運用的記憶體很大。

直到這幾年我開始寫伺服器程式時,才發現若是繼續依照慣例來寫程式,

則很快的會讓伺服器程式發生記憶體配置失敗和效能低落等問題,
我才開始找更好的解決方案!

期間找很多資料研究如何設計伺服器程式,也問過工作上的程式朋友他的作法,

得到一個結論,要撰寫高效穩定的伺服器程式,務必得使用『記憶體池』(memory pool)。

記憶體池,就是程式啟動時,預先配置一塊所需使用的超大記憶體,

然後,後續的所有記憶體配置請求都由此配給出去。
這樣就能完全解決上述的種種問題喔!
不過,使用新技巧,當然也會帶來新的問題。

傳統的記憶體池操作方式都是一樣擁有『配置和釋放』二種功能,

所以用久了,也是會有效能上的問題!

而根據我對效能的嚴格要求,我則有不同的作法。

我選擇以空間換取時間,因為遊戲設計是很講究效能的!
我的作法程序如下:
[1]先配置一塊經過需求計算的超大記憶體。
[2]然後用一組『記憶體池操作類別』來做各種運用。
[3]直到程式結束時才釋放整塊記憶體池!

此重點在於第二點,一組『記憶體池操作類別』。

例如:
要操作串列時,『記憶體池操作串列類別』會先從記憶體池被配置出來,
然後使用者就可進行運用,期間若是有需要釋放節點時,
則是由『記憶體池操作串列類別』自己回收起來即可(不是做真正的釋放),
下次使用者要配置新節點時,則從已回收節點優先分配出去。

此種記憶體池和操作的設計優點如下:

[1]避免了記憶體破碎所衍生的問題。
[2]避免了配置和釋放記憶體所花費的效能問題,所以速度極佳。
[3]極適合伺服器應用程式的長期運作。
[4]記憶體間是緊密相鄰的,所以能提高系統cache命中率,進而提高軟體效能。

缺點是此大塊記憶體一直霸佔著,直到程式結束!

不過,對遊戲來說,這也不算缺點!
因為遊戲軟體本身就是一種很操作業系統的軟體!
在要求高效能的前提下,先將系統資源霸佔起來,
遊戲中才能有較好的效能。
所以,先計算好會使用到多少記憶體,對遊戲來說是必須的!
而且,其實遊戲中最佔記憶體的是屬於圖形部份!
若要省記憶體,就請在此下功夫吧!


記憶體池操作串列類別  程式碼範例如下:

//===================================================
template < class AVATAR >
struct MPNode2 : AVATAR
{
    MPNode2< AVATAR >  *pPrev;
    MPNode2< AVATAR >  *pNext;
};

template < class AVATAR >
struct AV_MemoryPoolList2 // 記憶池雙向串列類別
{
    //------------------------------------------------------------
    MPNode2< AVATAR >  *pBufferList;
    DWORD               BufferListCount;
    MPNode2< AVATAR >  *pFreeList;
    DWORD               FreeListCount;
    DWORD               NodeUseCount;
    //------------------------------------------------------------
    inline void  Create( DWORD object_max )
    {
        pFreeList = NULL;
        FreeListCount = 0;
        NodeUseCount = 0;

        BufferListCount = object_max;
        const DWORD object_size = sizeof( MPNode2< AVATAR > );
        pBufferList = (MPNode2< AVATAR >*)AV_KernelMemoryPool::sMemoryAllocate( object_size * object_max );
    }
    //------------------------------------------------------------
    // 由外部傳入pbuffer_list記憶體, 只將AV_MemoryPoolList2當作介面來操作
    inline void  CreateLink( DWORD object_max, MPNode2< AVATAR > *pbuffer_list )
    {
        pFreeList = NULL;
        FreeListCount = 0;
        NodeUseCount = 0;

        BufferListCount = object_max;
        pBufferList = pbuffer_list;
    }
    //------------------------------------------------------------
    inline AVATAR*  GetNode()
    {
        MPNode2< AVATAR > *node = NULL;
        if( FreeListCount > 0 )
        {
            node = pFreeList;
            pFreeList = pFreeList->pNext;
            --FreeListCount;
        }
        else
        {
            if( BufferListCount > 0 )
            {
                node = pBufferList;
                ++pBufferList;
                --BufferListCount;
            }
            else
            {
                Debug::SBox( "AV_MemoryPoolList2< %s >::GetNode   allocate a new node.", typeid( AVATAR ).name() );
                node = (MPNode2< AVATAR >*)AV_KernelMemoryPool::sMemoryAllocate( sizeof( MPNode2< AVATAR > ) );
            }
        }

        node->pPrev = NULL;
        node->pNext = NULL;
        ++NodeUseCount;
        return node;
    }
    //------------------------------------------------------------
    inline BOOL  CheckSpaceFull()
    {
        if( FreeListCount == 0 && BufferListCount == 0 )
            return TRUE;
        return FALSE;
    }
    //------------------------------------------------------------
    inline void  ReleaseNode( AVATAR *&release_node )
    {
        ZeroMemory( release_node, sizeof( MPNode2< AVATAR > ) );

        if( FreeListCount > 0 )
            ((MPNode2< AVATAR >*)release_node)->pNext = pFreeList;
        pFreeList = (MPNode2< AVATAR >*)release_node;
        release_node = NULL;
        ++FreeListCount;
        --NodeUseCount;
    }
    //------------------------------------------------------------
    inline void  ReleaseNodeNoClean( AVATAR *&release_node )
    {
        ((MPNode2< AVATAR >*)release_node)->pPrev = NULL;
        ((MPNode2< AVATAR >*)release_node)->pNext = NULL;

        if( FreeListCount > 0 )
            ((MPNode2< AVATAR >*)release_node)->pNext = pFreeList;
        pFreeList = (MPNode2< AVATAR >*)release_node;
        release_node = NULL;
        ++FreeListCount;
        --NodeUseCount;
    }
    //------------------------------------------------------------
};

//===================================================
template < class AVATAR > // 記憶池雙向串列類別( 有包含pHeadNode連結 ) struct AV_MemoryPool2HeadList : AV_MemoryPoolList2< AVATAR > {     //------------------------------------------------------------     MPNode2< AVATAR > *pHeadNode;     MPNode2< AVATAR > *pTailNode;     DWORD              LinkNodeCount;     //------------------------------------------------------------     inline void  Create( DWORD object_max )     {         AV_MemoryPoolList2< AVATAR >::Create( object_max );         pHeadNode = NULL;         pTailNode = NULL;         LinkNodeCount = 0;     }     //------------------------------------------------------------     inline void  CreateLink( DWORD object_max, MPNode2< AVATAR > *pbuffer_list )     {         AV_MemoryPoolList2< AVATAR >::CreateLink( object_max, pbuffer_list );         pHeadNode = NULL;         pTailNode = NULL;         LinkNodeCount = 0;     }     //------------------------------------------------------------     inline void  Link( AVATAR *link_node )     {         MPNode2< AVATAR > *node = (MPNode2< AVATAR >*)link_node;         node->pPrev = NULL;         node->pNext = NULL;         if( !pHeadNode )         {             pTailNode = pHeadNode = node;         }         else         {             pTailNode->pNext = node;             node->pPrev = pTailNode;             pTailNode = node;         }         ++LinkNodeCount;     }     inline void  Unlink( AVATAR *unlink_node )     {         MPNode2< AVATAR > *node = (MPNode2< AVATAR >*)unlink_node;         if( node->pPrev )         {             if( node->pNext ) // middle node             {                 node->pPrev->pNext = node->pNext;                 node->pNext->pPrev = node->pPrev;             }             else // tail node             {                 if( pTailNode == node )                 {                     pTailNode = pTailNode->pPrev;                     pTailNode->pNext = NULL;                 }                 else                     Debug::SBox( "AV_MemoryPool2HeadList< %s >::Unlink   if( pTailNode != node )", typeid( AVATAR ).name() );             }         }         else // head node         {             if( pHeadNode == node )             {                 if( pHeadNode->pNext )                 {                     pHeadNode = pHeadNode->pNext;                     pHeadNode->pPrev = NULL;                 }                 else                 {                     pHeadNode = NULL;                     pTailNode = NULL;                 }             }             else                 Debug::SBox( "AV_MemoryPool2HeadList< %s >::Unlink   if( pHeadNode != node ) it will lose the node.", typeid( AVATAR ).name() );         }         node->pPrev = NULL;         node->pNext = NULL;         --LinkNodeCount;     } };
//===================================================
template < class AVATAR, DWORD ARRAY_MAX > // 記憶池雙向串列類別(用陣列當pBufferList的緩衝區) struct AV_MemoryPool2HeadArrayList : AV_MemoryPool2HeadList< AVATAR > {     //------------------------------------------------------------     MPNode2< AVATAR > NodeBuffer[ ARRAY_MAX ];     //------------------------------------------------------------     inline void  Create()     {         ZeroMemory( NodeBuffer, sizeof( NodeBuffer ) );         AV_MemoryPool2HeadList< AVATAR >::CreateLink( ARRAY_MAX, NodeBuffer );     } };


7/05/2007

introduction myself.

我是一位曾在遊戲公司風風雨雨奮戰多年的遊戲程式設計師. 
目前以遊戲研發工作室的狀態繼續活躍著! 

遊戲程式設計專長: 

1. C++ / OOP 
2. DirectX / 3D 
3. RPG / ARPG 
4. Fruit Slot / Tiger Slot / Mahjong
5. Winsock / IOCP / Online game 
6. Unity engine

並打造一個超過13年經驗累積、專業的Avatar Game Engine. 


使用的程式開發工具: 

1. Visual C++ 2008 Express Edition 
2. Embarcadero RAD Studio 2010 
3. Unity engine 4.2 + NGUI

過往參與製作的遊戲作品: 

1- 翔翔傳說(RPG)- 1999年 
 從國中開始自學遊戲程式,於民國88年2月退伍後面試的DOS遊戲DEMO。 
 但因公司需要視窗遊戲程式師,所以我沒被錄取。 

2- 魔導物語(SLG)- 1999年 

 第一份工作是擔任遊戲企劃,工作是多款遊戲新聞稿和說明書撰寫等。 
 期間仍不忘自學視窗程式,以便於之後能順利轉任程式師。 

3- 少林十八銅人(SLG)- 2000年 

 終於開始成為正式的遊戲程式設計師,好感動~ 
 先製作一些遊戲工具後,再接下此程式改版任務。 

4- 吸血鬼學園(RPG)- 2000年 

 負責程式主系統,遺憾的是研發半年後因為企劃問題導致案子終結。 
 同時感謝主管鄭先生的教導,讓我很快瞭解如何用好的方式設計遊戲。 

5- 劍靈一代(RPG)- 2001年 

 負責戰鬥系統,兼任主系統除錯維護,而戰鬥系統做的很有魄力喔! 
 專案過程也是經過一些人事風雨,所幸能順利熬過,讓遊戲上市。 

6- 三國梟雄傳-曹操篇(ARPG)- 2002年 

 承接外包案,負責全部程式,遺憾的是程式快完成時發包公司結束營業。 

7- 南俠展昭痞子龍(ARPG)- 2003年 

 負責全部程式和專案部份統籌,此套是我在程式面很滿意的一套喔。 
 並配合公司銷售時程,所以每天趕工,半年多就完成嚕~ 
 加上朋友阿利的MMX技術支援,讓我進展更順利。 
 之後還有改版成簡體版。 

8- 童話物語(ARPG)- 2003年 

 負責遊戲引擎技術提供和支援。 

9- 古文明大冒險(ARPG)- 2003年 

 一個展示成熟技術的DEMO。 

10- 光之國度online(MMORPG)- 2003~2004年 

 擔任研發一部經理,負責遊戲引擎、專案管理、團隊管理。 
 此次的研發過程最為辛苦又黑暗...公司結束後, 
 我繼續帶領本團隊和大陸某大遊戲研發公司達成投資合作, 
 計畫在台灣成立研發分公司,但仍一波多折,如履薄冰, 
 最終此專案被迫終結,宣告失敗~ 

11- 仿魔法氣泡online(PZG)- 2004年 

 承上,接著再負責本案全部伺服端、客戶端程式,順利完成。 
 本團隊做了眾多專案,仍無奈於團隊整體競爭力不夠,最終宣告解散團隊~ 

12- 仿淡水阿給online(ACT)- 2005年 

 負責全部伺服端和客戶端程式,遺憾的是公司結束。 
 (公司又~~~倒了,遊戲公司倒閉速度之快,令人咋舌) 

 這幾年潛心研究3D/Server programming,略有小成,同時繼續進修英文學業。 


13- 水果盤online - 2008年 

 開始以研發工作室型態承接遊戲外包, 
 負責伺服端(不含DB)和客戶端程式,已製作完成,現正營運中。 

14- 3D麻將online - 2009~2011年12月 

 負責伺服端(不含DB)和客戶端程式,歷經改版後,現正營運中。 

15- 象棋online - 2011年~2012年 

 用我的game engine協助我的夥伴承接此遊戲,可惜因故沒有完成。 

16- 5輪15線online - 2011~2012年10月 

 用我的game engine協助我的夥伴承接此遊戲並完成上市,現正營運中。 

17- 唐博虎SLOT online - 2013年11月~2014年1月
 用水果盤專案去改的產品。現正營運中。

學歷: 
1. 南山商工- 美工科(畢) 
2. 致理技術學院- 應用英語科(畢)

兵役: 

憲兵學校志願役預士214期 
憲兵學校戰技班58期11隊 
(213營第一連-陽明山憲兵分隊-上士榮退) (兵役期:三年半) 

座右銘:

你的時間花在哪裡,你的成就就在哪裡。

早期部份作品集:
http://album.blog.yam.com/avatar996

領導者修練明師:吳建宏資深顧問 

http://nn919.pixnet.net/blog