Task④

タスクのキャンセル

タスクに渡したラムダ式内でOperationCanceledExceptionをスローすることでタスクをキャンセルできます。
キャンセルされたタスクのTask.StatusはCanceledになります。

var task = Task.Run(() => throw new OperationCanceledException());

try
{
    // タスクは即時キャンセルされるわけでなく、終了を待機する必要があります
    task.Wait();
}
catch (AggregateException)
{

}
finally
{
    // 出力結果は「task status : Canceled」になります
    Console.WriteLine($"task status : {task.Status}");
}

CancellationToken

CancellationTokenSource・CencellationTokenを使用することで、外部からタスクのキャンセルをコントロールできます。

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Run(
    () =>
    {
        while (!token.IsCancellationRequested)
        {
            Thread.Sleep(200);
            Console.WriteLine($"processing...");
        }
    });

tokenSource.CancelAfter(1000);

try
{
    task.Wait();
}
catch (AggregateException)
{

}
finally
{
    Console.WriteLine($"task status : {task.Status}");
}

CancellationTokenSource.Cancel()・CancellationTokenSource.CancelAfter()を実行することで、CancellationTokenSource.Tokenから取得されたトークン及びそのコピーにキャンセルが通知されます。
キャンセルが通知された後、CancellationTokenSource.IsCancellationRequested・CancellationToken.IsCancellationRequestedの値がTrueになるため、
タスクに渡したラムダ式内で上記プロパティ値をモニターし、Trueの場合に終了するような仕組みを組み込むことで、外部からタスクをキャンセルすることができます。
ただし、この方法はフラグで終了判定を行っているに過ぎないため、Task.StatusはRanToCompletionになります。
Task.StatusをCanceledにしたい場合、OperationCanceledExceptionをスローすることで実現できます。

上記サンプルのケースでは適当なローカル変数でも代用可能なので、CancellationTokenのメリットが薄いですが、タスク内で他クラスのメソッドを呼ぶなど、メソッド外に制御が移る場合は有用です。
呼び出し先に順次CancellationTokenを渡していくことで、すべてのメソッドでキャンセル状態をモニターできるようになります。

また、タスクにCancellationTokenを渡すことで、もう少し細かな制御が可能になります。

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Run(
    () =>
    {
        while (true)
        {
            token.ThrowIfCancellationRequested();

            Thread.Sleep(200);
            Console.WriteLine($"processing...");
        }
    },
    token);

tokenSource.CancelAfter(1000);

try
{
    task.Wait();
}
catch (AggregateException)
{

}
finally
{
    Console.WriteLine($"task status : {task.Status}");
}

CancellationToken.ThrowIfCancellationRequested()はOperationCanceledExceptionをスローします。
また、以下条件を満たす場合にTask.StatusをCanceledにし、満たさない場合はFaultedにします。

  • CancellationToken.IsCancellationRequestedがTrue
  • 自身とタスクに渡されたトークンが同一

ちなみに、上記サンプルでCancelAfter()のパラメーターを0にした場合、即時キャンセル通知が行われます。
タスク実行開始前であればラムダ式は評価されずに終了し、Task.StatusはCenceledとなります。

マルチスレッドの管理①

排他制御

スレッドセーフでないオブジェクトに対して複数スレッドからアクセスした場合、競合が発生します。
例えば、コレクションへの追加中に、他スレッドからは削除が行われた場合、追加結果が期待値と異なってしまうといった状況です。
追加処理に排他制御を掛けることで、こういった状況を回避できます。

lockステートメント

他スレッドからのアクセスを制限したい一連の処理をlockステートメント内に記述します。
lockステートメントには、ロック中かどうかを判別するためのobject(ロックオブジェクト)を指定します。

var lockObj = new object();
lock (lockObj)
{
     // 排他制御が必要な処理
}

動きは以下のようになります。

  1. スレッド①がlockステートメントに到達した場合、ロックオブジェクトがロックされます。
  2. スレッド②がlockステートメントに到達した場合、同様にロックオブジェクトのロックを試みますが、既にロック済みなので待機します。
  3. スレッド①がlockステートメントを抜けた段階でロックが開放され、スレッド②がロックして処理を再開します。

ポイントとして、ロックオブジェクトはそのlockステートメント専用のものを用意するのが好ましいです。
他でもロックオブジェクトとして使用されるような場合、デッドロックが発生する可能性があります。

Monitor

Monitorクラスを使用することで、lockと同様に排他処理を実現できます。
(lockステートメントコンパイルされるとtry/catch+Monitorを使用したコードに展開される。)
Monitor.Enter()~Monitor.Exit()の間に処理を記述します。

try
{
    Monitor.Enter(array);
    // 排他制御が必要な処理
}
finally
{
    Monitor.Exit(array);
}

Monitorはロック開放のために確実にMonitor.Exit()を呼び出す必要があります。
例外が発生した場合を考慮し、try/finallyで実装するのが安全です。
また、ロックに成功したかどうかを返すMonitor.TryEnter()メソッドなど、より柔軟な排他実装を行うためのメソッドが用意されています。
参考 : Monitor Class (System.Threading) | Microsoft Docs

Interlocked

Interlockedクラスは変数に対する排他的な処理を提供します。

Interlocked.Add(ref hoge, 5); // 変数hogeに対して5を加算する処理を排他的に行います
Inerlocked.Increment(ref hoge); // 変数hogeに対してインクリメントする処理を排他的に行います

例としてあげた上記コードの場合、メモリからの読み出し・計算・書き戻しの処理に排他を掛けるため、その間に他スレッドからの割り込みは発生しません。
参考 : Interlocked Class (System.Threading) | Microsoft Docs

Task③

async/await

await

メソッド内でTaskの実行を非同期的に待機する場合、awaitキーワードを付与します。

var task = Task.Run(() => hoge(();
await task;

awaitした時点で、メソッドは待機状態となり、呼び出し元に処理が戻ります。
taskが完了するとメソッドが再開され、await以降の処理が行われます。
await以降に処理が存在しない場合、待機の必要がないため、awaitは不要です。

async

awaitキーワードを含むメソッドにはasyncキーワードを付与する必要があります。

private async {戻り値型} Hoge()
{
}

asyncなメソッドの戻り値型は以下のいずれかです。

Task
  • メソッドが何らかの値をreturnしない場合
  • 呼び出し元でtry/catchすることで例外処理可能

以下サンプルコードと実行結果です。

public static void Main(string[] args)
{
    AsyncTest();
    Console.WriteLine("AsyncTest Called");
    Console.ReadKey();
}

private static async Task AsyncTest()
{
    try
    {
        await AsyncTask();
    }
    catch (Exception e)
    {
        Console.WriteLine($"Message : {e.Message}");
    }

    Console.WriteLine("AsyncTest Complete");
}

private static async Task AsyncTask()
{
    await Task.Delay(1000);
    Console.WriteLine("AsyncTask Complete");
    throw new InvalidOperationException("Error in AsyncTask");
}
AsyncTest Called
AsyncTask Complete
Message : Error in AsyncTask
AsyncTest Complete
Task<T>
  • メソッドが何らかの値をreturnする場合
  • Tはreturnする値の型
  • 呼び出し元でtry/catchすることで例外処理可能

以下サンプルコードと実行結果です。 Main()の実装は同じです。

private static async Task AsyncTest()
{
    try
    {
        var result = await AsyncHasValueTask();
        Console.WriteLine(result);
    }
    catch (Exception e)
    {
        Console.WriteLine($"Message : {e.Message}");
    }

    Console.WriteLine("AsyncTest Complete");
}

private static async Task<string> AsyncHasValueTask()
{
    await Task.Delay(1000);
    // このコメントアウトを解除すると、AsyncTest()で例外メッセージだけ出力されます。
    //throw new InvalidOperationException("Error in AsyncHasValueTask");
    return "AsyncHasValueTask Complete";
}
AsyncTest Called
AsyncHasValueTask Complete
AsyncTest Complete
void
  • イベントハンドラーの場合
  • 呼び出し元でawaitできず、例外も検出できないため、上記以外の使用は避ける

以下サンプルコードと実行結果です。 Main()の実装は同じです。 例外は処理されず、コンソールに出力されます。

private static async Task AsyncTest()
{
    try
    {
        AsyncVoid(); // コンパイルエラーになるのでawaitできない
    }
    catch (Exception e)
    {
        Console.WriteLine($"Message : {e.Message}");
    }

    Console.WriteLine("AsyncTest Complete");
}

private static async void AsyncVoid()
{
    await Task.Delay(1000);
    Console.WriteLine("AsyncVoid Complete");
    throw new InvalidOperationException("Error in AsyncVoid");
}
AsyncTest Complete
AsyncTest Called
AsyncVoid Complete

Unhandled Exception: System.InvalidOperationException: Error in AsyncVoid
   at ProgrammingInCSharp_Async.Program.AsyncVoid() in C:\Users\maki8\Documents\Visual Studio 2017\Projects\ProgrammingInCSharp_Async\ProgrammingInCSharp_Async\Program.cs:line 53
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
   at System.Threading.ThreadPoolWorkQueue.Dispatch()

Thread②

スレッドの基本②

ThreadStatic/ThreadLocal

以下2つの方法で、アクセスするスレッド毎にフィールド/変数に異なる値を持つことができます。

  1. staticフィールドに対してThreadStatic属性を付与する
[TheadStatic]
private static int hoge;
  1. ThreadLocal型のフィールド/変数として宣言する
private ThreadLocal hoge;

ThreadStaticはstaticフィールドにのみ使用可能です。
ThreadLocalはローカル変数でも使用可能です。

スレッドプール

スレッドを作成するということは、電車を走らせるために線路もセットで作るようなものです。
これは非常にコストがかかります。
スレッドプールは予めいくつかのスレッドを準備しておき、各処理で使い回す仕組みです。
スレッドプール管理下のスレッドに処理を登録する方法は以下のようなものがあります。

  • ThreadPool.QueueUserWorkItem()を使用する。
ThreadPool.QueueUserWorkItem(() => hoge());
  • Taskを使用する。

スレッドプールは便利ですが、以下のような制約があります。

  • 管理するスレッド数は上限があります。
    大量のスレッドを必要とする処理や、スレッドを長時間専有する処理は不向きです。
    前者は実行しきれない処理が、後者はスレッドプールを利用する他処理が待機状態になってしまいます。
  • スレッドはすべてバックグラウンドプロセスです。
    優先度の変更もできません。
    etc...

Thread①

スレッドの基本①

スレッドの作成

コンストラクターラムダ式を渡して生成します。

var thread = new Thread(() => hoge());

ラムダ式に引数を与えることもできます。
単一の引数のみ許容し、object型として扱われます。

var thread = new Thread(hoge => fuga());

ラムダ式の代わりに専用のデリゲートを渡すこともできます。
ただし、これはレガシーな方法です。
Actionは使用できません。

new Thread(new ThreadStart({メソッド名}));
new Thread(new ParameterizedThreadStart({単一引数のメソッド名});

スレッドの開始

Start()メソッドで開始されます。

thread.Start();

スレッドはフォアグラウンドプロセスになります。
アプリケーションはスレッドの終了を待機します。
IsBackGroundプロパティにtrueを設定することでバックグラウンドプロセスとして動きます。
この場合、アプリケーションはスレッドの終了を待機しません。

スレッドの中止

Abort()メソッドで中止できます。

thread.Abort();

即時にスレッドが中止されるため、実行中の処理がやりかけの状態になる可能性があります。
スレッド外の変数をスレッド内でキャプチャすることでコントロールするほうが安全です。

スレッドの同期

Join()を実行することで、対象スレッドの終了を待機できます。
以下コードでは、thread2のコンソール出力はthread1の終了を待って行われます。

var thread1 = new Thread(
    () =>
    {
        Thread.Sleep(1000);
        Console.WriteLine("thread1 processed");
    });
var thread2 = new Thread(
    () =>
    {
        thread1.Join();
        Console.WriteLine("thread2 processed");
    });

thread2.Start();
thread1.Start();

Task②

Taskの基本②

継続タスク

Task.ContinueWith()を使用することで、後続タスクを登録できます。
後続タスクは先行タスクを受けるAction/ラムダ式を引数にします。
先行タスクに戻り値がある場合、Resultから取得できます。
継続方法のオプションを指定することも可能です。

var task = Task.Run(() => 1)
    .ContinueWith(prev => prev.Result + 1)
    .ContinueWith(
        prev =>
        {
            Console.WriteLine($"prev.Result : {prev.Result}");
            throw new InvalidOperationException();
        },
        TaskContinuationOptions.OnlyOnRanToCompletion)
    .ContinueWith(
        prev => Console.WriteLine("Failed"),
        TaskContinuationOptions.NotOnRanToCompletion);

実行結果は以下のようになります。
例外スローしているコードをコメントアウトした場合、"Failed"は出力されません。

prev.Result : 2
Failed

子タスク

タスクのAction/ラムダ式内で更にタスクを生成することが可能です。
基本的には親から独立したタスクとなるため、親タスクは子の完了を待ちません。
以下コードでは"Parent Complete"が"Child Complete"より先に出力されます。

var task = Task.Run(
    () =>
    {
        Task.Run(
            () =>
            {
                Thread.Sleep(100);
                Console.WriteLine("Child Complete");
            });
    });
task.Wait();
Console.WriteLine("Parent Complete");

以下のようにすると、子タスクは親にアタッチされ、親は子の完了を待ちます。
つまり、"Parent Complete""Child Complete"の順に出力されます。 Task.Run()でタスクを作成した場合、TaskCreationOptions.DenyChildAttachが指定された状態になるため、Task.Factory.StartNew()を使用します。

var task = Task.Factory.StartNew(
    () =>
    {
        var child = Task.Factory.StartNew(
            () =>
            {
                Thread.Sleep(100);
                Console.WriteLine("Child Complete");
            },
            TaskCreationOptions.AttachedToParent); // 親にアタッチされる
    },
    TaskCreationOptions.None); // DenyChildAttach(子のアタッチを許容しない)以外を指定する
task.Wait();
Console.WriteLine("Parent Complete");

Task①

Taskの基本①

Taskの作成/開始

以下コードでタスクを作成/開始できます。

var task = new Task(() => hoge());
task.Start();

以下のように①行で書くこともできます。

var task = Task.Run(() => hoge());

Taskの待機

Taskの終了を待機するには以下のようにします。

task.Wait();

以下のコードの場合、Wait()を実行することで"Sample Task"が"Method End"より先に出力されます。
Wait()しない場合、100ミリ秒待機している間に"Method End"が先に出力されます。

var task = Task.Run(
    () =>
    {
        Thread.Sleep(100);
        Console.WriteLine("Sample Task");
    });
task.Wait();
Console.WriteLine("Method End");

複数タスクの全終了を待機するには以下のようにします。

var tasks = new Task[] {...};
Task.WaitAll(tasks);

複数タスクのいずれかの終了を待機するには以下のようにします。

var tasks = new Task[] {...};
Task.WaitAny(tasks);

WaitAny()が処理を戻して以降も、未完了タスクは実行され続けます。

Taskの戻り値

Action/ラムダ式に戻り値が存在する場合、ResultプロパティからTaskの戻り値として取得できます。

var task = Task.Run(
    () =>
    {
        Thread.Sleep(1000);
        return 1;
    });
var result = task.Result;

Resultにアクセスするとタスクの完了を待機することになるので注意が必要です。
上記コードでは値を取得するために1000ミリ秒の待機が発生します。