0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

0
Posted at

目的

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

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

task async await CancellationToken mutex ってなんぞや?

まず初めに、非同期スレッドについて調べると上記のキーワードが出てきました。
以下の記事が参考になりました。

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

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

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

非同期スレッドから画面UIにアクセスする方法

非同期スレッド上から画面を動かすスレッドにアクセスすることはできません。例外が発生します。
そこで、DispatcherInvokeを使って画面を動かすスレッドにアクセスします。
Dispatcherクラスの``Invokeは、Dispatcher`のキューに非同期スレッドから呼び出されたメソッドを詰め込めるらしいです。
詳しくは以下を参照してください。
Dispatcher - Microsoft Docs

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

実際に使って作ってみる

GithubにVisualStudioのプロジェクト(AsyncTestForm)ごと上げています。
Form1.csに以下のボタンの処理を書いています。

  • button1->テキストを1秒ごとに出力する。button2が先に押されていたら動作をキャンセルする。
  • button2->テキストを1秒ごとに出力する。button1が先に押されていたら動作をキャンセルする。
  • button3->テキスト出力を停止させる。
(ソースのみはこちら)Form1.cs
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 --");
            }
        }

    }
}

結果

AsyncTestForm.jpg

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を取得してしまうようです。

最後に

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?