simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Task.Run:Unityマルチスレッド処理に引数を渡す方法

AIに身体性を持たせるためのマイクロワールド:ブロックロジックの可視化システムの実装メモです。
まずは結果動画をご覧いただきたい。

www.youtube.com
f:id:simplestar_tech:20180128172844g:plain

マルチスレッド処理にて空気に触れている土ブロックだけが確率的に消失(正しくは空気ブロックにマテリアル変換される)

今回、技術的に記録しておかなければならないと思った点は二つ

1. Task.Runによるマルチスレッド処理に引数を渡す書式
2. スレッドごとの乱数生成器

1. Task.Runによるマルチスレッド処理に引数を渡す書式

参考にした記事はこちら↓
kazenetu.exblog.jp

Task に引数を渡す方法、それは関数内のローカル変数をラムダ式内で参照するという書式で実現できます。
私の場合は for 文を回している時の、増えていくインデックスをスレッドに渡したかったので、次のように記述して解決しました。

_workerThreads = new Task[SpiralScanRadius + 1/*Lattice*/];
for (int radius = 0; radius < SpiralScanRadius; radius++)
{
    _CreateSpiralTask(radius);
}
_workerThreads[SpiralScanRadius] = Task.Run(_ScannLattice);

private void _CreateSpiralTask(int radius)
{
    if (0 == radius)
    {
        _workerThreads[radius] = Task.Run(() =>
        {
            ulong[] localBlockData = new ulong[Chunk.Width * Chunk.Depth * Chunk.Height];
            while (Instance._workThread)
            {
                if (!Instance._SpiralWork(0, 0, localBlockData))
                {
                    break;
                }
            }
        });
    }
    else
    {
        _workerThreads[radius] = Task.Run(() =>
        {
            ulong[] localBlockData = new ulong[Chunk.Width * Chunk.Depth * Chunk.Height];
            while (Instance._workThread)
            {
                int offsetZLow = -radius;
                for (int offsetX = -radius; offsetX < radius; offsetX++)
                {
                    if (!Instance._SpiralWork(offsetX, offsetZLow, localBlockData))
                        goto ExitLoop;
                }
                int offsetXHigh = radius;
                for (int offsetZ = -radius; offsetZ < radius; offsetZ++)
                {
                    if (!Instance._SpiralWork(offsetXHigh, offsetZ, localBlockData))
                        goto ExitLoop;
                }
                int offsetZHigh = radius;
                for (int offsetX = radius; offsetX > -radius; offsetX--)
                {
                    if (!Instance._SpiralWork(offsetX, offsetZHigh, localBlockData))
                        goto ExitLoop;
                }
                int offsetXLow = -radius;
                for (int offsetZ = radius; offsetZ > -radius; offsetZ--)
                {
                    if (!Instance._SpiralWork(offsetXLow, offsetZ, localBlockData))
                        goto ExitLoop;
                }
            }
            ExitLoop:;
        });
    }
}

参考記事にもありますが for 文の中で Task.Run を記述しても、実際に Task が Run するのは for 文を抜けた後なので、for文後のインデックス値がすべての Task に渡ってしまいます。
そういう Task の仕組みを先に理解しておけば、みなさんも自力で解決方法を思いついたと思います。

2. スレッドごとの乱数生成器

Random は生成したスレッドと異なるスレッドで使用すると 0 が返る仕様です。(正しいが、これは困った)
スレッド内で乱数生成したい場合は、スレッド用の生成処理として ThreadLocal を使用します。

以下の記事で知りました。(UniRx でお世話になってます。)
neue cc - C#とランダム

実装メモ

public static class RandomProvider
{
    private static ThreadLocal<System.Random> randomWrapper = new ThreadLocal<System.Random>(() =>
    {
        using (var rng = new RNGCryptoServiceProvider())
        {
            var buffer = new byte[sizeof(int)];
            rng.GetBytes(buffer);
            var seed = BitConverter.ToInt32(buffer, 0);
            return new System.Random(seed);
        }
    });

    public static System.Random GetThreadRandom()
    {
        return randomWrapper.Value;
    }
}

使い方としては次の通り

public class EarthLogic : IBlockLogic
{
    static Random _random = new Random();

    public int MaterialId()
    {
        return (int)MaterialID.Earth;
    }

    public BlockDataSt Work(BlockDataSt blockData, int widthIndex, int depthIndex, int heightIndex, ulong[] localBlockData, IHexWorldUtility pluginUtility)
    {
        if (99 == HexWorldUtility.RandomProvider.GetThreadRandom().Next(0, 100))
        {
            BlockDataSt upBlock = new BlockDataSt(localBlockData[widthIndex * Chunk.Depth * Chunk.Height + depthIndex * Chunk.Height + Math.Min(heightIndex + 1, Chunk.Height - 1)]);
            if (MaterialID.Air == upBlock.materialId)
            {
                blockData.materialId = MaterialID.Air;
            }
        }
        return blockData;
    }
}

この Work 関数が複数のスレッドから呼び出されるのですが、スレッド内にて最初に乱数生成器が作られて、それ以降はスレッドごとの Random が使われるようになります。
上のコードはちょうど土ブロックのロジックになります。

まとめ

以下の状況を突破する知識が得られました。
1. Task.Runによるマルチスレッド処理に引数を渡したいが、Runする Action に引数を追加すると今度は Task.Run にActionを渡せない、この問題を解決するための書式の調べ方が不明
2. 複数スレッドから呼び出される関数内で乱数生成したいが、メインスレッドで作った Random は常に0を返してくる、どうにか複数スレッドから呼び出しても乱数を利用できるようにしたい