C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

Task起動先が、Task起動元の終了を検知する方法について

[トピック内 9 記事 (1 - 9 表示)]  << 0 >>

■96002 / inTopicNo.1)  Task起動先が、Task起動元の終了を検知する方法について
  
□投稿者/ taro (11回)-(2020/10/15(Thu) 10:33:04)

分類:[C#] 

VisualStudio2019で、C#の学習をしております。
当方、C++やJAVA等の他言語は多少の経験がありますが、C#は未経験です。

以下のようなテストアプリを作って、Taskの学習をしています。

・アプリ起動すると、ボタンが1つだけのForm1が起動する。
・Form1のボタンを押すと、Form2が起動する(Form1はForm2を起動する為だけのもの)。
・Form2は
 ・終了ボタン(押すとForm2を終了し、処理をForm1に戻す)「button1」
 ・ログ出力テキストボックス「textBox1」
 だけがある。
・Form2起動時(Shownイベント)に、「処理タスク」を起動する。
・「処理タスク」は、ログ出力テキストボックスに対して、1秒間隔で"処理"という文字列を出力する。


■困っていること

Form2を起動し、1秒間隔で"処理"と出力されている途中に、終了ボタンを押すと
-----
System.InvalidOperationException: 'ウィンドウ ハンドルが作成される前、コントロールで Invoke または BeginInvoke を呼び出せません。'
-----
が発生する。
原因は、ログ出力テキストボックスがあるForm2が既に閉じられている為と思われる。


■Form2のコード(Form1は割愛)

public partial class Form2 : Form
{
  public Form2()
  {
    InitializeComponent();
  }

  /// <summary>
  /// 「終了」ボタン押下
  /// </summary>
  private void button1_Click(object sender, EventArgs e)
  {
    // フォームを閉じて上位に返す
    this.DialogResult = DialogResult.OK;
    this.Close();
  }
  

  /// <summary>
  /// フォーム表示直後に一度だけ実行されるShownイベントのコールバック
  /// </summary>
  private void Form2_Shown(object sender, EventArgs e)
  {
    // テキストボックスにログ出力
    // (このテキストボックスは、メインタスクからも処理タスクからも出力処理が行われる)
    textBox1.AppendText("UDP通信タスクを起動します" + "\r\n");

    // 「処理タスク」を起動
    Task task = Task.Run(() => {
      RunProcTask();
    });
  }

  /// <summary>
  /// 処理タスク
  /// </summary>
  private void RunProcTask()
  {
    // 1秒ごとにログを出力する
    while (true) {
      // 1秒待ってからログ出力
      Thread.Sleep(1000);
      //textBox1.AppendText("処理" + "\r\n");  //←このような出力はできない
      Invoke(new Action<string>(this._PutLog), "処理");  //←ここで例外が発生する
    }
  }

  /// <summary>
  /// 処理タスクからログ出力を行う
  /// </summary>
  private void _PutLog(string msg)
  {
    textBox1.AppendText(msg + "\r\n");
  }
}


■教えてほしいこと

・処理タスクからログを出力する方法として、色々調べた結果上記のようにInvokeを用いていますが、
 効率や可読性・処理速度等、総合的に見てベストな対応でしょうか?
・InvalidOperationExceptionを回避する方法として
 ・Form2に「static bool flg;」というメンバを用意し、Form2_Shownメソッド冒頭でtrueセット、button1_Clickメソッド冒頭でfalseセット。
 ・「Invoke(new Action<string>(this._PutLog), "処理");」は、flgがtrueの場合のみ実行
 という対応をしました。
 「Form1起動→ボタンを押下しForm2起動→"処理"出力中に終了ボタン押下→無事Form1に戻る」まではいいのですが、
 再度Form1のボタンを押下しForm2を起動したところ、
 System.InvalidOperationException: 'ウィンドウ ハンドルが作成される前、コントロールで Invoke または BeginInvoke を呼び出せません。'
 というエラーが発生しました。
 何か足らないことがあるのでしょうか?
・上記の「Form2が存在するかどうかをフラグで判定」という方法はベストではない気がしています。
 本当は、終了ボタン押下時に「処理タスクを止める」という事をしたかったのですが、やり方が分からず・・・。
 ベストな実装はどのようなものでしょうか?


長くなりましたが、よろしくお願い致します。


 



引用返信 編集キー/
■96003 / inTopicNo.2)  Re[1]: Task起動先が、Task起動元の終了を検知する方法について
□投稿者/ WebSurfer (2122回)-(2020/10/15(Thu) 10:51:15)
No96002 (taro さん) に返信

以下の記事はご存じですか?

非同期プログラミングのベスト プラクティス
https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming

特に「自分のツールを理解する」のセクションにある、

"キャンセルの処理方法と進捗状況の報告方法です。基本クラス ライブラリ (BCL) には、この問題を
解決するための CancellationTokenSource/CancellationToken と IProgress<T>/Progress<T> という
型があります"

あたり。

引用返信 編集キー/
■96005 / inTopicNo.3)  Re[1]: Task起動先が、Task起動元の終了を検知する方法について
□投稿者/ WebSurfer (2123回)-(2020/10/15(Thu) 11:10:17)
No96002 (taro さん) に返信

> 終了ボタン押下時に「処理タスクを止める」という事をしたかったのですが、やり方が分からず・・・。

そのあたりは以下の記事が参考になりませんか?

マネージド スレッドのキャンセル
https://docs.microsoft.com/ja-jp/dotnet/standard/threading/cancellation-in-managed-threads

上の記事のサンプルはコンソールアプリですが、Windows Forms アプリに実装した例を以下の記事に
書きましたので、よろしければ参考にしてください。進捗も表示するようにしています。

非同期タスクのキャンセル
http://surferonwww.info/BlogEngine/post/2020/09/27/cancellation-of-async-task.aspx
引用返信 編集キー/
■96009 / inTopicNo.4)  Re[1]: Task起動先が、Task起動元の終了を検知する方法について
□投稿者/ kiku (199回)-(2020/10/15(Thu) 14:58:55)
No96002 (taro さん) に返信
> ■教えてほしいこと
> ・上記の「Form2が存在するかどうかをフラグで判定」という方法はベストではない気がしています。
>  本当は、終了ボタン押下時に「処理タスクを止める」という事をしたかったのですが、やり方が分からず・・・。
>  ベストな実装はどのようなものでしょうか?

ベストではないかもしれませんが、
こんな方法でタスクを止めることができます。

    public partial class Form2 : Form
    {
        private CancellationTokenSource cts;
        private bool TaskEnd = false;

        public Form2()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // フォームを閉じて上位に返す
            this.DialogResult = DialogResult.OK;
            this.Close();
        }

        private void Form2_Shown(object sender, EventArgs e)
        {
            textBox1.AppendText("UDP通信タスクを起動します" + "\r\n");
            cts = new CancellationTokenSource();
            Task task = Task.Run(() => {
                RunProcTask(cts.Token);
            });
        }

        private void RunProcTask(CancellationToken ct)
        {
            try
            {
                TaskEnd = false;
                while (true)
                {
                    if (ct.IsCancellationRequested) return;
                    //故意にここにスリープを入れた。
                    //これによりここで終了ボタンが押される確率が高くなる
                    Thread.Sleep(1000);
                    Invoke(new Action<string>(this._PutLog), "処理");
                }
            }
            finally
            {
                TaskEnd = true;
            }
        }

        private void _PutLog(string msg)
        {
            textBox1.AppendText(msg + "\r\n");
        }

        private void Form2_FormClosing(object sender, FormClosingEventArgs e)
        {
            cts.Cancel();
            while (TaskEnd == false)
            {
                //これがないとデットロック
                //Invokeを実行できるようにしている
                Application.DoEvents();
            }
        }
    }

引用返信 編集キー/
■96010 / inTopicNo.5)  Re[2]: Task起動先が、Task起動元の終了を検知する方法について
□投稿者/ furu (78回)-(2020/10/15(Thu) 15:45:46)
No96009 (kiku さん) に返信
FormClosingイベントのタスク終了判定は
IsCompletedを使ったほうが
TaskEnd変数の処理がいらなくなり
すっきりします。

それとToken渡さなくても大丈夫な気がします。

    //private bool TaskEnd = false;
    private Task task;

    private void Form2_Shown(object sender, EventArgs e)
    {
        textBox1.AppendText("UDP通信タスクを起動します" + "\r\n");
        cts = new CancellationTokenSource();
        task = Task.Run(RunProcTask);
    }

    private void RunProcTask(C)
    {
        while (true)
        {
            if (cts.IsCancellationRequested) return;
            Thread.Sleep(1000);
            Invoke(new Action<string>(this._PutLog), "処理");
        }
    }

    private void Form2_FormClosing(object sender, FormClosingEventArgs e)
    {
        cts.Cancel();

        while (!task.IsCompleted)
        {
            Application.DoEvents();
        }
    }

引用返信 編集キー/
■96011 / inTopicNo.6)  Re[3]: Task起動先が、Task起動元の終了を検知する方法について
□投稿者/ kiku (200回)-(2020/10/15(Thu) 15:48:35)
No96010 (furu さん) に返信
> ■No96009 (kiku さん) に返信
> FormClosingイベントのタスク終了判定は
> IsCompletedを使ったほうが
> TaskEnd変数の処理がいらなくなり
> すっきりします。
>
> それとToken渡さなくても大丈夫な気がします。

いいですね。
引用返信 編集キー/
■96012 / inTopicNo.7)  Re[4]: Task起動先が、Task起動元の終了を検知する方法について
□投稿者/ furu (79回)-(2020/10/15(Thu) 16:03:28)
申し訳ない。


  private void RunProcTask(C)
    ↓↓↓
  private void RunProcTask()

引用返信 編集キー/
■96016 / inTopicNo.8)  Re[1]: Task起動先が、Task起動元の終了を検知する方法について
□投稿者/ WebSurfer (2125回)-(2020/10/16(Fri) 11:05:59)
No96002 (taro さん) に返信

No96003 に書きました、

非同期プログラミングのベスト プラクティス
https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming

に沿ったサンプルを以下に書いておきます。

上の記事に書いてあるように「すべて非同期にする」ようにし、「自分のツールを理解する」に書い
てあるように CancellationTokenSource/CancellationToken と IProgress<T>/Progress<T> を利用し
ます。

Form1 から初期化して表示する Form2 という形ではなくて、Form1 単独として button1_Click で 
RunProcTask を起動、button2_Click でキャンセルしています。

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

namespace WindowsFormsAsyncTest
{
    public partial class Form1 : Form
    {
        private CancellationTokenSource cts = null;

        public Form5()
        {
            InitializeComponent();
        }

        // 開始(質問のコードの Form2_Shown の代わり)
        private async void button1_Click(object sender, EventArgs e)
        {
            this.textBox1.Text = "タスク起動";

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

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

        // キャンセル
        private void button2_Click(object sender, EventArgs e)
        {
            if (this.cts != null) cts.Cancel();
        }


        // 処理タスク
        // ポーリングによるキャンセルのリッスン
        //  IProgress<T>/Progress<T> で進捗をレポート
        private async Task RunProcTask(IProgress<string> progress, CancellationToken token)
        {
            int i = 0;
            while (true)
            {
                token.ThrowIfCancellationRequested();
                await Task.Delay(1000);
                progress.Report(i.ToString());
                i++;
            }
        }

        // 進捗を TextBox に表示するコールバック
        // UIスレッドで呼び出される
        private void ShowProgress(string log)
        {
            this.textBox1.Text = "ログ出力その " + log;
        }

    }
}

マネージド スレッドのキャンセル
https://docs.microsoft.com/ja-jp/dotnet/standard/threading/cancellation-in-managed-threads

に書いてある「ポーリングによるリッスン」を行っています。RunProcTask メソッドの中の

token.ThrowIfCancellationRequested();

がそれで、以下のコードと同じ結果になります。

if (token.IsCancellationRequested)   
    throw new OperationCanceledException(token);

引用返信 編集キー/
■96093 / inTopicNo.9)  Re[1]: Task起動先が、Task起動元の終了を検知する方法について
□投稿者/ WebSurfer (2136回)-(2020/10/21(Wed) 11:50:30)
No96002 (taro さん) に返信

質問者さん、最初の質問の投稿以来無言ですが、回答&レスが多々ついているので、それらに
対するフィードバックを書いてください。読んだ/読まなかった、分かった/分からなかった、
役に立った/立たなかったぐらいはすぐ返事できるはず。

役に立たなかったなら、どこが期待する回答と違うか書いてもらえると代案などが出てきて、
問題解決に結びつくかも。

とにかく無言は NG です。
引用返信 編集キー/

このトピックをツリーで一括表示


トピック内ページ移動 / << 0 >>

このトピックに書きこむ