前回からの流れ
前回の記事**「UMLとスレッド」**では、
オブジェクトは自律的な性質を持っていると認識できることに触れ、
その認識に基づいてUMLのモデルを描き、実装すれば、スレッドが分かりやすくなるということを書きました。
今回は、オブジェクトのアクティブな性質を意識したUMLモデリングと、それを実装につなげる例を挙げたいと思います。
今回の題材の説明
以前、書いた「オブジェクト指向分析の基本的な思考法」の洗濯機の例を使おうと思いましたが、
今回は、もっと簡単な例を使って分かりやすくします。洗濯機の例は、乞うご期待とさせてください。
クラス図
検査技師と扇風機がいるモデルです。
どちらも、アクティブな存在です。
技師は人間で自律したオブジェクトです。
扇風機も電気で動く自律したオブジェクトです。
シーケンス図
技師がテストを始めると、扇風機をスタートさせます。
20秒後に扇風機をストップさせます。
ただこれだけです。
C#で実装してみる
自律したオブジェクトにそれぞれ単一のスレッドを割り当てて動作させるために、
自作のタスクスケジューラを利用しています。
詳細は、後述+参考サイトの紹介をしていますので、読み進めてください。
Program.cs
コンソールアプリです。
Program.csで、インスタンスの生成と処理全体の流れを実装しています。
コード内のコメントを読んで理解してください。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TestFan
{
class Program
{
static void Main(string[] args)
{
扇風機 検査対象扇風機 = new 扇風機(); // 扇風機のインスタンスを生成
検査技師 技師 = new 検査技師(); // 検査技師のインスタンスを生成
技師.検査対象を確保(検査対象扇風機); // 検査技師に扇風機を持たせる
技師.Test(); // 検査技師に検査を始めさせる(この中でシーケンス図の流れをする)
Console.ReadKey(); // コンソールを勝手に閉じないようにするおまじない
}
}
}
検査技師.cs
検査技師のクラスは、検査技師自身のスレッドで動作するようにしています。
スレッドプールではなく指定スレッドで動かすため、
TaskをRunSynchronously()させるときに、自作タスクスケジューラ(QueTaskScheduler)を使います。
QueTaskSchedulerは、Taskを単一のスレッドにキューイングさせて実行させるタスクスケジューラです。
自作なのでコードは下に載せます。
技師の処理自体は、Task( () => {} ); 内にラムダ式で書いてあります。
using Azuki.Behavior;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TestFan
{
class 検査技師
{
// この技師のスレッドになるタスクスケジューラ
private QueTaskScheduler queuedTaskScheduler = new QueTaskScheduler("技師");
// アクセッサ
private 扇風機 検査対象の扇風機 = null;
public void 検査対象を確保(扇風機 _扇風機)
{
Task task = new Task(() => {
検査対象の扇風機 = _扇風機;
});
task.RunSynchronously(queuedTaskScheduler); // タスクを開始するときにタスクスケジューラを指定します
}
public void Test()
{
Task task = new Task(() => {
if (検査対象の扇風機 != null)
{
検査対象の扇風機.Start();
System.Threading.Thread.Sleep(20 * 1000);
検査対象の扇風機.Stop();
}
});
task.RunSynchronously(queuedTaskScheduler); // タスクを開始するときにタスクスケジューラを指定します
}
}
}
扇風機.cs
扇風機も検査技師と同様で、
扇風機自身のスレッドで動作させるため、QueTaskSchedulerを使います。
こちらは非同期処理なので、Task.RunSynchronously()ではなく、Task.Start()を呼び出します。
Stopさせるときのキャンセレーショントークンについては、説明を「参考」に委ねます。
using Azuki.Behavior;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace TestFan
{
class 扇風機
{
// この扇風機のスレッドになるタスクスケジューラ
private QueTaskScheduler queuedTaskScheduler = new QueTaskScheduler("扇風機");
// ストップさせるためのキャンセレーショントークンソース
private CancellationTokenSource tokenSource = null;
public void Start()
{
tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
Task task = new Task(() => {
while (token.IsCancellationRequested == false) // 停止されるまで繰り返す
{
Console.WriteLine("ブーン");
System.Threading.Thread.Sleep(1000);
}
Console.WriteLine("止まった");
}, token);
Task continueTask = task.ContinueWith
(t =>
{
// 後始末
tokenSource.Dispose();
tokenSource = null;
}, queuedTaskScheduler); // 後始末も同じタスクスケジューラで行う
task.Start(queuedTaskScheduler); // タスクを開始するときにタスクスケジューラを指定します
}
public void Stop()
{
tokenSource?.Cancel();
}
}
}
QueTaskScheduler.cs
以下は、自作のタスクスケジューラです。
以下は自作と言っても、「参考」のページを見て作成したものですので、説明はそちらをご覧ください。
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Azuki.Behavior
{
public sealed class QueTaskScheduler : TaskScheduler, IDisposable
{
private BlockingCollection<Task> tasksCollection = new BlockingCollection<Task>();
private readonly Thread mainThread = null;
public String Name
{
get { return mainThread.Name; }
}
public QueTaskScheduler(string threadName)
{
// スレッドを生成し、名前を付けます。スレッドは下記のExecuteを実行します。
mainThread = new Thread(new ThreadStart(Execute))
{
Name = threadName
};
if (!mainThread.IsAlive)
{
// スレッドを開始
mainThread.Start();
}
}
private void Execute()
{
// tasksCollectionにタスクがある間だけ、このforeachが回ります。
// タスクがないときは、foreachは停止しています。
foreach (var task in tasksCollection.GetConsumingEnumerable())
{
TryExecuteTask(task);
}
}
// オーバーライドしなければなりません。
protected override IEnumerable<Task> GetScheduledTasks()
{
return tasksCollection.ToArray();
}
// オーバーライドしなければなりません。
protected override void QueueTask(Task task)
{
// タスクをキューイングします。
if (task != null)
{
tasksCollection.Add(task);
}
}
// オーバーライドしなければなりません。
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// falseを返すと、taskがキューに並びます。
return false;
// ここでtrueを返すと、キューに入れずにそのまま実行させてしまいます。
}
private void Dispose(bool disposing)
{
if (!disposing) { return; }
tasksCollection.CompleteAdding();
tasksCollection.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
実行結果
最後にもう一度説明
以上の例は、
自律的なオブジェクトである 技師 に、1つQueTaskSchedulerを割り当て、
もう一つの自律的なオブジェクトである 扇風機に、別の1つQueTaskSchedulerを割り当てています。
このようにして、現実世界のオブジェクトの「自律的である(=独自のスレッドで動いている)という状況」を実装しています。
このようなスレッドの割当てができていると、想定外の動きになることがほぼ無くなったり、
想定外の動きをしてもデバッグしやすいのではないか、と思いませんか?
少なくとも、スレッドの動きが雲をつかむような話にはならなくなると思いませんか?
参考
Building your own task scheduler in C#
タスクスケジューラの自作について書かれています。こちらを参考にしてQueTaskSchedulerを作成しました。
英語サイトですが、ウェブブラウザの翻訳機能を使えば、ほぼ日本語として読むことができます。
C# タスクのキャンセル方法
タスクのキャンセルのコードは、こちらを参考にさせて頂きました。