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

絵文字のカラー表示

by WebSurfer 11. June 2019 12:18

Unicode 絵文字の表示が IE11 と Edge で異なるという話を書きます。異なるというのは絵文字用のカラーフォントの指定が無くても Edge はカラー表示されるが、IE11 では白黒になるということです。

Segoe UI Emoji 指定

ちなみに、Edge 以外のブラウザでも Chrome, Firefox, Opera はカラーフォント指定無しでカラー表示されます。

(確認したブラウザはこの記事を書いた時点の最新版で、Windows 10 Pro 64-bit バージョン 1903 上で動く、IE11 11.116.18362.0, Edge 44.18362.1.0, Chrome 75.0.3770.80, Firefox 67.0.1, Opera 60.0.3255.151 です)

Wondows OS での Unicode 絵文字のカラーフォントは Windows 8.1 からサポートされるようになり、そのフォント名は Segoe UI Emoji といいます。

ただし、Unicode 絵文字全部のフォントが実装されているわけではなく、サポートされているカラー文字は 743 文字のみだそうです。(全部で 2344 文字だそうです)

詳しくは @IT の記事「OpenTypeカラーフォント」の "Windowsの新しいカラーフォント" のセクションに書いてありますので読んでください。

Segoe UI Emoji がサポートしている絵文字については、Windows 8.1 以降なら IE11 で font-family に "Segoe UI Emoji" を指定すれば上の画像のようにカラーで表示されます。

上の画像は紹介した @IT の記事からダウンロードできるサンプルコードを IE11 に表示させたものです。興味がありましたらダウンロードして、いろいろなブラウザで試してみると良いと思います。

font-family に "Segoe UI Emoji" を指定しない場合は IE11 では以下の画像のように白黒になります。(このフォントは何か不明ですが Segoe UI Symbol ではないかと思われます)

IE11 でフォント指定なし

ところが、Edge の場合はフォントの指定は一切無しで以下の画像のように大多数の絵文字がカラー表示されます。Chrome, Firefox, Opera も同様です。

Edge でフォント指定なし

以前は Edge でもフォントに "Segoe UI Emoji" を指定しない場合は IE11 同様白黒表示だったのですが、2017 年の 6 月頃から自動検出してカラー化するようになったようです。

その経緯は Microsoft Developer Issue #7900499 の Make emoji look good without explicit "Segoe UI Emoji" assignment にありますので読んでください。

上の記事が書かれた 2016 年 6 月ごろは Edge もフォント指定 "Segoe UI Emoji" がないと白黒表示だったそうです。(当時は Chrome もだめで、Firefox だけは自動検出してカラー化していたそうですが)

それが、MICROSOFT EDGE TEAM からの回答に "Nolan L. Jan 15, 2017 This is fixed in Edge 15.15010.1002." とあるように、Edge では自動検出が可能になって、font-family に "Segoe UI Emoji" を指定しなくてもカラー化されるようになったようです。ちなみに、Chrome は 53 から対応したそうです。

IE11 は "Note that IE11 still has the black-and-white emoji, though" と書いてある通りで、カラー化するには依然として font-family に "Segoe UI Emoji" の設定が必要です。

なお、font-family に "Segoe UI Emoji" の設定をした場合と、その設定はせずブラウザ任せにした場合は若干カラー化の結果が異なります。上の一番上と一番下の画像を見比べてみてください。

Tags: , , ,

その他

About this blog

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

Calendar

<<  July 2019  >>
MoTuWeThFrSaSu
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar