質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.02%

async/awaitでイベント待ちってできるのでしょうか?

解決済

回答 4

投稿

  • 評価
  • クリップ 0
  • VIEW 2,873

wwbQzhMkhhgEmhU

score 229

前提・実現したいこと

async/awaitなど最新?C#の技術を使ってみたいのですが、イベントを待って戻り値を返すI/Fのある部分をasync/awaitで表現できませんでした。
いろいろ考えた結果、自分では下のソースのような、Monitorを使った同期機構でしか実現できなかったのですが、もっといい方法があれば教えてください。

下のソースではMyReader.Read()が該当箇所です。Readerインターフェースを実装しており、ただただ、同期I/Oで読んだ結果を返すI/Fです。
このI/Fを実装し、外からイベントで渡されてくるデータを返したいのです。同期I/Oなのでブロックする必要があり、要求されたデータが来るまで待つ必要があります。ここをなんとかawaitで実現したいのですが、いい方法を思いつきませんでした。

なお、EventHolder/Readerの形はいじれないので、ご注意ください。
また主旨に関係ないご指摘も、気づいたことは何でも言って頂けると嬉しいです。

該当のソースコード

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace EventAsyncAwait
{
    interface Reader
    {
        int Read();
    }

    class MyEventArgs
    {
        public int val { get; set; }
    }

    class MyReader : Reader
    {
        List<int> buffer = new List<int>();

        public int Read()
        {
            int result = 0;
            lock (buffer)
            {
                while (buffer.Count == 0)
                    Monitor.Wait(buffer);
                result = buffer[0];
                buffer.RemoveAt(0);
            }
            return result;
        }

        public MyReader(EventHolder parent)
        {
            parent.Event += (object sender, MyEventArgs e) =>
            {
                lock (buffer)
                {
                    buffer.Add(e.val);
                    Monitor.PulseAll(buffer);
                }
            };
        }
    }

    class EventHolder
    {
        public event EventHandler<MyEventArgs> Event;

        public void Fire(int v)
        {
            Event(this, new MyEventArgs() { val = v });
        }
    }

    class Program
    {
        static EventHolder holder = new EventHolder();

        static void Main(string[] args)
        {
            MyReader reader = new MyReader(holder);
            Task.Run(async () =>
            {
                await Task.Delay(3000);
                holder.Fire(5);
            });
            Console.WriteLine(reader.Read());
        }
    }
}

※質問しておいて何ですが、本日反応できるのは夕方くらいからになります。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • wwbQzhMkhhgEmhU

    2019/02/10 14:06

    Task.Run()の戻り値からではなく、Reader.Read()の実装をしたいんですよ。。。

    キャンセル

  • tamoto

    2019/02/10 14:45

    要件の確認をさせてください。
    このイベントは「1回きり」発動することを想定したものですか?それとも、複数回発動する、「ストリーム」を想定していますか?

    キャンセル

  • wwbQzhMkhhgEmhU

    2019/02/10 14:55

    最終的には複数回だけど、1回きりの方法すら思いついていないので、まずは1回きりでも大丈夫、という意味で、ここでは1回しか待っていません。

    キャンセル

回答 4

+6

他にも方法はあるでしょうが、IObservable<T> を使ってください。

Reactive Extensions を使うと簡単です。これを使うには NuGet で System.Reactive をインストールする必要があります。

追記

例えば Reactive Extensions を使ってボタンのクリックイベントを受け取るには次のようにします。

using System;
using System.Reactive.Linq;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            Observable
                .FromEventPattern<EventArgs>(button1, nameof(Button.Click))
                .Subscribe(_ => MessageBox.Show("クリックされた!"));
        }
    }
}

追記

ボタンを二つ貼り付け、button2 のイベントハンドラを次のように書き換えてください。
そして button2 を押した後、button1 を押すとメッセージが表示されます。

private async void button2_Click(object sender, EventArgs e)
{
    var task = Observable
        .FromEventPattern<EventArgs>(button1, nameof(Button.Click))
        .Take(1)
        .ToTask();
    await task;
    MessageBox.Show("button1 が押されました");
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/02/11 16:42

    まあそれなら用はないんでつきまとわないでください。

    キャンセル

  • 2019/02/11 16:44

    期待に添えなくて悪いね
    じゃあこれで終わりってことで

    キャンセル

  • 2019/02/11 16:45

    気持ち悪いんで、はなしかけないで。

    キャンセル

+4

こんにちは。

そのMain関数を別関数にしてasync関数とし、Task.Runの中でReadを呼び出し、そのTask.Runをawaitすれば良いはずです。int ret=await Task<int>.Run(()=>{return reader.Read();});のようなイメージです。

async関数を呼び出すと、awaitにてTaskを起動したら直ぐにasync関数の呼び出し元に戻ってきます。
Task.Runはサブ・スレッドで実行を開始しReadが終わればそのままawaitの次から実行を継続します。
aync関数から戻った地点と、awaitの次の地点がそれぞれ異なるスレッドで実行されています。これらを同期させる仕組みも何か必要と思います。

以上の動作はコンソール・アプリの場合です。GUIアプリの場合も見た目には良く似た動作をしますが、実際の処理内容はかなり異なります。最大の差はawaitの次をメッセージ・ループを回しているスレッド(通常はメイン・スレッド)で実行することです。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/02/10 18:21

    う〜ん、それではReadでどう待つと言っているのでしょうか?
    根本的なところが違っているような…

    キャンセル

  • 2019/02/10 20:26

    提示されているRead()関数は同期的にイベント発生を待っていると思います。そのままで良いですよ。
    同期関数をTask.Runで呼び出して非同期化するという、async/awaitの定番的な使い方の1つです。
    https://chronoir.net/cs-async-await-taskrun/

    キャンセル

  • 2019/02/10 20:27

    ああ、ごめんなさい。Zuishinさんとのやりとりを見ていませんでした。
    私はこれにて降りさせて頂きます。

    キャンセル

  • 2019/02/10 22:31

    一応書いておくと、そのままで何か不都合のある動作をしているわけではありません。
    MyReaderクラスのReadメソッドの中身をasync/await/Taskで実装できないかを検討したく、呼び出し方法部分は検討対象ではない説明用の部品なので、申し訳ありませんが、このままにします。

    キャンセル

check解決した方法

-8

さらにSystem.Reactive拡張を読んでみたのですが、肝心の待ち合わせの部分のソースを見つけられなくてできませんでした。恐らく、async/await/Task+言語仕様だけで待ち合わせを実現したのではなく、何らかの同期機構をセットで使用したのではないか?と推測しています。

そこで、今回はMonitorを使用して待ち合わせた実装を、もう一つの回答で書いたものと同様な、汎用なイベントハンドラをラップする実装に組み込んでみました。ようはSystem.Reactive拡張を真似て、Monitorを使う待ち合わせを使ったasync/awaitの簡易版汎用実装をしてみたということです。なるべくメモリ効率を良くして、コンテキストスイッチを抑える実装にはしてみたつもりです。

なお、前回同様エラーハンドリングはないので、そのまま使っちゃダメですよ。
これを数日様子見して解決の判断をするつもりです。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace EventAsyncAwait
{
    interface Reader
    {
        int Read();
    }

    class MyEventArgs
    {
        public int val { get; set; }
    }

    class EventAwaitableFunction<A>
    {
        public class EventContext
        {
            public object Sender { get; set; }
            public A Args { get; set; }
        }

        public delegate void AddHandler(EventHandler<A> h);
        private List<EventContext> buffer = new List<EventContext>();

        public EventAwaitableFunction(AddHandler add)
        {
            add((s, a) =>
            {
                var elm = new EventContext();
                elm.Sender = s;
                elm.Args = a;
                lock (buffer)
                {
                    buffer.Add(elm);
                    Monitor.PulseAll(buffer);
                }
            });
        }

        public async Task<EventContext> GetFirstEvent()
        {
            EventContext result = null;
            lock (buffer)
            {
                if (buffer.Count > 0)
                {
                    result = buffer[0];
                    buffer.RemoveAt(0);
                }
            }
            if (result == null)
            {
                await Task.Run(() =>
                {
                    lock (buffer)
                    {
                        while (buffer.Count == 0)
                        {
                            Monitor.Wait(buffer);
                        }
                        result = buffer[0];
                        buffer.RemoveAt(0);
                    }
                });
            }
            return result;
        }
    }

    class MyReader : Reader
    {
        EventAwaitableFunction<MyEventArgs> func;
        List<int> buffer = new List<int>();

        public int Read()
        {
            return ReadAsync().Result.Args.val;
        }

        public async Task<EventAwaitableFunction<MyEventArgs>.EventContext> ReadAsync()
        {
            return await func.GetFirstEvent();
        }

        public MyReader(EventHolder parent)
        {
            func = new EventAwaitableFunction<MyEventArgs>(h => parent.Event += h);
        }
    }

    class EventHolder
    {
        public event EventHandler<MyEventArgs> Event;

        public void Fire(int v)
        {
            Event(this, new MyEventArgs() { val = v });
        }
    }

    class Program
    {
        static EventHolder holder = new EventHolder();

        static void Main(string[] args)
        {
            MyReader reader = new MyReader(holder);
            Task.Run(async () =>
            {
                await Task.Delay(3000);
                holder.Fire(5);
            });
            Console.WriteLine(reader.Read());
        }
    }
}


追記)
SynchronizationContextによっては、実行スレッドによりawait後のスレッドに制約がかかるため、Task.Waitと競合してデッドロックすることがあります。
コンソールアプリ以外で使用するケースでは、そのようなケースが起こりうるので、ご注意ください。
例えば、これ100msくらいで終わるからUIスレッドで実行しよう、とかすると、100ms経過しても返ってこず、固まるということです。確実にUIスレッド以外から実施してください。ASP.NETでの回避方法は未確認です。
ConfigureAwaitによる回避は↓の記事にもある通りやめるべきだと思います。

デッドロックについては、↓が詳しいです。
http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

SynchronizationContextについては、↓参照です。
https://msdn.microsoft.com/magazine/gg598924.aspx

最後に、納得の行かない人もいるかもしれないので、書いておきますが、待ち合わせという性格上、このI/Fをawaitで実装する限り、Task.Waitは仕方のない選択です。有効なケースであれば問題はないので、個人的に分かって使う分には益がある、と考えています。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/02/13 14:29

    ほかの人のために書いておきます。
    仕方のない選択ではありません。
    無知故の間違った結論です。

    キャンセル

-8

とりあえずZuishinさんのタスクをキューに入れたバージョンで、相変わらずロックなどの同期は必要なもののMonitorを使用しない形には出来るので、書いておきます。コメントで(1)と(2)を出していましたが、その間といったところのものを作ってみました。
エラー処理とメモリが心配な作りになっていますが、今の所他に問題は見つかっていません。一応ちゃんと書いておくと、控えめに言っても多分あまりRectiveな実装ではなく、また元の実装と比較して効率がいいわけでもありませんが、aync/awaitで非同期のReadメソッドが簡単に作成できます。

数日待って問題の指摘がなければ解決とさせて頂きます。

using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading.Tasks;

namespace EventAsyncAwait
{
    interface Reader
    {
        int Read();
    }

    class MyEventArgs
    {
        public int val { get; set; }
    }

    class MyReader : Reader
    {
        public int Read()
        {
            TaskContext context;
            lock (_queue)
            {
                context = _queue.Dequeue();
            }
            context.Task.Wait();
            context.Observer.Dispose();
            return context.Args.EventArgs.val;
        }

        class TaskContext
        {
            public EventPattern<MyEventArgs> Args { set; get; }
            public Task Task { set; get; }
            public IDisposable Observer { set; get; }
        }

        Queue<TaskContext> _queue = new Queue<TaskContext>();

        private void CreateTask(IObservable<EventPattern<MyEventArgs>> subject)
        {
            var observable = subject.Take(1);
            var elm = new TaskContext();
            elm.Observer = observable.Subscribe((args) => {
                elm.Args = args;
                CreateTask(subject);
            });
            elm.Task = observable.ToTask();
            lock(_queue)
            {
                _queue.Enqueue(elm);
            }
        }

        public MyReader(EventHolder parent)
        {
            CreateTask(
                Observable.FromEventPattern<MyEventArgs>(
                    h => parent.Event += h,
                    h => parent.Event -= h));
        }
    }

    class EventHolder
    {
        public event EventHandler<MyEventArgs> Event;

        public void Fire(int v)
        {
            Event(this, new MyEventArgs() { val = v });
        }
    }

    class Program
    {
        static EventHolder holder = new EventHolder();

        static void Main(string[] args)
        {
            MyReader reader = new MyReader(holder);
            Task.Run(async () =>
            {
                await Task.Delay(3000);
                holder.Fire(5);
            });
            Console.WriteLine(reader.Read());
        }
    }
}

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.02%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる