WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

不正なクロススレッドコールの捕捉

by WebSurfer 11. July 2019 16:43

Windows Forms のマルチスレッドアプリで不正なクロススレッドコールがある場合、Visual Studio から[デバッグの開始(S)]でなら以下のような例外が捕捉されるが、[デバッグなしで開始(H)]では捕捉されないという話を書きます。

不正なクロススレッドコールの捕捉

元の話は Teratail の「非同期処理でデバッグ時・ビルド実行時で処理結果が異なる?」というスレッドから来ています。

例えば、Windows Forms アプリの Button.Click のイベントハンドラで、以下のように AsParallel 拡張メソッドを使ってマルチスレッドで処理するケースを考えます。

private void button4_Click(object sender, EventArgs e)
{
    Enumerable.Range(1, 10).AsParallel().ForAll(z =>
    {
        this.textBox2.Text += z.ToString();
    });
}

this.textBox2.Text += z.ToString(); の行で、textBox2 が作成された UI スレッドとは別のスレッドから textBox2 の呼び出しを行っています。これが不正なクロススレッドコールになります。

このコードを Visual Studio から[デバッグの開始(S)]で実行した場合は、デバッガがこの行で InvalidOperationException 例外をスローし、"コントロールが作成されたスレッド以外のスレッドからコントロール 'textBox2' がアクセスされました" というエラーメッセージを表示します。それが上の画像です。

ところが[デバッグなしで開始(H)]で実行した場合は例外はスローされず、あたかもこのコードは正常終了したかのように見えます。集約的例外ハンドラで捕捉してアプリケーションを終了させるということもできません(そもそも例外がスローされないので)。

という訳で、特にマルチスレッドアプリを開発している場合は必ず[デバッグの開始(S)]で実行して動作確認をするようにした方がよさそうです。

なお、[デバッグなしで開始(H)]でも CheckForIllegalCrossThreadCalls プロパティを true(デフォルトは false)に設定すれば不正なクロススレッドコールで例外をスローさせることはできます。

でも、たぶんオーバーヘッドが増えてパフォーマンスに影響があるでしょうからリリース前には元に戻さなければならず、そんな面倒なことをするより[デバッグの開始(S)]で実行して確認するのが正解だと思います。

Tags:

.NET Framework

デリゲートを利用した非同期メソッドの実装

by WebSurfer 19. June 2019 15:10

Windows Forms アプリでのデリゲートを利用した非同期メソッドの実装について備忘録を書いておきます。.NET Framework 4.5 で async / await / Task.Run が利用できる今はデリゲートを使うことはこの先もうないのかもしれませんが。

非同期メソッドの実装

非同期メソッド実装の変遷については @IT の記事「第1回 .NET開発における非同期処理の基礎と歴史」にまとめられています。デリゲートを使う方法とはその記事の中の「Asynchronous Programming Model(非同期プログラミング・モデル)」です。

上の記事では概要しか書いてなくて分かり難いと思います。詳しく知りたい方は、かなり古い記事ですが同じく @IT の「第2回 .NETにおけるマルチスレッドの実装方法を総括 (1/4)」を見ていただいた方が良く理解できると思います。

上の画像は、以下の同期メソッドを、(1) そのまま同期呼び出し、(2) delegate を利用した非同期呼び出し、(3) async / await / Task.Run を利用した非同期呼び出しを行う windows forms アプリを実行したものです。

// テスト用の時間がかかるメソッド
private string TimeCosumingMethod(string s)
{
    if (string.IsNullOrEmpty(s))
    {
        throw new ArgumentException("引数が無い");
    }
    Thread.Sleep(3000);
    return s + " + ManagedThreadId: " + 
                     Thread.CurrentThread.ManagedThreadId;
}

delegate を利用した非同期呼び出しの場合、(a) EndInvoke を行う対象となるデリゲートの取得、(b) UI スレッドに戻り値を渡す、(c) 例外の捕捉が課題になると思います。

具体例は以下のコードを見てください。上の (a) ~ (c) についてはコールバックメソッド MyCallBack 内で行っています。詳しくはコード内にコメントで書きましたのでそれを見てください。(手抜きでスミマセン)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
using System.Runtime.Remoting.Messaging;

namespace WindowsFormsClient
{
  public partial class Form2 : Form
  {
    public Form2()
    {
      InitializeComponent();

      this.textBox1.Text = "サンプル文字列";
    }

    // テスト用の時間がかかるメソッド
    private string TimeCosumingMethod(string s)
    {
      if (string.IsNullOrEmpty(s))
      {
        throw new ArgumentException("引数が無い");
      }
      Thread.Sleep(3000);
      return s + " + ManagedThreadId: " + 
                     Thread.CurrentThread.ManagedThreadId;
    }

    // 同期呼び出し
    private void button1_Click(object sender, EventArgs e)
    {
      this.label1.Text = "ManagedThreadId: " + 
              Thread.CurrentThread.ManagedThreadId + " / ";

      // Application.DoEvents で上の文字列を即ラベルに反映
      Application.DoEvents();

      try
      {
        this.label1.Text += 
                    TimeCosumingMethod(this.textBox1.Text);
      }
      catch(ArgumentException ex)
      {
        MessageBox.Show(ex.Message);
        throw;
      }
    }


    // delegate を利用した非同期呼び出し

    // 別スレッドとして処理したいメソッドをデリゲート宣言
    delegate string MyDelegate(string s);

    // 別スレッドによる処理が終了したことをトリガーとして
    // 自動的に呼び出されるコールバックメソッド
    private void MyCallBack(IAsyncResult ar)
    {
      // 課題 (a) コールバックで EndInvoke を行う対象となる
      // デリゲートの取得
      MyDelegate p = (MyDelegate)((AsyncResult)ar).AsyncDelegate;
      string result = "";

      // 課題 (c) 例外の捕捉
      // コールバックの中でしか例外は捕捉できないので注意
      try
      {
        result = p.EndInvoke(ar);
      }
      catch (ArgumentException ex)
      {
        // Invokeメソッドで UIスレッドで MessageBox を表示
        // (そうする意味はなさそうだが・・・)
        this.Invoke((Action)(() => MessageBox.Show(ex.Message)));
        throw;
      }

      // 課題 (b) UI スレッドに戻り値を渡す
      // Invokeメソッドで UIスレッドの Label に文字列を設定
      this.Invoke((Action)(() => this.label1.Text += result));
    }

    private void button2_Click(object sender, EventArgs e)
    {
      this.label1.Text = "ManagedThreadId: " + 
                Thread.CurrentThread.ManagedThreadId + " / ";
            
      // デリゲートのインスタンスを作成
      MyDelegate p = new MyDelegate(TimeCosumingMethod);

      // デリゲートによるスレッド処理呼び出し
      p.BeginInvoke(this.textBox1.Text, this.MyCallBack, null);
    }


    // async/await/Task を使った非同期呼び出し
    private async void button3_Click(object sender, EventArgs e)
    {
      this.label1.Text = "ManagedThreadId: " + 
                Thread.CurrentThread.ManagedThreadId + " / ";

      try
      {
        this.label1.Text += await Task.Run(() => 
                      TimeCosumingMethod(this.textBox1.Text));
      }
      catch(ArgumentException ex)
      {
        MessageBox.Show(ex.Message);
        throw;
      }
    }        
  }
}

delegete を利用した非同期呼び出しの例外の処置については、上のコードのコールバックメソッド MyCallBack 内で完了できれば良いのですが、処理しないで集約的例外ハンドラで捕捉してからアプリケーションを終了させるというケースも多々あると思います。

それをどうするかについては以下の Main メソッドのコードを見てください。

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

namespace WindowsFormsClient
{
  static class Program
  {
    [STAThread]
    static void Main()
    {
      // 捕捉されなかった例外を処理する集約的例外ハンドラを実装

      // 同期呼出と async/await を使った非同期呼び出しの例外は
      // Application.ThreadException イベントで捕捉

      Application.ThreadException += 
            new ThreadExceptionEventHandler(ThreadException);

      // delegate を使った非同期呼び出しの例外の捕捉
      // Application.ThreadException では UI スレッドの例外しか
      // 捕捉できないので AppDomain.UnhandledException で捕捉
           
      AppDomain currentDomain = AppDomain.CurrentDomain;
      currentDomain.UnhandledException += UnhandledException;

      // だだし、上記では同期呼出と async/await を使った非同期呼
      // び出しの例外の処理が期待通りにならない。デバッグ実行す
      // ると捕捉できているように見えるが MessageBox が出ない。
      // 理由不明。同期、delegate、async/await 全部に対応するに
      // は両方のハンドラを生かしておく必要がある。

      // アプリケーション構成ファイルの設定を無視し、UI スレッド
      // の例外は常に ThreadException ハンドラに送る
      Application.SetUnhandledExceptionMode(
                        UnhandledExceptionMode.CatchException);

      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
      Application.Run(new Form2());
    }

    // Application.ThreadException のハンドラ
    private static void UnhandledException(object sender, 
                                 UnhandledExceptionEventArgs e)
    {
      Exception ex = (Exception)e.ExceptionObject;
      MessageBox.Show("AppDomain のハンドラ\n" + ex.Message);
      Application.Exit();
    }

    // AppDomain.UnhandledException のハンドラ
    static void ThreadException(object sender, 
                                    ThreadExceptionEventArgs e)
    {
      Exception ex = e.Exception;
      MessageBox.Show("Application のハンドラ\n" + ex.Message);

      // Environment.Exit を使わないとダイアログが出るとの記事が
      // あったが、このサンプルでは Application.Exit で問題なし  
      Application.Exit();
    }
  }
}

Microsoft のドキュメント Control.BeginInvoke Method に "Exceptions within the delegate method are considered untrapped and will be sent to the application's untrapped exception handler." と書いてあります。

デリゲートメソッド内で発生した例外を捕捉するには AppDomain.UnhandledException イベントのハンドラを利用します。Application.ThreadException イベントは UI スレッドで発生した例外しか捕捉できません。

なので、(1) 同期、(2) delegate、(3) async / await の全てのケースで集約的例外処理を行うには、(1) と (3) は Application.ThreadException イベントのハンドラで、(2) は AppDomain.UnhandledException イベントのハンドラで処理できるように両方のハンドラを設定するのが正解のようです。

Tags: , , , ,

.NET Framework

Dispose パターン

by WebSurfer 31. May 2019 18:30

Visual Studio Community 2015(以下、VS2015 と書きます)には Dispose パターンに準拠したコードの骨組みを自動生成する機能が追加されています。

Dispose パターンの実装

上の画像がそれで、Disposable とタイプすると電球マークが表示されるので、その横の▼印をクリックすると Disposeパターンを選択できるようになります。

ここで、[Dispose パターンを使ってインターフェイスを実装します]にマウスをポイントすると以下の画像(Dispose パターンの骨組みのコード)が表示され、クリックするとそのコードがクラスに挿入されます。

([... 明示的に ...]の方を選ぶと、Dispose メソッドが IDisposable.Dispose と明示的に実装されます。その場合、クラスインスタンスから Dispose には直接アクセスできませんので注意してください。明示的の使い道は調べてなくて不明です)

コードの実装

そういう機能が追加されていることを知ってました? 実は自分は最近まで知らなかったです。(汗) VS2010 にはその機能はありませんでしたので、VS2012 ~ VS2015 のどれかから追加されたようです。

Dispose パターンの実装については、Microsoft のドキュメント「Dispose メソッドの実装」などに書かれていますが、読んでもよく分かりませんでした。(笑)

(ちなみに、VS2015 で自動生成されるのは上の Microsoft のドキュメントの「Dispose パターンには 2 種類あります」以下に書いてある後者の Object.Finalize メソッドをオーバーライドする方です)

VS2015 で自動生成されたコードと、ネットの情報「確保したリソースを忘れずに解放するには?」や「C# のファイナライザ、Dispose() メソッド、IDisposable インターフェースについて」などを読んで、やっと Dispose パターンを理解できたような気がします。

Dispose パターンの実装が必要なのは、アンマネージドリソースを使用するクラスのみです。アンマネージドリソースの種類で一般的なのは、ファイル、ウィンドウ、ネットワーク接続、データベース接続などのオペレーティングシステムリソースをラップしたオブジェクトです。

.NET Framework のクラスライブラリの中にも、例えば FileStream のようにファイルを開くために Windows API を呼び出してファイルハンドルを保持するというように、アンマネージドリソースを利用するものがあります。

そういうクラスは、IDisposable インターフェイスを継承して Dispose パターンを使った実装がされていて、Dispose メソッドでアンマネージドリソースを開放できるようになっているはずです。

ソースを見たわけではないので、.NET Frameowrk のクラスライブラリの全部が全部そうなのかは分かりませんが、Dispose メソッドを実装しているクラスは内部でアンマネージドリソースを使っていると考えて、そのオブジェクトが使用されなくなった時点で Dispose メソッドを呼び出すことを基本とすべきと思います。

自分で作るカスタムクラスの場合、マネージドリソースしか保持しない場合は Dispose パターンの実装は不要です。必要なのはアンマネージドリソースを保持する場合のみですが、それには以下のケースがあると思います。

  1. Dispose パターンを実装した .NET のクラスのインスタンスを保持している。
  2. クラス内でアンマネージドリソースを取得し、それを保持している。  

VS2015 のウィザードが生成する Dispose パターンの骨組みのコードを利用して、上記 1 および上記 1 + 2 のケースでコードを実装してみます。

上記 1 のケース

Dispose パターンを実装した .NET のクラスの例として DataSet を使いました。

public class DisposableSample : IDisposable
{
    private DataSet myDataSet;

    public DisposableSample()
    {
        myDataSet = new DataSet();
        // ・・・中略・・・
    }

    private bool disposedValue = false; // 重複呼出の検出用

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // .NET のオブジェクトを解放
                myDataSet.Dispose();
            }

            disposedValue = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
    }
    
    // ・・・中略・・・
}

上記 1 + 2 のケース

アンマネージド(= GC では解放できない)リソースの例として SqlConnection で接続プールから取得する接続を使いました。接続プールはデフォルトで有効で、SqlConnection を Open すると接続プールから接続を取得してきます。

public class DisposableSample : IDisposable
{
    private DataSet myDataSet;
    private SqlConnection connection;

    public DisposableSample()
    {
        myDataSet = new DataSet();

        // アンマネージドリソースを取得
        string connString = ConfigurationManager.
                            ConnectionStrings["MyDB"].
                            ConnectionString;
        connection = new SqlConnection(connString);
        connection.Open();

        // ・・・中略・・・
    }

    private bool disposedValue = false; // 重複呼出の検出用

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // .NET のオブジェクトを解放
                myDataSet.Dispose();
            }

            // アンマネージドリソースを解放
            // SqlConnection の Dispose と Close は同じ
            connection.Dispose();

            disposedValue = true;
        }
    }

    // Dispose し忘れても、ガベージコレクタが働いたときに
    // ファイナライザが呼ばれるので、そこでアンマネージド
    // リソースを解放できるよう以下のコードを実装する。
    // C++ のデストラクタの構文だが、C# ではこれによりフ
    // ァイナライザをオーバーライドすることになるらしい
    ~DisposableSample() 
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);

        // 上の Dispose(bool disposing) メソッドでアンマネー
        // ジドリソースを解放した場合はファイナライザが実行
        // されないようにする
        GC.SuppressFinalize(this);
    }

    // ・・・中略・・・
}

上のコードのアンマネージドリソースの取得で、実際にこのような形で接続を保持するケースはないのかもしれませんが、他に具体例が思いつかなかったのでこうしてみました。

プログラマが Dispose() するのを忘れた場合でもガベージコレクタは働きます。ガベージコレクタではアンマネージドリソースは開放できませんが、その際ファイナライザが呼び出されます。なので、最悪でもファイナライザでアンマネージドリソースを開放できるようにしています。

Dispose() メソッドが呼ばれ、それから Dispose(bool disposing) メソッドが呼ばれてアンマネージドリソースを解放した場合は GC.SuppressFinalize(this) を呼び出します。 ファイナライザの実行はパフォーマンスに大きな影響を与えるそうですが、GC.SuppressFinalize メソッドを呼び出しておくと、ファイナライザの呼び出しは行われなくなります。

Tags: , , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  August 2019  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar