前提・実現したいこと
async/awaitなど最新?C#の技術を使ってみたいのですが、イベントを待って戻り値を返すI/Fのある部分をasync/awaitで表現できませんでした。
いろいろ考えた結果、自分では下のソースのような、Monitorを使った同期機構でしか実現できなかったのですが、もっといい方法があれば教えてください。
下のソースではMyReader.Read()が該当箇所です。Readerインターフェースを実装しており、ただただ、同期I/Oで読んだ結果を返すI/Fです。
このI/Fを実装し、外からイベントで渡されてくるデータを返したいのです。同期I/Oなのでブロックする必要があり、要求されたデータが来るまで待つ必要があります。ここをなんとかawaitで実現したいのですが、いい方法を思いつきませんでした。
なお、EventHolder/Readerの形はいじれないので、ご注意ください。
また主旨に関係ないご指摘も、気づいたことは何でも言って頂けると嬉しいです。
該当のソースコード
C#
1using System; 2using System.Collections.Generic; 3using System.Threading; 4using System.Threading.Tasks; 5 6namespace EventAsyncAwait 7{ 8 interface Reader 9 { 10 int Read(); 11 } 12 13 class MyEventArgs 14 { 15 public int val { get; set; } 16 } 17 18 class MyReader : Reader 19 { 20 List<int> buffer = new List<int>(); 21 22 public int Read() 23 { 24 int result = 0; 25 lock (buffer) 26 { 27 while (buffer.Count == 0) 28 Monitor.Wait(buffer); 29 result = buffer[0]; 30 buffer.RemoveAt(0); 31 } 32 return result; 33 } 34 35 public MyReader(EventHolder parent) 36 { 37 parent.Event += (object sender, MyEventArgs e) => 38 { 39 lock (buffer) 40 { 41 buffer.Add(e.val); 42 Monitor.PulseAll(buffer); 43 } 44 }; 45 } 46 } 47 48 class EventHolder 49 { 50 public event EventHandler<MyEventArgs> Event; 51 52 public void Fire(int v) 53 { 54 Event(this, new MyEventArgs() { val = v }); 55 } 56 } 57 58 class Program 59 { 60 static EventHolder holder = new EventHolder(); 61 62 static void Main(string[] args) 63 { 64 MyReader reader = new MyReader(holder); 65 Task.Run(async () => 66 { 67 await Task.Delay(3000); 68 holder.Fire(5); 69 }); 70 Console.WriteLine(reader.Read()); 71 } 72 } 73}
※質問しておいて何ですが、本日反応できるのは夕方くらいからになります。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/02/10 04:25
2019/02/10 05:03 編集
2019/02/10 05:03
2019/02/10 05:06
2019/02/10 05:45
2019/02/10 05:55
回答4件
0
他にも方法はあるでしょうが、IObservable<T> を使ってください。
Reactive Extensions を使うと簡単です。これを使うには NuGet で System.Reactive をインストールする必要があります。
追記
例えば Reactive Extensions を使ってボタンのクリックイベントを受け取るには次のようにします。
C#
1using System; 2using System.Reactive.Linq; 3using System.Windows.Forms; 4 5namespace WindowsFormsApp1 6{ 7 public partial class Form1 : Form 8 { 9 public Form1() 10 { 11 InitializeComponent(); 12 Observable 13 .FromEventPattern<EventArgs>(button1, nameof(Button.Click)) 14 .Subscribe(_ => MessageBox.Show("クリックされた!")); 15 } 16 } 17}
追記
ボタンを二つ貼り付け、button2 のイベントハンドラを次のように書き換えてください。
そして button2 を押した後、button1 を押すとメッセージが表示されます。
C#
1private async void button2_Click(object sender, EventArgs e) 2{ 3 var task = Observable 4 .FromEventPattern<EventArgs>(button1, nameof(Button.Click)) 5 .Take(1) 6 .ToTask(); 7 await task; 8 MessageBox.Show("button1 が押されました"); 9}
投稿2019/02/09 23:51
編集2019/02/10 06:10総合スコア28660
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/02/10 04:24
2019/02/10 04:27
2019/02/10 04:32
2019/02/10 04:35
2019/02/10 04:35
2019/02/10 04:41
2019/02/10 04:41
2019/02/10 04:42
2019/02/10 04:42
2019/02/10 04:43
2019/02/10 04:46
2019/02/10 04:51
2019/02/10 04:53
2019/02/10 05:01
2019/02/10 05:06 編集
2019/02/10 05:05
2019/02/10 05:06
2019/02/10 05:07
2019/02/10 05:08
2019/02/10 05:10
2019/02/10 05:11
2019/02/10 05:13
2019/02/10 05:17
2019/02/10 05:18 編集
2019/02/10 05:24
2019/02/10 05:27
2019/02/10 05:28
2019/02/10 05:29
2019/02/10 05:29
2019/02/10 05:33
2019/02/10 05:36
2019/02/10 05:37
2019/02/10 05:39
2019/02/10 05:47
2019/02/10 05:52
2019/02/10 05:54
2019/02/10 05:54
2019/02/10 05:56
2019/02/10 05:58
2019/02/10 05:59
2019/02/10 06:01
2019/02/10 06:02
2019/02/10 06:02
2019/02/10 06:03
2019/02/10 06:03
2019/02/10 06:05
2019/02/10 06:07
2019/02/10 06:08
2019/02/10 06:10
2019/02/10 06:10
2019/02/10 06:11
2019/02/10 06:13
2019/02/10 06:19
2019/02/10 06:22
2019/02/10 06:30
2019/02/10 06:31
2019/02/10 06:33
2019/02/10 06:33
2019/02/10 06:35
2019/02/10 06:39
2019/02/10 06:40
2019/02/10 06:41
2019/02/10 06:41
2019/02/10 06:44
2019/02/10 06:47
2019/02/10 06:48
2019/02/10 06:50
2019/02/10 06:51
2019/02/10 06:51
2019/02/10 06:52
2019/02/10 06:52
2019/02/10 06:53
2019/02/10 06:55
2019/02/10 07:08
2019/02/10 07:12
2019/02/10 07:17
2019/02/10 07:28
2019/02/10 07:30
2019/02/10 07:34
2019/02/10 07:42
2019/02/10 07:44
2019/02/10 07:47
2019/02/10 07:51
2019/02/10 07:53
2019/02/10 07:58
2019/02/10 08:00
2019/02/10 08:17
2019/02/10 08:19
2019/02/10 08:22
2019/02/10 08:38
2019/02/10 08:41
2019/02/10 08:49
2019/02/10 08:51 編集
2019/02/10 09:03
2019/02/10 09:11
2019/02/10 09:17
2019/02/10 09:22
2019/02/10 09:33
2019/02/10 09:47
2019/02/10 10:02
2019/02/10 10:06
2019/02/10 10:08
2019/02/10 10:11
2019/02/10 10:14
2019/02/10 10:14
2019/02/10 10:16
2019/02/10 10:18
2019/02/10 10:18
2019/02/10 10:18
2019/02/10 10:19
2019/02/10 10:23
2019/02/10 10:26
2019/02/10 10:28
2019/02/10 10:28
2019/02/10 10:30
2019/02/10 10:51
2019/02/10 12:31
2019/02/10 21:22
2019/02/10 21:35
2019/02/11 04:57
2019/02/11 05:13
2019/02/11 05:21
2019/02/11 05:30
2019/02/11 05:36
2019/02/11 05:38
2019/02/11 05:41
2019/02/11 05:55
2019/02/11 05:58
2019/02/11 06:43
2019/02/11 06:50
2019/02/11 06:55
2019/02/11 06:56
2019/02/11 07:05
2019/02/11 07:13
2019/02/11 07:14
2019/02/11 07:17
2019/02/11 07:19
2019/02/11 07:22
2019/02/11 07:25 編集
2019/02/11 07:27
2019/02/11 07:32
2019/02/11 07:36
2019/02/11 07:40
2019/02/11 07:42
2019/02/11 07:44
2019/02/11 07:45
0
こんにちは。
その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 08:36
総合スコア23272
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/02/10 09:21
2019/02/10 11:26
2019/02/10 11:27
2019/02/10 13:31
0
自己解決
さらにSystem.Reactive拡張を読んでみたのですが、肝心の待ち合わせの部分のソースを見つけられなくてできませんでした。恐らく、async/await/Task+言語仕様だけで待ち合わせを実現したのではなく、何らかの同期機構をセットで使用したのではないか?と推測しています。
そこで、今回はMonitorを使用して待ち合わせた実装を、もう一つの回答で書いたものと同様な、汎用なイベントハンドラをラップする実装に組み込んでみました。ようはSystem.Reactive拡張を真似て、Monitorを使う待ち合わせを使ったasync/awaitの簡易版汎用実装をしてみたということです。なるべくメモリ効率を良くして、コンテキストスイッチを抑える実装にはしてみたつもりです。
なお、前回同様エラーハンドリングはないので、そのまま使っちゃダメですよ。
これを数日様子見して解決の判断をするつもりです。
C#
1using System; 2using System.Collections.Generic; 3using System.Threading; 4using System.Threading.Tasks; 5 6namespace EventAsyncAwait 7{ 8 interface Reader 9 { 10 int Read(); 11 } 12 13 class MyEventArgs 14 { 15 public int val { get; set; } 16 } 17 18 class EventAwaitableFunction<A> 19 { 20 public class EventContext 21 { 22 public object Sender { get; set; } 23 public A Args { get; set; } 24 } 25 26 public delegate void AddHandler(EventHandler<A> h); 27 private List<EventContext> buffer = new List<EventContext>(); 28 29 public EventAwaitableFunction(AddHandler add) 30 { 31 add((s, a) => 32 { 33 var elm = new EventContext(); 34 elm.Sender = s; 35 elm.Args = a; 36 lock (buffer) 37 { 38 buffer.Add(elm); 39 Monitor.PulseAll(buffer); 40 } 41 }); 42 } 43 44 public async Task<EventContext> GetFirstEvent() 45 { 46 EventContext result = null; 47 lock (buffer) 48 { 49 if (buffer.Count > 0) 50 { 51 result = buffer[0]; 52 buffer.RemoveAt(0); 53 } 54 } 55 if (result == null) 56 { 57 await Task.Run(() => 58 { 59 lock (buffer) 60 { 61 while (buffer.Count == 0) 62 { 63 Monitor.Wait(buffer); 64 } 65 result = buffer[0]; 66 buffer.RemoveAt(0); 67 } 68 }); 69 } 70 return result; 71 } 72 } 73 74 class MyReader : Reader 75 { 76 EventAwaitableFunction<MyEventArgs> func; 77 List<int> buffer = new List<int>(); 78 79 public int Read() 80 { 81 return ReadAsync().Result.Args.val; 82 } 83 84 public async Task<EventAwaitableFunction<MyEventArgs>.EventContext> ReadAsync() 85 { 86 return await func.GetFirstEvent(); 87 } 88 89 public MyReader(EventHolder parent) 90 { 91 func = new EventAwaitableFunction<MyEventArgs>(h => parent.Event += h); 92 } 93 } 94 95 class EventHolder 96 { 97 public event EventHandler<MyEventArgs> Event; 98 99 public void Fire(int v) 100 { 101 Event(this, new MyEventArgs() { val = v }); 102 } 103 } 104 105 class Program 106 { 107 static EventHolder holder = new EventHolder(); 108 109 static void Main(string[] args) 110 { 111 MyReader reader = new MyReader(holder); 112 Task.Run(async () => 113 { 114 await Task.Delay(3000); 115 holder.Fire(5); 116 }); 117 Console.WriteLine(reader.Read()); 118 } 119 } 120}
追記)
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/11 21:19
編集2019/02/13 04:44総合スコア343
0
とりあえずZuishinさんのタスクをキューに入れたバージョンで、相変わらずロックなどの同期は必要なもののMonitorを使用しない形には出来るので、書いておきます。コメントで(1)と(2)を出していましたが、その間といったところのものを作ってみました。
エラー処理とメモリが心配な作りになっていますが、今の所他に問題は見つかっていません。一応ちゃんと書いておくと、控えめに言っても多分あまりRectiveな実装ではなく、また元の実装と比較して効率がいいわけでもありませんが、aync/awaitで非同期のReadメソッドが簡単に作成できます。
数日待って問題の指摘がなければ解決とさせて頂きます。
C#
1using System; 2using System.Collections.Generic; 3using System.Reactive; 4using System.Reactive.Linq; 5using System.Reactive.Threading.Tasks; 6using System.Threading.Tasks; 7 8namespace EventAsyncAwait 9{ 10 interface Reader 11 { 12 int Read(); 13 } 14 15 class MyEventArgs 16 { 17 public int val { get; set; } 18 } 19 20 class MyReader : Reader 21 { 22 public int Read() 23 { 24 TaskContext context; 25 lock (_queue) 26 { 27 context = _queue.Dequeue(); 28 } 29 context.Task.Wait(); 30 context.Observer.Dispose(); 31 return context.Args.EventArgs.val; 32 } 33 34 class TaskContext 35 { 36 public EventPattern<MyEventArgs> Args { set; get; } 37 public Task Task { set; get; } 38 public IDisposable Observer { set; get; } 39 } 40 41 Queue<TaskContext> _queue = new Queue<TaskContext>(); 42 43 private void CreateTask(IObservable<EventPattern<MyEventArgs>> subject) 44 { 45 var observable = subject.Take(1); 46 var elm = new TaskContext(); 47 elm.Observer = observable.Subscribe((args) => { 48 elm.Args = args; 49 CreateTask(subject); 50 }); 51 elm.Task = observable.ToTask(); 52 lock(_queue) 53 { 54 _queue.Enqueue(elm); 55 } 56 } 57 58 public MyReader(EventHolder parent) 59 { 60 CreateTask( 61 Observable.FromEventPattern<MyEventArgs>( 62 h => parent.Event += h, 63 h => parent.Event -= h)); 64 } 65 } 66 67 class EventHolder 68 { 69 public event EventHandler<MyEventArgs> Event; 70 71 public void Fire(int v) 72 { 73 Event(this, new MyEventArgs() { val = v }); 74 } 75 } 76 77 class Program 78 { 79 static EventHolder holder = new EventHolder(); 80 81 static void Main(string[] args) 82 { 83 MyReader reader = new MyReader(holder); 84 Task.Run(async () => 85 { 86 await Task.Delay(3000); 87 holder.Fire(5); 88 }); 89 Console.WriteLine(reader.Read()); 90 } 91 } 92} 93
投稿2019/02/11 10:03
総合スコア343
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。