WebSurfer's Home

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

タスク並列ライブラリ (TPL)

by WebSurfer 2020年12月27日 14:53

タスク並列ライブラリ (TPL) は Windows Forms のような GUI アプリでは使い難い点があるということを今更ながらですが学びましたので、備忘録として書いておきます。

タスク並列ライブラリの検証

最近の PC の CPU はマルチコアが当たり前になっているようですが、PC のマルチコアを有効に使うプログラミングをサポートするため、.NET Framework 4.0 で導入されたタスク並列ライブラリ (TPL・・・Parallel.For とか Parallel.Invoke とか) を利用するという話を巷でよく耳にします。

マルチスレッドの基本的な話ですが、@IT の記事「第1回 マルチスレッドはこんなときに使う (1/2)」の「マルチスレッドの動作原理」セクションを見てください。リンク切れになると困るので画像だけ借用して以下に表示しておきます。

マルチスレッドの比較

② のようになると OS がスレッドを切り替えて処理を行うのでそのオーバーヘッドの分逆に遅くなるということになります。なので、マルチスレッドアプリで処置時間の短縮を図るなら ③ のようにマルチコアを利用できる環境が必要で、さらにマルチコアを有効に利用するプログラミングを行うという話になると思います。

その際に自分がよく聞くのがタスク並列ライブラリ (TPL) や Parallel LINQ (PLINQ) を使うという話です。Microsoft のドキュメント「.NET での並列プログラミング」や、ググるとヒットする記事例えば @IT の記事「ループをParallelクラスで並列処理にするには?」を読むと TPL, PLINQ は並列処理には万能のような気がしてました。

Microsoft のドキュメント「タスク並列ライブラリ (TPL)」には、

"TPL は、使用可能なすべてのプロセッサを最も効率的に使用するように、コンカレンシーの程度を動的に拡大します。The TPL scales the degree of concurrency dynamically to most efficiently use all the processors that are available."

・・・と言う記述がありますし、PC のマルチコアを有効に使うという局面に限れば最強のように思えます。

しかし、Windows Forms のような GUI アプリではそうでもなさそうな感じです。自分が気が付いた限りですが、以下の 2 点が問題だと思いました。(自分が回避策を知らないだけという可能性は否定できませんが )

  1. 並列に実行される複数のメソッドがすべて完了するまで UI がブロックされる。 ⇒ PLINQ 以外は async / await を使って回避できる方法がありました。下の【2021/6/13 追記】を見てください
  2. 並列実行するメソッドには非同期版は使えない。

以下のサンプルコードの ParallelInvoke_Click メソッドは Parallel.Invoke を使って 5 つの同期版メソッド Work を並列に実行するものですが、5 つのメソッドがすべて完了するまで UI がブロックされるので、アプリはフリーズしたようになります。その他の TPL, PLINQ を使った xxxxx_Click メソッドも同様で、完了するまで UI がブロックされます。それを回避する手段は、自分が探した限りですが、なさそうです。

さらに、非同期版メソッド WorkAsync は使えません。詳しくはサンプルコードのコメントに書きましたので見てください。なので、ライブラリなどで非同期版メソッドしか提供されてない場合は何ともならないと思われます。

というわけで、下のサンプルコードの WhenAll1_Click, WhenAll2_Click メソッドのように、Parallel.Invoke など使わないで、await Task.WhenAll(...) で待機するようにし、並列化については OS に任せるのが良さそうと思いました。(実際にサンプルを動かしてみると並列化してくれているような感じはしました。② のようになっている可能性は否定しきれませんが)

非同期版メソッド WorkAsync も使えます。下のサンプルコードの WhenAll2_Click メソッドを見てください。上の画像がその実行結果です。(ThreadID が 1 で UI スレッドと同じなのは、WorkAsync メソッドの中で ManagedThreadId を取得するのが UI スレッドだからです)

using System;
using System.Windows.Forms;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

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

        // 同期版メソッド
        private string Work(int n)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            string retunVlaue =
                $"n = {n}, ThreadID = {id}, start:{DateTime.Now:ss.fff}, ";

            Thread.Sleep(3000);

            retunVlaue += $"end:{DateTime.Now:ss.fff}";

            return retunVlaue;
        }

        // 非同期版メソッド
        private async Task<string> WorkAsync(int n)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            string retunVlaue =
                $"n = {n}, ThreadID = {id}, start: {DateTime.Now:ss.fff}, ";

            await Task.Delay(3000);

            retunVlaue += $"end: {DateTime.Now:ss.fff}";

            return retunVlaue;
        }

        // 非同期版メソッド 5 つを for ループで逐次実行
        private async void InOrder_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            for (int n = 0; n < 5; n++)
            {
                this.label1.Text += await WorkAsync(n) + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 同期版メソッド 5 つを Parallel.Invoke で実行
        // Executes each of the provided actions, possibly in parallel.
        // 終了まで UI はブロックされる
        private void ParallelInvoke_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.Invoke(
                () => results[0] = Work(0),
                () => results[1] = Work(1),
                () => results[2] = Work(2),
                () => results[3] = Work(3),
                () => results[4] = Work(4));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";

            // 非同期版メソッド WorkAsync は使えない

            // Task.Result でデッドロックになる
            //Task<string> result1 = null, result2 = null;
            //Parallel.Invoke(
            //    () => result1 = WorkAsync(0),
            //    () => result2 = WorkAsync(1));
            //this.label1.Text += result1.Result + "\r\n";
            //this.label1.Text += result2.Result + "\r\n";

            // await で待つことなく終わってしまう
            //string result1 = "", result2 = "";
            //Parallel.Invoke(
            //    async () => result1 = await WorkAsync(0),
            //    async () => result2 = await WorkAsync(1));
            //this.label1.Text += result1 + "\r\n";
            //this.label1.Text += result2 + "\r\n";

        }

        // 同期版メソッド 5 つを Parallel.For で実行
        // Executes a for loop in which iterations may run in parallel.
        // 終了まで UI はブロックされる
        private void ParallelFor_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.For(0, 5, n => results[n] = Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 同期版メソッド 5 つを Parallel.ForEach で実行
        // Executes a foreach operation in which iterations may run in parallel.
        // 終了まで UI はブロックされる
        private void ParallelForEach_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.ForEach(Enumerable.Range(0, 5), 
                             n => results[n] = Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 同期版メソッド 5 つを PLINQ で実行
        // PLINQ は、システムのすべてのプロセッ��を十分に活用しようとする。
        // そのために、データ ソースをセグメントにパーティション分割し、
        // 複数のプロセッサで個々のワーカー スレッドの各セグメントに対して
        // クエリを並行実行します。
        // 終了まで UI はブロックされる
        private void PLINQ_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            var results = Enumerable.Range(0, 5).AsParallel().
                          Select(n => Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // ***** 以下 TPL, PLINQ に代えて Task.WhenAll を使用 *****

        // 同期版メソッド 5 つを Task.Run で実行、
        // await Task.WhenAll で待機        
        private async void WhenAll1_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];
            var taskList = new List<Task>();
            for (int n = 0; n < 5; n++)
            {
                int i = n;
                taskList.Add(Task.Run(() => results[i] = Work(i)));
            }

            // WaitAll ではブロックされてしまう。
            // WhenAll でないと await で待機できないので注意
            await Task.WhenAll(taskList.ToArray());

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを実行、
        // await Task.WhenAll で待機
        private async void WhenAll2_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            Task<string>[] results = new Task<string>[5];

            for (int n = 0; n < 5; n++)
            {
                int i = n;
                results[i] = WorkAsync(i);
            }

            // WaitAll ではデッドロックになる
            await Task.WhenAll(results);

            foreach (Task<string> result in results)
            {
                this.label1.Text += result.Result + "\r\n";
            }

            this.label1.Text += "完了";
        }
    }
}

-----【2021/6/13 追記】-----

UI スレッドがブロックされるのを回避する方法ですが、Parallel LINQ (PLINQ) 以外は、.NET Framework 4.5 から導入された async / await を使って TPL の部分を await Task.Run で動かすと UI スレッドはブロックされず、メッセージループでマウスのクリックやキーボードのストロークなどのユーザーイベントが処理されることが分かりました。

具体的には、Parallel.For を使った場合を例に取ると、以下のようにします。

private async void ParallelFor_Click(object sender, EventArgs e)
{
    int id = Thread.CurrentThread.ManagedThreadId;
    this.label1.Text = $"UI Thread ID = {id}\r\n";

    string[] results = new string[5];

    await Task.Run(() => Parallel.For(0, 5, n => results[n] = Work(n)));

    foreach (string result in results)
    {
        this.label1.Text += result + "\r\n";
    }

    this.label1.Text += "完了";
}

ただ、async / await が使えるなら、上のコード例の await Task.WhenAll で待機することもできますので、TPL は使わなくても良いのではないかという気はします。上に書いた "TPL は、使用可能なすべてのプロセッサを最も効率的に使用するように、コンカレンシーの程度を動的に拡大します" という点に意味があるのかもしれませんが。

なお、Parallel LINQ (PLINQ) の方は以下のようにしてみたのですが UI スレッドがブロックされるのは回避できないようで、マウスのクリックなどのユーザーイベントには無反応(フリーズ状態)のままになります。

var results = await Task.Run(() => Enumerable.Range(0, 5)
                                   .AsParallel()
                                   .Select(n => Work(n)));

PLINQ では回避できない理由や対応策は分かりません。今後の検討課題ということで。

Tags: , , , , ,

.NET Framework

SynchronizationContext とは?

by WebSurfer 2020年9月30日 15:53

SynchronizationContext とは何かを調べましたので、十分とは言えないまでも取りあえず分かった(分かった気になっただけかもしれませんが)ことを備忘録として書いておきます。

SynchronizationContext

非同期プログラミングの勉強の際に、Microsoft のドキュメント「非同期プログラミングのベストプラクティス」を読んだのですが、詳しい説明なしでいきなり、

"・・・async void メソッドが開始されたときにアクティブだった SynchronizationContext で直接発生します・・・・・・未完了の Task を待機するときは、現在の "コンテキスト" がキャプチャされ、Task が完了するときのメソッドの再開に使用されます。この "コンテキスト" は現在の SynchronizationContext で・・・"

というように SynchronizationContext という言葉が出てきます。上記の他にも何か所かで出てくるのですが、非同期プログラミングを理解するのに重要なキーワードのようで、それが何かを理解してない自分には、そのドキュメントの内容が半分も理解できませんでした。

Microsoft のドキュメント「SynchronizationContext クラス」によると .NET Framework 2.0 の時代から存在していたようで、以下のように説明されています。

"同期コンテキストをさまざまな同期モデルに反映させるための基本機能を提供します。SynchronizationContext クラスは、同期なしのフリー スレッド コンテキストを提供する基本クラスです。このクラスで実装する同期モデルの目的は、共通言語ランタイムの非同期または同期の内部操作をさまざまな同期モデルで正しく動作させることです。"

残念ながらその説明では自分の頭では全く理解できません。なので、そのドキュメントからリンクが貼ってある MSDN マガジンの記事「並列コンピューティング - SynchronizationContext こそすべて」を読んでみました。

他に「async/await と SynchronizationContext」、「async/awaitと同時実行制御」、「ASP.NET の非同期でありがちな Deadlock を克服する」という記事を参考にさせていただきました。これらは Microsoft のドキュメントに比べれば少し分かりやすかったです。

上の記事を読んで、SynchronizationContext とは何かを理解する上で重要と思った点を以下にまとめておきます。無知ゆえの独断と偏見による個人的解釈も含まれているので注意してください。

  1. マルチスレッドプログラムでは、あるスレッドから別のスレッドに作業単位を受け渡す必要が生じることがよくある。SynchronizationContext クラスはそれを支援するツール。
  2. Windows Forms, WPF, ASP.NET, Silverlight, コンソールアプリなど、すべての .NET プログラムには SynchronizationContext の概念が含まれる。(公式ドキュメントを見ると .NET だけでなく Core なども適用対象に含まれているようです)  
  3. 古くはメッセージキューを使用して作業単位を受け渡していたが、.NET Framework が登場した時に汎用ソリューションとして ISynchronizeInvoke が考案され、その後 .NET Framework 2.0 で ASP.NET の非同期プログラミングをサポートするため SynchronizationContext に置き換えられた。
  4. SynchronizationContext の特徴は (1) 作業単位をコンテキストのキューにする (unit of work is queued to a context rather than a specific thread)、(2) 全てのスレッドは "現在の" コンテキストを持つ (every thread has a “current” contex)、(3) 未完了の非同期操作の数を管理する (it keeps a count of outstanding asynchronous operations)。
  5. Windows Forms, WPF, ASP.NET に使用されている SynchronizationContext はそれぞれ実装が異なっており、順に以下の通りとなる。(Current プロパティで確認できる)

    WindowsFormsSynchronizationContext
    DispatcherSynchronizationContext
    AspNetSynchronizationContext (.NET 4.5 以降)
  6. Windows Forms, WPF などの GUI アプリでは、await で待機するとき現在の SynchronizationContext がキャプチャされ、await が完了するとキャプチャした SynchronizationContext で続きを実行する。その際に使われるスレッドは await 前後で同じ、即ち UI スレッドになる。
  7. ASP.NET でも await 前後での SynchronizationContext のキャプチャと続きの実行は GUI アプリと同様だが、await 前後で HttpContext が同じになるようにしている。await 前までに使っていたスレッドはスレッドプールに戻し、await が完了後の処理はスレッドプールから新たにスレッドを取得して行う。(GUI アプリとは非同期にする目的が違うため。詳しくは「ASP.NET の非同期/待機の概要」を参照)
  8. コンソールアプリでは SynchronizationContext.Current プロパティは null になる(ということは、await で待機する際に「現在の SynchronizationContext をキャプチャ」ということはないはず)。await が完了するとき、スレッドプールを備えた既定の SynchronizationContext を使って、スレッドプールのスレッドで async メソッドの残り処理のスケジュールが設定される。(なので、「await と Task.Result によるデッドロック」に書いたようなデッドロックには陥らない)

async / await を使った非同期プログラミングではプログラマが SynchronizationContext を意識することはほとんどなさそうで、知らなくても済むような気がします。

実際、以下の画像の赤枠部分のコードのように Control.Invoke の代わりに SynchronizationContext.Post メソッドが使えるということぐらいしか使い道は思い当たりません。(自分が知らないだけという可能性は否定できませんが)

SynchronizationContext.Post

MSDN マガジンの記事「並列コンピューティング - SynchronizationContext こそすべて」にも "開発者を支援するために SynchronizationContext クラスが用意されています。残念なことに、多くの開発者はこの便利なツールに気が付いてすらいません" と書いてありましたが、そうだろうなと思いました。

ただ、async / await を使った非同期プログラミングでも内部的には SynchronizationContext が大きく関与しているのは間違いなく、上記の程度は知っておいて損はないかもしれませんね。

Tags: , ,

.NET Framework

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

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

About this blog

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

Calendar

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

View posts in large calendar