WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

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

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: , , ,

その他

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