WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

非同期タスクのキャンセル

by WebSurfer 2020年9月27日 16:38

.NET Framework 4.5 以降で利用できる async / await を使って非同期で実行されるタスクをキャンセルする方法についていろいろ調べたので備忘録として書いておきます。

非同期タスクのキャンセル

基本は Microsoft のドキュメント「マネージド スレッドのキャンセル」に書いてあります。が、それを読んだだけでは自分の頭では理解できなかったので、実際に自分の手を動かしてコードを書いて試してみました。

Microsoft のドキュメントに書いてありますが、キャンセル処理を実装するための一般的なパターンは以下の通りだそうです。

  1. CancellationTokenSource クラスのインスタンスを作成する。
  2. CancellationTokenSource.Token プロパティで CancellationToken を取得し、キャンセルをリッスンするタスクに渡す。  
  3. タスクにはキャンセル通知を適切に処置するコードを実装しておく。
  4. CancellationTokenSource.Cancel メソッドを呼び出し、リッスンしているタスクにキャンセルを通知する。
  5. キャンセル通知を受けたタスクは、あらかじめ実装されているコードに従ってキャンセル処置を行う。  

タスクがキャンセルの通知を受けとるには、(1) ポーリング、(2) コールバックの登録、(3) 待機ハンドルの待機という方法があるそうです。

参考にした Microsoft のドキュメントには (1) ~ (3) のコード例の紹介がありますが、コンソールアプリのものでちょっとピンとこなかったので、Windows Forms アプリで実装してみました。

ベースは @IT の記事「WPF/Windowsフォーム:時間のかかる処理をバックグラウンドで実行するには?」のコードです。それに手を加えて[キャンセル]ボタンクリックでキャンセルできるようにしています。

コードは以下の通りです。この記事の上に貼った画像を表示したものです。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsAsyncTest
{
    public partial class Form4 : Form
    {
        private CancellationTokenSource cts = null;
        
        public Form4()
        {
            InitializeComponent();
            EableButtons();
            toolStripStatusLabel1.Text = "";
            toolStripProgressBar1.Value = 0;
        }

        // (1) ポーリングによるリッスン。進捗表示なし
        private async void button1_Click(object sender, EventArgs e)
        {
            DisableButtons();
            toolStripStatusLabel1.Text = "処理中…";
            toolStripProgressBar1.Value = 0;
            string result = "";
            bool isCanceled = false;

            using (this.cts = new CancellationTokenSource())
            {
                CancellationToken token = this.cts.Token;
                try
                {
                    result = await DoWorkAsync(100, token);
                }
                catch (OperationCanceledException)
                {
                    isCanceled = true;
                }
            }

            // 処理結果の表示
            if (isCanceled)
            {
                toolStripStatusLabel1.Text = "キャンセル";
                toolStripProgressBar1.Value = 0;
            }
            else
            {
                toolStripStatusLabel1.Text = result;
                toolStripProgressBar1.Value = 100;
            }

            EableButtons();
        }

        // 時間のかかる処理を行うメソッド
        // ポーリングによるリッスン用。進捗表示なし
        private async Task<string> DoWorkAsync(
            int n, 
            CancellationToken token)
        {
            for (int i = 1; i <= n; i++)
            {
                token.ThrowIfCancellationRequested();
                await Task.Delay(100);
            }

            return "全て完了";
        }

        // (2) ポーリングによるリッスン。進捗表示あり
        private async void button2_Click(object sender, EventArgs e)
        {
            DisableButtons();

            var p = new Progress<int>(ShowProgress);

            using (this.cts = new CancellationTokenSource())
            {
                CancellationToken token = this.cts.Token;
                try
                {
                    await DoWorkAsync(p, 100, token);
                }
                catch (OperationCanceledException)
                {
                    // 必要なら何らかの処置
                }
            }

            EableButtons();
        }

        // 時間のかかる処理を行うメソッド
        // ポーリングによるリッスン用。進捗表示あり
        private async Task<string> DoWorkAsync(
            IProgress<int> progress,
            int n,
            CancellationToken token)
        {
            for (int i = 1; i <= n; i++)
            {
                token.ThrowIfCancellationRequested();
                await Task.Delay(100);

                int percentage = i * 100 / n; // 進捗率
                progress.Report(percentage);
            }

            return "全て完了";
        }

        // 進捗をプログレスバーとラベルに表示するコールバック
        // UIスレッドで呼び出される
        private void ShowProgress(int percent)
        {
            toolStripStatusLabel1.Text = percent + "%完了";
            toolStripProgressBar1.Value = percent;
        }

        // キャンセルボタン
        private void button3_Click(object sender, EventArgs e)
        {
            if (this.cts == null) return;

            cts.Cancel();
        }

        // (3) コールバックの登録によるリッスン
        // Microsoft のドキュメントをまねて Web アクセスをシミュ
        // レートする自作クラス MyWebClient を定義して使う。
        // MyWebClient の定義はこのサンプルコードの下の方にあり

        private MyWebClient client;
        
        private async void button4_Click(object sender, EventArgs e)
        {
            DisableButtons();

            // Timer(デザイン画面で Form にドラッグ&ドロップした
            // もの)を利用して定期的に進捗状況を取得して表示。
            // 
            // System.Windows.Forms.Timer を利用するにはメッセージ
            // ループの実行が必要なので注意。メッセージループは UI
            // スレッドで実行される。await は UI スレッドをブロック
            // しない。従い、await で待機中でも Timer は動く

            this.timer1.Interval = 100;

            using (this.cts = new CancellationTokenSource())
            {
                CancellationToken token = this.cts.Token;
                this.client = new MyWebClient();
                token.Register(async () => { 
                    await this.client.CancelAsync(); 
                    this.timer1.Stop(); }) ;
                this.timer1.Start();
                string result = await this.client.GetStringAsync(100);

                // キャンセルせず完了する場合 Timer はここで止めること。
                // 止めると、プログレスバーの表示も 100 になる前に止ま
                // ってしまうので、ここで設定が必要。
                if (this.client.Progress >= 100)
                { 
                    this.timer1.Stop();
                    toolStripStatusLabel1.Text = this.client.Progress + "%完了";
                    toolStripProgressBar1.Value = this.client.Progress;
                }
            }

            EableButtons();
        }

        // 進捗を表示するためのイベントハンドラ
        private void timer1_Tick(object sender, EventArgs e)
        {
            // ここは UI スレッドで実行される
            toolStripStatusLabel1.Text = this.client.Progress + "%完了";
            toolStripProgressBar1.Value = this.client.Progress;
        }      

        // (4) 待機ハンドルを使用したリッスン
        private async void button5_Click(object sender, EventArgs e)
        {
            DisableButtons();

            var p = new Progress<int>(ShowProgress);

            using (this.cts = new CancellationTokenSource())
            {
                CancellationToken token = this.cts.Token;
                try
                {
                    await DoWorkAsync3(p, 100, token);
                }
                catch (OperationCanceledException)
                {
                    // 必要なら何らかの処置
                }
            }

            EableButtons();
        }

        // 待機ハンドルを使用したリッスン用
        static ManualResetEventSlim mres = new ManualResetEventSlim(false);

        // 時間のかかる処理を行うメソッド
        private async Task<string> DoWorkAsync3(
            IProgress<int> progress,
            int n,
            CancellationToken token)
        {
            // 時間のかかる処理
            for (int i = 1; i <= n; i++)
            {
                token.ThrowIfCancellationRequested();

                try
                {
                    // Microsoft のドキュメントをまねて mres.Wait(token); と
                    // するとここでフリーズしてしまう。「CancellationToken
                    // が信号を受信するまで、現在のスレッドをブロックします」
                    // ということだから当たり前か・・・
                    // mres.Wait(10, token); にすると 10 ミリ秒待って動くよう
                    // になるが、それでは意味がなさそう
                    mres.Wait(10, token);
                }
                catch (OperationCanceledException)
                {
                    // 必要なら何らかの処置
                    throw;
                }

                await Task.Delay(100);

                int percentage = i * 100 / n;
                progress.Report(percentage);
            }

            return "全て完了";
        }

        // ボタンの Enable / Disable を切り替えるヘルパメソッド
        private void DisableButtons()
        {
            button1.Enabled = false;
            button2.Enabled = false;
            button3.Enabled = true;
            button4.Enabled = false;
            button5.Enabled = false;
        }

        private void EableButtons()
        {
            button1.Enabled = true;
            button2.Enabled = true;
            button3.Enabled = false;
            button4.Enabled = true;
            button5.Enabled = true;
        }
    }

    // Microsoft のドキュメントをまねて Web にアクセスする
    // WebClient をシミュレートする自作クラス
    public class MyWebClient
    {
        private bool isCanceled = false;
        private int progress = 0;

        public async Task<string> GetStringAsync(int n)
        {
            for (int i = 1; i <= n; i++)
            {
                if (isCanceled) break;

                await Task.Delay(100);

                progress = i * 100 / n;
            }

            if (isCanceled)
            {
                return "キャンセル";
            }
            else
            {
                return "全て完了";
            }
        }

        public async Task CancelAsync()
        {
            // 通信・処理に要する時間
            await Task.Delay(100);
            isCanceled = true;
        }

        public int Progress
        {
            get { return progress; }
        }
    }
}

待機ハンドルを使用する方法については、そもそも使い方が間違っている(上のような Windows Forms アプリで使うものではない?)ような気がしますが、せっかくいろいろ考えたので、とりあえず書いておきました。

上のコードのコメントに書きましたが、Microsoft のドキュメントをまねて mres.Wait(token); を使うと、そこでブロックされてフリーズしてしまいます。mres.Wait(10, token); にすると 10 ミリ秒待って一応は動くようになるのですが、それは意味がなさそうです。今後の検討課題ということで・・・

Tags: , ,

.NET Framework

await と Task.Result によるデッドロック

by WebSurfer 2020年9月20日 12:50

先の記事「非同期プログラミング」の続きで、非同期プログラミングに関して調べたことを備忘録的に書いたものです。

(4) デッドロック

先の記事の「(3) 訳が分からなかった話」に書いた Task.Result プロパティを使ったコードを Windows Forms のような GUI アプリに使うとデッドロックになります。(注: 先の記事のコンソールアプリではデッドロックになることはありません。理由後述)

Windows Forms アプリのコードでそのあたりを分かりやすく書くと以下のような感じです。コメントに「// デッドロック」と書いた方のメソッドを実行するとデッドロックが発生してアプリは固まってしまいます。

using System;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsAsyncTest
{
    public partial class Form3 : Form
    {
        public Form3()
        {
            InitializeComponent();
        }

        // デッドロック        
        private void button1_Click(object sender, EventArgs e)
        {
            label1.Text = "";
            string str = TimeCosumingMethod().Result;
            label1.Text = str;
        }

        // 正常動作
        private async void button2_Click(object sender, EventArgs e)
        {
            label1.Text = "";
            string str = await TimeCosumingMethod();
            label1.Text = str;
        }

        private async Task<string> TimeCosumingMethod()
        { 
            await Task.Delay(3000);
            return "TimeCosumingMethod の戻り値";
        }
    }
}

ちなみに ASP.NET Web アプリでも、Task.Result を使った同様なコードで、デッドロックが発生してアプリは固まってしまいます。

その理由は Microsoft のドキュメント「非同期プログラミングのベストプラクティス」の「すべて非同期にする」のセクションに書いてあります。

・・・が、自分はその説明を読んでもよく分かりませんでした。(汗) 自分の独断と偏見による個人的解釈まじりですが、以下のようなことになっているのであろうと想像しています。

Microsoft のドキュメント「Task<TResult>.Result プロパティ」と「Task.Wait メソッド」に以下の説明があります。

"Task.Result プロパティの get アクセサーにアクセスすると、非同期操作が完了するまで呼び出し元のスレッドがブロックされます。これは、Wait メソッドを呼び出すことと同じです。"

"Wait は、現在のタスクが完了するまで呼び出し元のスレッドを待機させる同期メソッドです。"

また、Microsoft のドキュメント「非同期プログラミングのベストプラクティス」に以下の説明があります。

"既定では、未完了の Task を待機するときは、現在のコンテキストがキャプチャされ、Task が完了するときのメソッドの再開に使用されます。 ・・・中略・・・ GUI アプリケーションと ASP.NET アプリケーションには、一度に実行するコードを 1 つのチャンクに限定する SynchronizationContext が使われます。await が完了するときは、キャプチャしたコンテキスト内で async メソッドの残りを実行しようとします。しかし、このコンテキストは既にその内部にスレッドを持っており、これは asyncメソッドが完了するのを (同期して) 待機します。それらは、それぞれもう一方を待機し、デッドロックを引き起こします。"

ということで、以下のようになっているのだろうと思いました。

  1. まず、上のコードの「デッドロック」とコメントしてある button1_Click メソッドに注目。その中の Task.Result プロパティを使っているところで、呼び出し元のスレッド(UI スレッド)は TimeCosumingMethod メソッドが完了するのを同期的に待機する。

    注: 上に書いた公式ドキュメントには Task.Result で「非同期操作が完了するまで呼び出し元のスレッドがブロックされます」とありますが、実際にコードを書いて検証してみると、呼び出し先の TimeCosumingMethod メソッドの await の前まで呼び出し元のスレッド(上の例では UI スレッド)で実行され、await の行に制御が移るとそこでデッドロックになります。
  2. 呼び出された TimeCosumingMethod メソッドに制御が移り、その中の await Task.Delay(3000); の await で待機するとき現在のコンテキストがキャプチャされ、await が完了した後はキャプチャしたコンテキストで残りを実行しようとする。  
  3. Windows Forms アプリでは、先の記事の「(2) Windows Forms アプリの ManagedThreadId」の例のとおり、スレッドの ManagedThreadId の値は変わらない。即ち、await で待機するときキャプチャする「現在のコンテキスト」および await 完了後に使われるキャプチャしたコンテキストでは同じ UI スレッドが使われ続けている。
  4. ということは、TimeCosumingMethod メソッドで await 完了後に使われる「キャプチャした現在のコンテキスト」のスレッドは、ステップ 1 で待機しているスレッドと同じということになる。
  5. ステップ 1 で待機しているスレッドは TimeCosumingMethod メソッドが完了しないと解放されないが、TimeCosumingMethod メソッド内の await が完了した時点では解放されていないので、その場所で開放されるのを永遠に待つことになってデッドロックに陥る。    

Windows Forms のような GUI アプリの場合は、実際にコードを書いての検証結果から、上に書いた通り UI スレッドが使われるのを確認しており、上記 1 ~ 5 で説明になっていると思います。

が、ASP.NET のケースが上記 1 ~ 5 では説明できません。ASP.NET で非同期にする目的はスレッドプールのスレッドの有効利用で、await 前までに使っていたスレッドはスレッドプールに戻し、await 完了後の処理はスレッドプールから新たにスレッドを取得して行いますので。(ASP.NET の場合 await 前後でスレッドが異なるということです)

Windows Forms アプリと異なり、ASP.NET アプリでは使うスレッドがどうなっているかより「一度に実行するコードを 1 つのチャンクに限定する」の方が関係しているのかも。つまり、以下のようなことではないかと思っています。

まず、button1_Click メソッドの Task.Result で 1 つの同期ブロックが待機中。 呼び出された TimeCosumingMethod メソッドの await で待機する際にキャプチャされた「現在のコンテキスト」で await 完了後の同期処理を実行しようとする。しかし「現在のコンテキスト」では一度に実行するコードは 1 つのチャンクに限定されている。なので、Task.Result での待機が終わるまで await 完了後の同期処理は実行できない。結果デッドロックに陥る。

Windows Forms アプリでデッドロックする理由も上記のようなことなのかもしれません。が、今はこれ以上調べる気力がないです。もし何か分かったら追記します。


一方、コンソールアプリでは SynchronizationContext.Current は null になります。ということは await で待機する際にキャプチャできる「現在のコンテキスト」は存在せず、await 完了後の残りの処理は ASP.NET や Windows Forms アプリとは異なるようです。「非同期プログラミングのベストプラクティス」に以下のように書いてあります。

"コンソールアプリケーションでは、一度に 1 つのチャンクに制限する SynchronizationContext ではなく、スレッドプールを備えた SynchronizationContext を使用するため、await が完了するとき、スレッド プールのスレッドで async メソッドの残り処理のスケジュールが設定されます。"

上のステップ 2 のところで TimeCosumingMethod メソッドの残りを実行するのは「スレッドプールを備えた SynchronizationContext」になるようです。await で待機するときにキャプチャした「現在のコンテキスト」ではないのでデッドロックにならないということのようです。


最後に、上のサンプルコードで「// 正常動作」とコメントしたように Task.Result を使わないで 2 ヶ所で await した場合はデッドロックにはならないのは何故でしょう?

参考にさせていただ記事「ASP.NET の非同期でありがちな Deadlock を克服する」に以下のように書いてありました。

"AspNetSynchronizationContext に Post された 2 つの同期ブロックを、例の医者と患者の方式で順番に処理してくれるから"

・・・だそうですが、正直言ってその説明では分かりませんでした。(汗) はっきりした理由が分かったら追記します。


ConfigureAwait によるデッドロックの回避に続く。

Tags: , , , , ,

.NET Framework

非同期プログラミング

by WebSurfer 2020年9月13日 20:01

表題の「非同期プログラミング」と言うと高度な話のように思えるかもしれませんが、実は大したことは書いてなくて、ある機会にいろいろ調べたことを備忘録的に書いただけです。(汗)

Windows Forms アプリの ManagedThreadId

上の画像はそのいろいろ調べたことの一つで、Windows Forms アプリで非同期にメソッドを実行して ManagedThreadId プロパティの値がどうなるかを調べた結果です。説明は下の (2) に書きました。

(1) コンソールアプリの ManagedThreadId

以下のコードを実行すると結果はどうなるでしょう? ManagedThreadId は 1 ⇒ 3 ⇒ 3 とか、時々、1 ⇒ 3 ⇒ 4 と言うようにすべて違うスレッドになります。

static async Task Main(string[] args)
{
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    await Task.Run(() =>
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    });
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}

ちなみに、上の Task.Run メソッドは何かと言うと "スレッドプール上で実行する指定された作業をキューに配置し、その作業を表す Task オブジェクトを戻します。" というものだそうです。要するに、上のコード例で言うと、Run メソッドの引数にある Console.WriteLine をメインスレッドとは別のスレッドで実行するものです。

何故 1 ⇒ 3 ⇒ 4 と言うようにすべて違うスレッドになるかというと、3 つの Console.WriteLine を実行するスレッドは、OS がその時の状況に応じてスレッドプールから良しなに選んでくるからということらしいです。

Microsoft のドキュメント「非同期プログラミングのベストプラクティス」の「すべて非同期にする」のセクションに以下のように書いてあります。

"コンソールアプリケーションでは、一度に 1 つのチャンクに制限する SynchronizationContext ではなく、スレッドプールを備えた SynchronizationContext を使用するため、await が完了するとき、スレッド プールのスレッドで async メソッドの残り処理のスケジュールが設定されます。"

"They have a thread pool SynchronizationContext instead of a one-chunk-at-a-time SynchronizationContext, so when the await completes, it schedules the remainder of the async method on a thread pool thread."

(注: 上の「一度に 1 つのチャンクに制限する SynchronizationContext」というのは GUI アプリと ASP.NET アプリに使われるものだそうです)

それが実行結果が 1 ⇒ 3 ⇒ 4 と言うようにすべて違うスレッドになることがあるという理由のようです。

(2) Windows Forms アプリの ManagedThreadId

この記事の一番上の画像が Windows Forms アプリで非同期にメソッドを実行して ManagedThreadId の値がどうなるか調べた結果です。

コンソールアプリとの一番大きな違いは。メインスレッド(UI スレッド)の ManagedThreadId の値は同じになる(上の画像の例では 1 で固定)、即ち同じスレッドが使われ続けるということです。

Microsoft のドキュメント「非同期プログラミングのベストプラクティス」の「すべて非同期にする」のセクションに以下のように書いてあり、これがメインスレッドには同じスレッドが使われ続ける理由のようです。

"既定では、未完了の Task を待機するときは、現在のコンテキストがキャプチャされ、Task が完了するときのメソッドの再開に使用されます。このコンテキストは現在の SynchronizationContext で、Null の場合は現在の TaskScheduler になります。GUI アプリケーションと ASP.NET アプリケーションには、一度に実行するコードを 1 つのチャンクに限定する SynchronizationContext があります。await が完了するときは、キャプチャしたコンテキスト内で async メソッドの残りを実行しようとします。"

"By default, when an incomplete Task is awaited, the current context is captured and used to resume the method when the Task completes. This context is the current SynchronizationContext unless it’s null, in which case it’s the current TaskScheduler. GUI and ASP.NET applications have a SynchronizationContext that permits only one chunk of code to run at a time. When the await completes, it attempts to execute the remainder of the async method within the captured context."

メッセージループでマウスのクリックやキーボードのストロークなどのユーザーイベントを処理して UI に反映する必要があることも、Windows アプリの UI スレッドは常に同じ&固定になる理由なのかもしれません(想像です)。

メッセージループに関しては、@IT の記事「第3回 Windowsアプリケーションの正体 (3/4)」や Microsoft のドキュメント「Application.Run メソッド」が自分的には参考になりました。

上の画像の Windows Forms アプリのコードを以下に書いておきます。

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

// メインスレッドの ManagedThreadId は最後まで同じ ID が保たれる

namespace WinFormsApp1
{
    public partial class Form7 : Form
    {
        public Form7()
        {
            InitializeComponent();

            textBox1.Text = "呼び出されたメソッド内の";
        }

        private async void button2_Click(object sender, EventArgs e)
        {
            label1.Text = 
                $"メイン ID: {Thread.CurrentThread.ManagedThreadId} / ";
            label2.Text = "";

            // await キーワードは TimeCosumingMethod の中で別スレッド
            // で実行される部分が終了するのを待つという意味。
            // その間メッセージループは処理されるのでフリーズはしない
            label2.Text = await TimeCosumingMethod(textBox1.Text);

            label1.Text += Thread.CurrentThread.ManagedThreadId;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            label3.Text = "[処理中にクリック]ボタンがクリックされた " + 
                Thread.CurrentThread.ManagedThreadId;
        }


        private async Task<string> TimeCosumingMethod(string s)
        {
            if (string.IsNullOrEmpty(s))
            {
                throw new ArgumentException("引数が無い");
            }
            s += $" ID: {Thread.CurrentThread.ManagedThreadId} / ";

            s += await MyMethod();

            s += Thread.CurrentThread.ManagedThreadId;

            return s;
        }


        private async Task<string> MyMethod()
        {
            string s1 = $"MyMethod {Thread.CurrentThread.ManagedThreadId} (IN), ";

            string s2 = await MyMethod2();

            s1 += $"{Thread.CurrentThread.ManagedThreadId} (OUT) / " + s2;
            return s1;
        }

        private async Task<string> MyMethod2()
        {
            string s = $"MyMethod2 {Thread.CurrentThread.ManagedThreadId} (IN), ";

            // Task.Delay(3000) で 3000 ms 後に完了するタスクを作成。
            // そのタスクは別スレッドで実行される。await があるので
            // タスクの完了を待つ
            await Task.Delay(3000);

            s += $"{Thread.CurrentThread.ManagedThreadId} (OUT) / ";
            return s;
        }
    }
}

上記のコードでは UI スレッドと別のスレッドで実行されるのは MyMethod2 メソッドの中の Task.Delay(3000) だけになるようです。上の画像で ManagedThreadId がすべて 1 になっているのがそれを裏付けていると思います。

ちなみに、TimeCosumingMethod メソッドの中で s += await MyMethod(); を s += await Task.Run(() => MyMethod()); に変更すると、MyMethod メソッドは UI スレッドとは別のスレッドで実行されます。結果は以下の画像のとおりで、赤枠の部分が上のケースとは異なってきます。

Windows Forms アプリの ManagedThreadId (その2)

(3) 訳が分からなかった話

下の画像はそのいろいろ調べた過程でコンソールアプリで間違った(?)やり方をして訳が分からなくなった結果です。また分からなくなると何なので(今でもホントに分かっているかは疑問ですが)、どういう話だったのかを書いておくことにしました。

コンソールアプリの ManagedThreadId

下のコードの実行結果が上の画像です。コードの中の (1) の方を実行しています。想像混じりですが、次のように動いていると思われます。

  1. TimeCosumingMethod の中に Task.Delay(5000) が無ければ 100% 単一のスレッドを使用する同期プログラミング。
  2. メインスレッド (ID = 1) で TimeCosumingMethod が呼び出され Task.Delay(5000) まで来る。  
  3. Task.Delay(5000) は 5000 ミリ秒後に完了する別スレッドのタスク。await が付与されているのでその場所でそのタスクの完了を待つ。別スレッドのタスクなのでその時点で ID = 1 のスレッドはフリーになる。(スレッドプールに戻す? それともそのまま同期実行を続ける?・・・そこが不明)
  4. TimeCosumingMethod には await は付与されてないので、すぐ次の行の HeavyMethod を実行しようとする。フリーになった ID = 1 のスレッドで HeavyMethod が実行される。
  5. TimeCosumingMethod 内の Task.Delay(5000) のタスクが完了すると、OS は ID = 4 のスレッドをスレッドプールから取得して、次の行 Console.WriteLine("Task.Delay(5000) の後 ...); から処理を再開する。  
  6. 結果、HeavyMethod は ID = 1 のスレッド、TimeCosumingMethod は ID = 4 のスレッドで並行して実行される形となり、それぞれコードに書かれたとおりのタイミングで文字列をコンソールに書き込んでいく。
  7. HeavyMethod の次の行の Console.WriteLine(task.Result); の引数 task.Result はそこで Wait メソッドを呼び出すことと同じ。それゆえ、そこで TimeCosumingMethod による「4秒経過」~「TimeCosumingMethod おわり」のコンソールへの書き込みを待つ。  
  8. TimeCosumingMethod 完了後、Console.WriteLine(task.Result) により「TimeCosumingMethod の戻り値」、次の Console.WriteLine("Main のおわり ...) により「Main のおわり」がコンソールに書き込まれてアプリは終了。
using System;
using System.Threading.Tasks;
using System.Threading;

namespace ConsoleAppAsync2
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Main のはじまり " + 
                Thread.CurrentThread.ManagedThreadId);

            // (1)
            Task<string> task = TimeCosumingMethod();
            HeavyMethod();

            // Task.Result プロパティの get アクセサーに
            // アクセスすると、非同期操作が完了するまで
            // 呼び出し元のスレッドがブロックされる。こ
            // れは、Wait メソッドを呼び出すことと同じ
            Console.WriteLine(task.Result);

            // (2)
            // 時間のかかる処理は別スレッドで非同期に実行
            // ということであれば以下のようにすると思う         
            //string result = await TimeCosumingMethod();
            //await Task.Run(() => HeavyMethod());
            //Console.WriteLine(result);

            Console.WriteLine("Main のおわり " + 
                Thread.CurrentThread.ManagedThreadId);

            Console.ReadLine();
        }

        static async Task<string> TimeCosumingMethod()
        {
            Console.WriteLine("TimeCosumingMethod はじまり " + 
                Thread.CurrentThread.ManagedThreadId);

            // Task.Delay(5000) で 5 秒後に完了するタスク
            // を作成。そのタスクは別スレッドで実行される
            // await でそのタスクの完了を待つ。await 無し
            // では完了を待たず下の行に進む
            await Task.Delay(5000);

            Console.WriteLine("Task.Delay(5000) の後 " + 
                Thread.CurrentThread.ManagedThreadId);

            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine(i + 1 + "秒経過 " + 
                    Thread.CurrentThread.ManagedThreadId);
            }

            Console.WriteLine("TimeCosumingMethod おわり " + 
                Thread.CurrentThread.ManagedThreadId);

            return "TimeCosumingMethod の戻り値 " + 
                Thread.CurrentThread.ManagedThreadId;
        }

        static void HeavyMethod()
        {
            Console.WriteLine("HeavyMethod はじまり " + 
                Thread.CurrentThread.ManagedThreadId);

            Thread.Sleep(9000);

            Console.WriteLine("HeavyMethod おわり " +
                Thread.CurrentThread.ManagedThreadId);
        }
    }
}

時間のかかる処理を別スレッドで非同期に実行ということであれば、上のコードのコメントアウトした (2) のようにするのが普通と思います。その場合の実行結果は以下の画像のようになります。

コンソールアプリの ManagedThreadId (その2)

これなら少なくとも訳が分からないということはないと思うのですが、どうでしょう? まぁ、訳が分からないのは自分が無知だからということは否めませんが。(汗)


await と Task.Result によるデッドロックに続く。

Tags: , , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar