[C#]task/async/await/CancellationToken/mutexを試してみた

#目的
マイコンを`FTDI`などの`USB -> UART`とか`USB -> SPI`とかで遊びで動かそうとかテストで動かそうと思った時にコンソールアプリケーションでもいいですけどデータを好きな設定値で送りたいとかってときは操作画面を持ったアプリケーションのほうが便利ですよね。

でも、画面からボタンクリックしてデータ送信、データ受信までポーリング待機するとほかのボタンが触れなかったり、表示を変化させたりするのでなんか手はないかなぁと思ったら、非同期スレッドが出てきたので調べてみました。

##`task` `async` `await` `CancellationToken` `mutex` ってなんぞや？
まず初めに、非同期スレッドについて調べると上記のキーワードが出てきました。
以下の記事が参考になりました。

 - **[Taskを極めろ！async/await完全攻略](https://qiita.com/acple@github/items/8f63aacb13de9954c5da)**
 - **[async/awaitについての備忘録](https://qiita.com/mounntainn/items/3f39e0c57412c48508bf)**
 - **[C# 並行・並列プログラミング パターン集](https://qiita.com/takutoy/items/c384fcb439d345a9a0d3)**
 - **[Mutex - Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/api/system.threading.mutex?view=netframework-4.8)**


非同期タスクを使う上で必要なものです。基本的にC#5.0以降のものだそうです。
ざっくりと以下の様に理解しました。(間違ってたらごめんなさい)

<dl>
  <dt>Task</dt>
  <dd>スレッドで使うメソッドの入れ物</dd>
  <dt>async</dt>
  <dd>非同期スレッドのメインメソッド。`async`節がついたメソッドを実行するとスレッドが分離する</dd>
  <dt>await</dt>
  <dd>Taskが終了するまで待つコマンド</dd>
  <dt>CancellationToken</dt>
  <dd>Taskを外部からキャンセルするためのトークン</dd>
  <dt>Mutex</dt>
  <dd>排他制御で使用する。スレッドやプロセスの占有権のやり取りに使う</dd>
</dl>

以上を使って`Winform`で試しに非同期タスクをつくってみます。

###非同期スレッドから画面UIにアクセスする方法
非同期スレッド上から画面を動かすスレッドにアクセスすることはできません。例外が発生します。
そこで、`Dispatcher`の`Invoke`を使って画面を動かすスレッドにアクセスします。
`Dispatcher`クラスの``Invoke`は、`Dispatcher`のキューに非同期スレッドから呼び出されたメソッドを詰め込めるらしいです。
詳しくは以下を参照してください。
**[Dispatcher - Microsoft Docs
](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.threading.dispatcher?view=netframework-4.8)**

使い方は以下の記事を参考にしました。

 - **[C#のWindowsフォームアプリケーションでメインスレッドのGUIを更新](https://qiita.com/taiyakisun/items/15b57df979eae7562aef)**
 - **[C# サブスレッドからフォームの表示を行う(Invoke)](http://cammy.co.jp/technical/2017/04/25/c-%E3%82%B5%E3%83%96%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E3%81%8B%E3%82%89%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%82%92%E8%A1%8C%E3%81%86invoke/)**

###実際に使って作ってみる

[GithubにVisualStudioのプロジェクト(AsyncTestForm)ごと上げています。](https://github.com/zasshyu66/Application_Study/tree/master/AsyncTestForm)
Form1.csに以下のボタンの処理を書いています。

 - `button1`->テキストを1秒ごとに出力する。`button2`が先に押されていたら動作をキャンセルする。
 - `button2`->テキストを1秒ごとに出力する。`button1`が先に押されていたら動作をキャンセルする。
 - `button3`->テキスト出力を停止させる。

<details><summary>(ソースのみはこちら)Form1.cs</summary><div>

```C#
namespace AsyncTestForm
{
    public partial class Form1 : Form
    {
        /// <summary>
        /// Mutexの生成
        /// </summary>
        private Mutex mut = new Mutex();

        /// <summary>
        /// Taskのキャンセルトークンの生成
        /// </summary>
        private CancellationTokenSource cancellation = new CancellationTokenSource();

        /// <summary>
        /// Form1のコンストラクタ
        /// </summary>
        public Form1()
        {
            InitializeComponent();
            //念のために中央に表示
            StartPosition = FormStartPosition.CenterScreen;
        }

        /// <summary>
        /// button1からのスレッド起動
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void button1_Click(object sender, EventArgs e)
        {
            MessageOutput("-- " + sender.ToString() + "Click --");

            //button2が先に起動していたらキャンセルする
            CancelTask();

            //cancellationを生成。usingを使ってTask.runが終了したらDisposeする。
            using (cancellation = new CancellationTokenSource())
            {
                await Task.Run(() => TestThread(sender, cancellation.Token));
            }
        }

        /// <summary>
        /// Button2からのスレッド起動
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void button2_Click(object sender, EventArgs e)
        {
            MessageOutput("-- " + sender.ToString() + "Click --");

            //button1が先に起動していたらキャンセルする
            CancelTask();

            //cancellationを生成。usingを使ってTask.runが終了したらDisposeする。
            using (cancellation = new CancellationTokenSource())
            {
                await Task.Run(() => TestThread(sender, cancellation.Token));
            }
        }

        /// <summary>
        /// 実行中のtaskをキャンセルする。
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button3_Click(object sender, EventArgs e)
        {
            CancelTask();
        }

        /// <summary>
        /// WInformに画面UIスレッドと別のスレッドからアクセスするためのメソッド
        /// </summary>
        /// <param name="message"></param>
        private void MessageOutput(string message)
        {
            //別スレッドからの呼ばれた場合
            if(InvokeRequired)
            {
                //Dispatcher.InvokeでDispatcherのキューにメソッドを突っ込む
                //MessageOutputをキューに突っ込むだけをして終了
                Invoke((Action)(() => MessageOutput(message)));
                return;
            }
            //Dispatcherのキューに入っている順番にMessage出力
            //画面スレッドが優先
            textBox1.Text += message + Environment.NewLine;
        }

        /// <summary>
        /// 適当なメソッド。1秒ごとに文字列を吐き出す
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="token"></param>
        private void TestThread(object sender,CancellationToken token)
        {
            //Mutexの取得。
            //別スレッドからこのメソッドが呼ばれたときは、Releaseされるまで止める。
            mut.WaitOne();
            MessageOutput("-- " + sender.ToString() + "START --");

            int i = 0;

            for (; ; )
            {
                if(token.IsCancellationRequested)
                {
                    MessageOutput("-- " + sender.ToString() + "Cancel --");
                    break;
                }
                MessageOutput(sender.ToString() + " : count " + i++);
                Thread.Sleep(1000);
            }
            MessageOutput("-- " + sender.ToString() + "END --");
            mut.ReleaseMutex();
            //Mutexの解放
        }


        /// <summary>
        /// 実行中のタスクのキャンセル
        /// </summary>
        private void CancelTask()
        {
            //cancellationTokenをキャンセルする。
            try
            {
                cancellation.Cancel();
                MessageOutput("-- Task Cancel --");

            }
            catch (ObjectDisposedException)
            {
                //cancellationTokenがDisposeされている場合の例外処理
                //cancellationTokenがDisposeの確認方法がこれしかわからない
                MessageOutput("-- ObjectDisposedException throw --");
            }
        }

    }
}
```
</div></details>

###結果

![AsyncTestForm.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407999/f47dd2eb-8bc4-b0c7-8d7b-4edd42834015.jpeg)

###`Mutex`の使い方について
`Mutex`の基本的な使い方で`Mutex.WaitOne()`で`Mutex`をゲットしてから処理をして、処理が終わったら`Mutex.ReleaseMutex()`をすることで、別のスレッドからのアクセスを制限することができる。

しかし、`Mutex.ReleaseMutex()`する前に`Mutex.WaitOne()`で所有権を取得してしまう場合がある。

 - `Mutex.WaitOne()`で待機する例

```MutexOK.cs
//Mutexの生成
private Mutex mut = new Mutex();

//button1クリックイベント
private async void button1_Click(object sender, EventArgs e)
{
     await Task.Run(() => sample(cancellation.Token));
}

private void sample(CancellationToken token)
{
    //通常のメソッド内だとブロックしてくれる
    //button1をクリックしてから1秒たってMutexを開放しないとこのメソッドを実行できない
    mut.WaitOne();
    Thread.Sleep(1000);
    mut.ReleaseMutex();

}
```

 - `Mutex.WaitOne()`で待機しない例

```MutexNG.cs
//Mutexの生成
private Mutex mut = new Mutex();

//button1クリックイベント
private async void button1_Click(object sender, EventArgs e)
{
    //async節のメソッドだと呼ばれるたびにMutexを所有できてしまう
    //この場合だとbutton1をクリックするたびに1秒待機せずにsampleメソッドが実行されてしまう
    mut.WaitOne();
    await Task.Run(() => sample(cancellation.Token));
    mut.ReleaseMutex();
}

private void sample(CancellationToken token)
{
    Thread.Sleep(1000);
}
```

以上の様に、`async`節だとおそらく即時別スレッドとして実行するので`Mutex`を取得してしまうようです。

#最後に

非同期処理は排他制御やキューなどをうまく使えないといけないのと、そもそも処理するうえでどういう処理を非同期で行うかなど設計面でも割と複雑になるので大変そうです。
ですが、実際使ってみて(見た目は)同時に動くということを見ると非常に楽しい制御です。個人的には好きな制御です。