Unity用のプラグイン、Fast Shadow Receiverを開発したときの話。

Unityは基本的にシングルスレッドで動くゲームエンジンである。マルチコアプロセッサの場合、スキニングの処理はマルチスレッドで行なわれるのだが、スクリプトで書かれた処理はシングルスレッドが大前提である。

しかし、Unityエンジンが管理していないデータを扱うぶんにはマルチスレッドで処理しても良い。驚いたことにUnityのObjectはnullチェックをすることすらマルチスレッドで出来ないのである。Objectは比較演算子をオーバーライドしていて、nullチェックをするにもUnityエンジンにお伺いを立てているようである。 というわけで、メインスレッドでUnityのObjectから情報を集めておいて、その情報をワーカースレッドに渡してバックグラウンドで処理をすることになる。このときに便利なのがSystem.ThreadingのThreadPool

と思ったら、これが驚く程ヒドイものだった。手持ちのiPhone 4Sで試してみたら、このスレッドプールはスレッドを20個も作っていたのである。スレッドプールってハードウェアスレッドの数(SystemInfo.processorCount)だけスレッドを作って、余計なコンテキストスイッチや同期にかかるコストを抑えるために使うと思っていたので、この挙動にはビックリした。これが.NET Frameworkの仕様なのか、Monoの実装が悪いのかは良くわからない。Instrumentsを使ってFast Shadow Receiverのデモのパフォーマンスを計測した結果が下の画像(クリックすると拡大画像が表示されます)。ワーカースレッドの処理が全体の36.7%を占めていて、そのうちの22.6%が同期にかかっているコストなのである! 本来のするべき仕事は12.0%しかないので、同期処理が本来の仕事の2倍近くも時間を費していることになる。しかもメインスレッド側でも、スレッドプールにタスクを投げる処理に8.8%もの時間をかけている。

SystemThreadPoolSystemThreadPool2

というわけで、自分でスレッドプールを実装することにした。自作のスレッドプールを使ってパフォーマンスを計測した結果がこれ。22.6%かかっていた同期のコストは3.4%程度にまで減少。8.8%かかっていたタスクを投げる処理も2.0%に! これはスレッドプールにスレッドを2つ作成した場合の結果で、スレッドを1つだけにした場合、同期のコストは1.2%しかなかった。

NyahoonThreadPoolNyahoonThreadPool2

でもこのスレッドプールには制限があって、それはこのスレッドプールはスレッドセーフには出来ていないということ(笑)。単独のスレッドからしかタスクを投げることしかできないので注意してください。ソースコードはFast Shadow Receiverをご購入いただければもれなく付いてきますが、下にもソースコードを公開しておくので興味のある方は使ってみてください。ただし不具合による責任はとれません。ご使用の際はご自分で十分テストしてください。そしてもし不具合を見つけたらメールとかコメントでお知らせいただければ幸いです。

使い方はInitInstance()でシングルトンインスタンスを初期化して、QueueUserWorkItem()でタスクを投げるだけ。タスクの中で他のスレッドと同期を取ろうとするとデッドロックする可能性があるので、投げっぱなしでいずれ終了するタスクしか扱えません。メインスレッドでタスクの終了を待つのはもちろんOKです。

Fast Shadow Receiverをご購入いただいた方はMeshShadowReceiver.csの中でこのスレッドプールを使っているので参考にしてみてください。

//
// ThreadPool.cs
//
// Copyright 2014 NYAHOON GAMES PTE. LTD. All Rights Reserved.
//
using UnityEngine;
using System.Threading;

namespace Nyahoon {
	/// <summary>
	/// Thread pool.
	/// This class itself is not thread safe. Only a single thread can call QueueUserWorkItem safely.
	/// </summary>
	public class ThreadPool {
		private static ThreadPool s_instance = null;
		public static void InitInstance()
		{
			if (s_instance == null) {
				InitInstance(128, 0);
			}
		}
		public static bool InitInstance(int queueSize, int threadNum)
		{
			if (s_instance != null) {
				Debug.LogWarning("TreadPool instance is already created.");
				return false;
			}
			s_instance = new ThreadPool(queueSize, threadNum);
			return true;
		}
		public static ThreadPool Instance
		{
			get { return s_instance; }
		}
		public static void QueueUserWorkItem(WaitCallback callback, object state)
		{
			s_instance.EnqueueTask(callback, state);
		}
		private Thread[] m_threadPool;
		struct TaskInfo {
			public WaitCallback callback;
			public object args;
		}
		private TaskInfo[] m_taskQueue;
		private int m_nPutPointer;
		private int m_nGetPointer;
		private int m_numTasks;
		private AutoResetEvent m_putNotification;
		private AutoResetEvent m_getNotification;
#if !UNITY_WEBPLAYER
		// according to this page (https://docs.unity3d.com/401/Documentation/ScriptReference/MonoCompatibility.html),
		// Semaphore is not available on web player.
		private Semaphore m_semaphore;
#endif
		private ThreadPool(int queueSize, int threadNum)
		{
#if UNITY_WEBPLAYER
			threadNum = 1;
#else
			if (threadNum == 0) {
				threadNum = SystemInfo.processorCount;
			}
#endif
			m_threadPool = new Thread[threadNum];
			m_taskQueue = new TaskInfo[queueSize];
			m_nPutPointer = 0;
			m_nGetPointer = 0;
			m_numTasks = 0;
			m_putNotification = new AutoResetEvent(false);
			m_getNotification = new AutoResetEvent(false);
#if !UNITY_WEBPLAYER
			if (1 < threadNum) {
				m_semaphore = new Semaphore(0, queueSize);
				for (int i = 0; i < threadNum; ++i) {
					m_threadPool[i] = new Thread(ThreadFunc);
					m_threadPool[i].Start();
				}
			}
			else
#endif
			{
				m_threadPool[0] = new Thread(SingleThreadFunc);
				m_threadPool[0].Start();
			}
		}
		private void EnqueueTask(WaitCallback callback, object state)
		{
			while (m_numTasks == m_taskQueue.Length) {
				m_getNotification.WaitOne();
			}
			m_taskQueue[m_nPutPointer].callback = callback;
			m_taskQueue[m_nPutPointer].args = state;
			++m_nPutPointer;
			if (m_nPutPointer == m_taskQueue.Length) {
				m_nPutPointer = 0;
			}
#if !UNITY_WEBPLAYER
			if (m_threadPool.Length == 1) {
#endif
				if (Interlocked.Increment(ref m_numTasks) == 1) {
					m_putNotification.Set();
				}
#if !UNITY_WEBPLAYER
			}
			else {
				Interlocked.Increment(ref m_numTasks);
				m_semaphore.Release();
			}
#endif
		}
#if !UNITY_WEBPLAYER
		private void ThreadFunc()
		{
			for (;;) {
				m_semaphore.WaitOne();
				int nCurrentPointer, nNextPointer;
				do {
					nCurrentPointer = m_nGetPointer;
					nNextPointer = nCurrentPointer + 1;
					if (nNextPointer == m_taskQueue.Length) {
						nNextPointer = 0;
					}
				} while (Interlocked.CompareExchange(ref m_nGetPointer, nNextPointer, nCurrentPointer) != nCurrentPointer);
				TaskInfo task = m_taskQueue[nCurrentPointer];
				if (Interlocked.Decrement(ref m_numTasks) == m_taskQueue.Length - 1) {
					m_getNotification.Set();
				}
				task.callback(task.args);
			}
		}
#endif
		private void SingleThreadFunc()
		{
			for (;;) {
				while (m_numTasks == 0) {
					m_putNotification.WaitOne();
				}
				TaskInfo task = m_taskQueue[m_nGetPointer++];
				if (m_nGetPointer == m_taskQueue.Length) {
					m_nGetPointer = 0;
				}
				if (Interlocked.Decrement(ref m_numTasks) == m_taskQueue.Length - 1) {
					m_getNotification.Set();
				}
				task.callback(task.args);
			}
		}
	}
}

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です


1 × = 二

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>