スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
[ --年--月--日 --:--:-- : スポンサー広告 ]

【Unity】 スレッドを使う

Unityでスレッドを使ってみるテスト。

アプリやゲームを作っているとほぼ100%ぶつかる問題、重たい同期処理(ブロッキング処理)
1フレームに掛かる処理が重たいと画面が引きつったように見えたり、入力の反応が無くなったりと
何かとデメリットばかりである。

世に出ている軽快なアプリはそれらを無くすためにおそらく様々な努力がなされている。
その1つとして、同期処理の非同期化はまず欠かせない。


というわけでUnityで非同期処理をしたくなったら、まず2つの手段がある。
コルーチンスレッドだ。

この2つは同じようで実は全然違う。
各々の特徴を簡潔にまとめてみた。


コルーチン

・メインスレッドで処理される
→なのでyieldで分けて処理を分散しても、その1つ1つの処理自体が重いと全体に影響する
・IEnumerator型の戻り値のメソッドである必要がある
・UnityのAPIが使える


スレッド

・別スレッドで処理されるので、メインスレッドの負荷に影響が無い
UnityのAPIが使えない



どちらを使えば良いのかは、この特徴を良く把握してからでないと判断出来ないと思う。
これだけは言えるのは、UnityのAPIを使わない重たい処理であれば必ずスレッドを使え、ただそれだけ。

コルーチンは結局メインスレッドで回っているので、
その中の1フレームで行われる処理が重いとどうしようもない
からだ。


スレッドと言っても、初めて触る人はどのように使うのか多分イメージしづらいと思う。
そこで簡単なサンプルを書いてみた。


ThreadManager

・起動時にスレッドを生成し、その中にスレッドで処理するメソッド(A)を渡す
・Aは登録されたジョブの先頭1個だけを取り出し、処理するだけ。whileループでメインスレッドとは異なるスピードで毎回回る。
・ジョブは重たい処理を仮で行っている。


using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Threading;

public class ThreadManager : MonoBehaviour
{
//------------------------------------------------------------
// class Job
//------------------------------------------------------------
public class Job
{
// ctor
public Job( System.Action< ThreadManager.Job > callback = null )
{
IsEnd = false;
Count = 0;
CallBack = callback;
}
// some blocking work
public void LoopCount()
{
while( Count < 100000000 )
{
Count ++ ;
}
}
// property
public bool IsEnd {
get;
set;
}
public int Count {
get;
set;
}
public System.Action< ThreadManager.Job > CallBack {
get;
set;
}
}

//------------------------------------------------------------
// Awake
//------------------------------------------------------------
void Awake()
{
m_Thread = new Thread( threadWork );
m_Thread.Start();
}
//------------------------------------------------------------
// Update
//------------------------------------------------------------
void Update()
{
m_JobCount = m_JobList.Count;
}
//------------------------------------------------------------
// OnApplicationQuit
//------------------------------------------------------------
void OnApplicationQuit()
{
m_Thread.Abort();
}

//------------------------------------------------------------
// job process on thread
//------------------------------------------------------------
private void threadWork()
{
while( true )
{
m_Thread.Sleep(0);

// if exist job, do job.
if( m_JobList.Count > 0 )
{
// only 1 work in this sample
ThreadManager.Job job;
lock( m_SyncObj ){
job = m_JobList[0];
}

if( !job.IsEnd )
{
Debug.Log( "job start." );

job.LoopCount(); // job work
job.IsEnd = true;

// do callback
if( job.CallBack != null ){
job.CallBack( job );
}

Debug.Log( "job end." );

// remove job
lock( m_SyncObj )
{
m_JobList.Remove( job );
job = null;
}
}
}
}
}

//------------------------------------------------------------
// request work
//------------------------------------------------------------
public void RequestWork( System.Action< ThreadManager.Job > callback = null )
{
lock( m_SyncObj )
{
var job = new ThreadManager.Job( callback );
m_JobList.Add( job );
}

Debug.Log( "request job." );
}

//------------------------------------------------------------
// member
//------------------------------------------------------------
private Thread m_Thread;
private List< ThreadManager.Job > m_JobList = new List< ThreadManager.Job >();
private Object m_SyncObj = new Object();
public int m_JobCount;
}


ThreadController

・起動時にThreadManagerのインスタンス(B)を拾ってくる
・キーボードのAボタンを押すと、Bにジョブリクエストを投げる
・キーボードのSボタンを押すと、Bにジョブリクエストを投げる+ジョブ終了時のコールバックを登録する。(終了時のジョブの値にアクセス可)


using UnityEngine;
using System.Collections;

public class ThreadController : MonoBehaviour
{
//------------------------------------------------------------
// Awake
//------------------------------------------------------------
void Awake()
{
var thread_mgr = GameObject.Find( "ThreadManager" ) as GameObject;
m_ThreadManager = thread_mgr.GetComponent< ThreadManager >();
}
//------------------------------------------------------------
// Update
//------------------------------------------------------------
void Update()
{
if( Input.GetKeyDown(KeyCode.A) )
{
m_ThreadManager.RequestWork();
}
if( Input.GetKeyDown(KeyCode.S) )
{
m_ThreadManager.RequestWork( (job) => {
Debug.Log( "call back called." );
Debug.Log( job.Count ); // access job when finished work
});
}
}
//------------------------------------------------------------
// OnGUI
//------------------------------------------------------------
void OnGUI()
{
float w = Screen.width;
float h = Screen.height;

GUI.Label(
new Rect( w*0.0f, h*0.0f, w*0.5f, h*0.1f ),
Time.realtimeSinceStartup.ToString("0.00000")
);
}

//------------------------------------------------------------
// member
//------------------------------------------------------------
private ThreadManager m_ThreadManager;
}



起動すると、画面左上に経過時間を表示しているが、キーボードのAかSを押してジョブをリクエストしても
その表示が実に滑らかなままである。

これはこのリクエストしたジョブがメインスレッドでは無く作成した別スレッドで処理されているからだ。
このスレッドの方では特に処理間隔を制御していない為、とにかくCPUの性能に任せてカウンターが回る。

メインスレッドにさえ影響が無ければいくらでもこのスレッド側で重たい処理を任せて良い。(限度はあるが)


今回のサンプルのジョブにFuncやActionを持たせて、リクエスト時に特定の重い処理を渡してやるといった
改造をすると、いい感じのものが出来るだろう。

ただし、UnityのAPIを使用しない処理限定、である。

ジェネリックを上手く使えばかなり汎用性の高いものが出来るだろうが、
まぁあまりやり過ぎると使う側もアレなので、主要機能毎にシングルトンで分けてやるのが落とし所な気もする。
(大体非同期にしたい処理って決まってるので、自分ならそうする)


ファイル入出力や圧縮・展開、重たい数学の計算といった処理でブロッキングになっているものは
全部スレッドに任せてしまうと幸せになれるだろう。

ただし、スレッドの扱いというか非同期処理はかなりデリケートな部分なので、
使う側はきちんと仕組みを理解し十分に気を付けてコードを書く必要がある。

非同期は基本、「処理をリクエストして待つ」事が必要だ。


この「待つ」処理も人によって様々な書き方があって、さらに待って終わった後の処理として、
C#の場合はコールバックをラムダやActionなどで手軽に書いて渡せるようになっているが、
これも1つ間違えると非常に読みにくいコードになったり、バグを引き起こす元になりがちである。

並列でリクエストを行うと必然的に排他制御(トランザクションの管理ともいうか)も必要となる。
上記コード中のlockはそれを行うものだ。
配列の要素を追加したり削除したり参照したりというのが並列または同時に行われる危険のある箇所に
行う必要がある。

さらにそこにエラーハンドリング等が加わった場合は、アプリやゲーム作成をある程度経験していないと
その場しのぎのコードが混ざってスパゲティコードになってしまう傾向が強い。


スレッドはメインスレッドと「並列で」動いているものであるから、きちんとその流れを理解して
自分が書いたコードが何かおかしなことをやる可能性が無いかを常に意識しつつ使う必要がある。

別に仕組みを細部まで理解する必要はほとんどなくて、漠然としたイメージだけでも持っておくと大分違う。
後は実際に書いてみて動かして確かめる事を繰り返していけば、自ずと非同期について掴めてくるはずだ。


Unityでスレッドを使ってみたの話ついでに非同期の話を長々としてしまったが、
実は別スレッドでもUnityのAPIを呼べるアセットが既にあるらしい。

[Unity3D]Unityでスレッドを使いつつUnityAPIを使う Spicy Pixel Concurrency Kit

これを使ってみたらまた記事を書こうと思う。
見た感じ、何か危険な香りはしているw


ほか、今回はC#のThreadクラスだけを使ったが、他にも便利なものが.NET frameworkやC#にはあるので
それらを使ってみて良さそうなものがあればまた紹介したいと思う。

おそらく、Monoが古くて実機では使えない系のものばかりだろうが...w

[ 2013年09月28日 19:34:28 : Unity ]

カレンダー

プルダウン 降順 昇順 年別

06月 | 2017年07月 | 08月
- - - - - - 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 - - - - -


全記事表示リンク

全ての記事を表示する

検索フォーム

リンク

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。