イベントハンドラを登録したスレッドでデリゲートしてねっていうクラス


せっかく下にお勧めしたので、ネタ的に作成してみた。

ヘッダーにwindows.hを記述したくなかったのと

テンプレート名のSignatureに依存したくなかったので

TypeErasureするためにbindしまくってみた。invoke_helperはpimplっぽく?してみたつもりで

呼び出しの引数は参照つけてるけど・・その辺はどうしたらいいのか謎。->追記へ

あとは、ネーミングセンス爆発しろみたいになった。ちょっと読みにくいけど許してください。

//synchronized_event_delegator.h

#pragma once

#include <utility>
#include <vector>

#include <boost/function.hpp>
#include <boost/bind.hpp>

namespace umyu
{
	struct invoke_helper
	{
		typedef boost::function<void ()> callback_handler_type;

		static void* get_thread_id();
		static void invoke(void* thread_id,callback_handler_type* handler);
	};

	template <typename Signature>
	struct synchronized_event_delegator
	{
		typedef Signature delegate_function_type;
		typedef boost::function<delegate_function_type> delegate_handler_type;

		synchronized_event_delegator& operator+=(const synchronized_event_delegator& delegator)
		{
			std::copy(delegator.handlers_.begin()
				,delegator.handlers_.end(),std::back_inserter(handlers_));
			return *this;
		}

		synchronized_event_delegator& operator+=(const delegate_handler_type& handler)
		{
			handlers_.push_back(std::make_pair(invoke_helper::get_thread_id(),handler));
			return *this;
		}

		template <typename Sig>
		synchronized_event_delegator& operator+=(const boost::function<Sig>& handler)
		{
			handlers_.push_back(std::make_pair(invoke_helper::get_thread_id(),handler));
			return *this;
		}

		void operator()() const
		{
			std::vector<std::pair<void*,delegate_handler_type> >::const_iterator itr=handlers_.begin();
			std::vector<std::pair<void*,delegate_handler_type> >::const_iterator itr_end=handlers_.end();
			for(itr;itr!=itr_end;++itr)
				invoke_helper::invoke(itr->first
					,new invoke_helper::callback_handler_type(itr->second));
		}

		template <typename Sender>
		void operator()(Sender& sender) const
		{
			std::vector<std::pair<void*,delegate_handler_type> >::const_iterator itr=handlers_.begin();
			std::vector<std::pair<void*,delegate_handler_type> >::const_iterator itr_end=handlers_.end();
			for(itr;itr!=itr_end;++itr)
				invoke_helper::invoke(itr->first
					,new invoke_helper::callback_handler_type(
					boost::bind(itr->second,boost::ref(sender))));
		}

		template <typename Sender,typename Arg1>
		void operator()(Sender& sender,Arg1& arg1) const
		{
			std::vector<std::pair<void*,delegate_handler_type> >::const_iterator itr=handlers_.begin();
			std::vector<std::pair<void*,delegate_handler_type> >::const_iterator itr_end=handlers_.end();
			for(itr;itr!=itr_end;++itr)
				invoke_helper::invoke(itr->first
					,new invoke_helper::callback_handler_type(
					boost::bind(itr->second,boost::ref(sender),boost::ref(arg1))));
		}

		void reset()
		{
			handlers_.clear();
		}

		std::vector<std::pair<void*,delegate_handler_type> > handlers_;
	};
}
//synchronized_event_delegator.cpp

#include "synchronized_event_delegator.h"

#define _WIN32_WINNT 0x0501
#include <windows.h>

namespace umyu
{
	VOID APIENTRY invoke_helper_static_handler(ULONG_PTR handler)
	{
		invoke_helper::callback_handler_type* invoke_handler=(invoke_helper::callback_handler_type*)handler;
		(*invoke_handler)();
		delete invoke_handler;
	}
	void* invoke_helper::get_thread_id()
	{
		HANDLE regist_thread;
		DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),GetCurrentProcess()
			,&regist_thread,0,FALSE,DUPLICATE_SAME_ACCESS);
		return (void*)regist_thread;
	}
	void invoke_helper::invoke(void* thread_id,callback_handler_type* handler)
	{
		QueueUserAPC(&invoke_helper_static_handler,(HANDLE)thread_id,(ULONG_PTR)handler);
	}

}

メインスレッドはSleepEx(0,TRUE)でプーリングを忘れずに。


追記
上記だと、呼び出し方によってスライシングしたり値でコピーされたりしますね。
以下のように参照もboost::refもはずして、呼び出す側でboost:refするように変更しました。

		template <typename Sender,typename Arg1>
		void operator()(Sender sender,Arg1 arg1) const
		{
			std::vector<std::pair<void*,delegate_handler_type> >::const_iterator itr=handlers_.begin();
			std::vector<std::pair<void*,delegate_handler_type> >::const_iterator itr_end=handlers_.end();
			for(itr;itr!=itr_end;++itr)
				invoke_helper::invoke(itr->first
					,new invoke_helper::callback_handler_type(
					boost::bind(itr->second,sender,arg1)));
		}

		...
		synchronized_event_delegator<void (file&,unsigned long)> on_close;
		...
		on_close(boost::ref(*this),error_code);

スレッドプールからのイベント呼び出し

スレッドプールからのイベント呼び出しを行う場合、そのイベントによって呼び出される側のハンドラは

当然スレッドセーフでじゃないといけない。

でも、場合によってはスレッドプールのワーカースレッドではなくて、メインのスレッドで処理したいハンドラも出てくる。

毎回スレッドセーフにするのも面倒だからね。


この場合、スレッドプールには、たとえばWindowsのEventオブジェクトを渡しておいて

処理終了時にシグナルにしてもらうなどの方法が思い浮かぶけど

この方法だとメインスレッドはイベントを登録したクラスごとにEventオブジェクトを毎回調べるために

Check関数を用意してプーリングして呼び出さないといけなくなる。

while(1)
{
	window.checkEvent();
	file.checkEvent();
	socket.checkEvent();
	...


これも結局面倒だよね。ってことでAPCキューを利用するのお勧めです。

windowsのスレッドはAPC(Asynchronous Procedure Call)キューと呼ばれるキューをもってます。

そのキューに関数を登録しておいて、スレッドが警告可能な状態(アラート可能な状態)になると

登録されている関数が実行されるという仕組みです。


警告可能な状態ってのは何かって言うとスレッドが一瞬でも待機する状態です。

このネーミングもどうかと思いますが・・・

これは、たとえばSleepEx,WaitForSingleObjectEx,WaitForMultipleObjectsExなどを使います。

待機するのでパフォーマンスが心配するかもしれませんが

これらの関数はAPCキューに登録された関数がなければ、そのまま指定された時間で処理が帰ってきます。

『SleepExとQueueUserAPCの割り込みパフォーマンス』
http://www.cycleof5th.com/tips/index.php?date=2007-07-13&lang=en

さらに、上記のサイトをみると詳細がわかりますが、そこまで悪くなさそうです。


さて、ワーカースレッドのほうメインスレッドのAPCキューに関数を登録することになりますが

QueueUserAPCというAPIを使います。

これはまぁ・・ぐぐってください。そのままですので分かるはずですしw


この方法のメリットはどのクラスで登録されてもプーリングの処理が同じというところでしょうか。

while(1)
{
	SleepEx(0,TRUE)
	...


たったこれだけでどんなクラスでもワーカースレッドからのinvokeを受け取れますので便利ですよー

ネットワーク部分の抽象化


MMOを作る場合に、まず初めに考えるのはサーバークライアント間の通信です。そのためにsocketクラスを誰もが作成すると思います。

フルスクラッチでも他のライブラリの薄いラッパーでもかまわないと思いますが仮にそのsocketクラスができたとしましょう。

で、次にどうすべきか・・。通信はできるようになったんだけど、どうしていいか分からない。

たくさんのキャラクターがいて、送るべき情報がいろいろあって、考え出すとなかなかカオスです。

特にサーバー側の構成をどうするのか。ここをどうするかはMMOの開発での第一の関門かもしれません。

前回のときもここの部分は悩みましたので、この辺のわたしなりの考え方を書いてみたいと思います。


ネットワークのゲームを作成する場合にその設計をややこしくしているのは

『同期のための通信はアプリケーション(ソケット)同士がする』

という考え方です。

考え方というよりは、あまり深く考えずに開発していくと考えてなくてもそうなっていきます。

ですので、これについて考えない場合もここに含まれるかもしれません。


もちろん現実は最終的にサーバーアプリケーションとクライアントアプリケーション同士の通信になるのですが

あるキャラクターの情報の同期をとるということと、アプリケーションの通信という二つの間には大きな乖離があります。

これまたどうしたらいいか分からない主要な部分になってしまっています。

通常アプリケーションごとにソケットは存在しますから、アプリケーションのところはソケットとも置き換えられます。



通信は普通ソケット同士でしますし、当たり前なのに、どうしてソケット同士の通信という考え方がいけないのでしょうか。

それは同期するための情報と通信の窓口であるソケットいうのは別次元の違うものだからです。

ちょっと分かりにくいですね。もうすこし分かりやすくいきましょう。


たとえばサーバー側での話なのですが、あるキャラクターがキャラクタークラスのオブジェクトcharacter1として存在したとします。

そしてサーバー側のソケットクラスserver_socket1とクライアントのソケットクラスclient_socket1があります。

このcharacter1が[右にちょっとだけ動きました]というパケットをserver_socket1が受信したとします。

というわけで、サーバーではcharacter1の情報を更新するわけです。


まぁcharacter1.move(character1.x()+1,character.y())のイメージですね。

サーバーではこのclient_socket1の情報が更新されたことをクライアント側に送らなければなりません。

送らないと他のプレイヤーさんにはこのキャラクターが止まったままですからね。

送るパケットの内容は[character1のオブジェクトのid,xが更新されました,新しいxは30です]といった簡単な情報でしょう。

さて、このパケットはいつ誰に対して送ったらいいのでしょうか?


まさにこれが同期をするための情報です。いつ誰に対して送るべきか。

MMOの場合、キャラクターが動いていたら、そのキャラクターが同期すべき情報っていうのは

・そのキャラクターをプレイしているクライアントの画面に描画されている別のクライアントのキャラクター

になるわけです。

もっと簡単に、チャットソフトを作っていて、チャットルームという機能をつけましょうとなったときに

同期すべき情報ってのは

・そのチャットしている人が入ったチャットルームにいる別のチャットしてる人

になるわけですね。


一応ここで役者が3つそろいました。キャラクターと同期をするための情報とソケットです。

ということはCharacterクラスと同期クラスとSocketクラスの3つが必要になりそうです。

同期クラスは上の例のような同期するための情報、たとえば

どのcharacterオブジェクトがどのsocketオブジェクトに結びついているかを管理します。


初めの役者はキャラクターとソケットだけでした。

小規模なネットワークソフトであれば問題ないですが、何も考えないで作り始めると

同期するための情報が最終的にはソケットクラスに実装されがちです。

キャラクターとソケットしかいないため当然そうなっていきます。

つまりソケットクラスの関数のなかで、このキャラクターの近くにいるキャラクターを検索してみたり・・

同期するための情報は通信の窓口であるソケットとは次元の違う情報なのに、そうなっていきます。

カオスが生まれる原因はここにあるわけです。

ここまでくると『同期のための通信はソケット同士がする』ことがなぜいけないか分かると思います。

同期するための情報は同期クラスという別の新しい概念を導入して

情報自体、同期クラス、通信の窓口

とすべきなのです。


この同期するための情報を管理する同期クラスですが、これを便利につくってあげると

かなり通信自体が楽になるとおもいます。

このクラスをどう作るべきかってのもまた難しい話なのですが、今日はここまでにしときましょう。

IO Completion Portで非同期のWriteFile

延々デバッグでしたまる


/^o^\


WriteFileを呼び出し続けてERROR_IO_PENDINGがくるとGetOverlappedResultの最後の引数をTRUEにして

書き込みを待機しないとちゃんと書き込めないという現象がおきていました。

もちろんGetQueuedCompletionStatusから『完了しました』ってエラーもなく通知が来るものだから

ものすごい勢いでWriteFileしてたんだけど、書き込んだファイルみたらちゃんと書き込めてないの

だからって非同期I/Oが売りなのにGetOverlappedResultで待機するなんて意味がない。したくないヤダヤダ><

ReadFileはちゃんとできてるのになあ・・


んでいろいろと考えたんですが、延々考えて最後にぱっとひらめいたのがWinsockのFD_WRITE

FD_WRITEは書き込みが『完了しました』じゃなくてバッファが開いたなどの理由で『書けるようになったよ』だったはず

たぶんこれもそんな感じなんだろーとWriteFileしてGetQueuedCompletionStatusが帰ってきたらWriteFileして・・・

とするように変更したらうまくかけた━━━━━。゚(゚´Д`゚)゚。━━━━━よ!!!!


ちなみに実行速度のほうも上のやり方で問題なさそうです。

ものすごい勢いでWriteFileしてもI/O自体の転送速度が上がるわけじゃないからねぇ・・


それにしても完了ポートとか、なんてややこしい名前なんだお