C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

サブルーチンから呼び出すawait/asyncの動きについて

[トピック内 10 記事 (1 - 10 表示)]  << 0 >>

■95217 / inTopicNo.1)  サブルーチンから呼び出すawait/asyncの動きについて
  
□投稿者/ ほにゃ太郎 (1回)-(2020/07/02(Thu) 12:13:54)

分類:[C#] 

VS2017、C#、Win10 64bit

async/awaitの動作がいまいち理解できず、勉強中です。
元々C開発者で、Cの従来のスレッド動作は理解しているつもりです。
C#は経験薄いです。

下記のコードを実行すると、label1に「完了」が表示されてから、iのカウントアップが表示されます。
てっきり@でスレッドが待ち(※1)になり、Task.Delay()が終わるまで次の行以降は実行されないと思いました。
しかしAは確かに実行されずでしたが、Bが実行されています。
サブルーチン内でasync/awaitを使う場合は、それを呼び出している関数全てにさかのぼってasync/awaitを使う必要があるのでしょうか。
AとBとで先にBが呼び出されるため、順番が変わったような感じでいまいち納得できていません。

※1:「待ち」という単語がおかしいかもしれません。ブロックされたような状態になるが、スレッド自体はアイドルになり、新規のスレッド処理を実行できる状態と認識しています。

public partial class Form1 : Form
{
 public Form1()
 {
  InitializeComponent();
 }

 private async void DoWorkAsync()
 {
  for (int i = 0; i < 10; i++)
  {
   await Task.Delay(1000); // @
   label1.Text = i.ToString(); // A
  }
 }

 private void button1_Click(object sender, EventArgs e)
 {
  label1.Text = "実行中・・・";

  DoWorkAsync();

  label1.Text = "完了"; // B
 }
}
引用返信 編集キー/
■95218 / inTopicNo.2)  Re[1]: サブルーチンから呼び出すawait/asyncの動きについて
□投稿者/ Hongliang (1054回)-(2020/07/02(Thu) 12:45:22)
>  private  void button1_Click(object sender, EventArgs e)
>  {
>   label1.Text = "実行中・・・";
> 
>   DoWorkAsync();
> 
>   label1.Text = "完了";                 // B
>  }
> }
DoWorkAsync()の完了を待っていないのですから、直ちに
label1.Text = "完了";
が実行されるのは当然ですよね。

基本的に、async voidになるのはイベントハンドラだけです。
つまり、button1_Clickメソッド以外は、必ずasync Task(返値を返す場合はasync Task<T>)にします。
DoWorkAsyncはasync Task DoWorkAsyncです。

そうすれば、button1_Clickは
await DoWorkAsync();
と記述でき、DoWorkAsyncの完了を待って
label1.Text = "完了";
を実行できるようになります。

引用返信 編集キー/
■95221 / inTopicNo.3)  Re[2]: サブルーチンから呼び出すawait/asyncの動きについて
□投稿者/ ほにゃ太郎 (2回)-(2020/07/02(Thu) 13:31:34)
Hongliangさん、

回答ありがとうございます。
もう少しだけ確認させてください。

No95218 (Hongliang さん) に返信
> DoWorkAsync()の完了を待っていないのですから、直ちに
> label1.Text = "完了";
> が実行されるのは当然ですよね。
DoWorkAsync()内の「await Task.Delay(1000);」ではbutton1_Click()は待っていることにならないのでしょうか?


> 基本的に、async voidになるのはイベントハンドラだけです。
> つまり、button1_Clickメソッド以外は、必ずasync Task(返値を返す場合はasync Task<T>)にします。
> DoWorkAsyncはasync Task DoWorkAsyncです。
つまりは末端の関数でasync/awaitをやる場合は、呼び出し関数全てでasync/awaitが必要になるのでしょうか?
全てのパターンではないかも知れませんが、通常は。
引用返信 編集キー/
■95224 / inTopicNo.4)  サブルーチンから呼び出すawait/asyncの動きについて
□投稿者/ ぶなっぷ (228回)-(2020/07/02(Thu) 13:57:45)
2020/07/02(Thu) 14:09:50 編集(投稿者)
2020/07/02(Thu) 14:07:28 編集(投稿者)

<pre><pre>私も、以前、ほにゃ太郎さんと同じように思って悩みました。
C言語から移ってきた人が陥りやすい話なのかもしれません。

問題になるのは、awaitの行に到達すると、いったんreturnするということです。
そのため、"完了"が表示されます。

awaitするメソッドが終わると、その次の行から処理が再開しますが、
その際は、もうメインスレッド(UIスレッド)に戻ってきているということが重要です。
https://qiita.com/rawr/items/5d49960a4e4d3823722f

> 基本的に、async voidになるのはイベントハンドラだけです。
の話も交えると、
ほにゃ太郎さんのやりたいことを実現するには、大きく以下の2つの方法が。

1) DoWorkAsync()メソッド内で完結
 private void button1_Click(object sender, EventArgs e)
 {
  label1.Text = "実行中・・・";
  var task = DoWorkAsync();
 }

 private async Task DoWorkAsync()
 {
  for (int i = 0; i < 10; i++)
  {
   await Task.Delay(1000); // @
   label1.Text = i.ToString(); // A
  }
  label1.Text = "完了"; // B
}

2) イベントハンドラで待機
 private async void button1_Click(object sender, EventArgs e)
 {
  label1.Text = "実行中・・・";
  await DoWorkAsync();
  label1.Text = "完了"; // B
 }

 private async Task DoWorkAsync()
 {
  for (int i = 0; i < 10; i++)
  {
   await Task.Delay(1000); // @
   label1.Text = i.ToString(); // A
  }
 }</pre></pre>
引用返信 編集キー/
■95225 / inTopicNo.5)  Re[3]: サブルーチンから呼び出すawait/asyncの動きについて
□投稿者/ Hongliang (1055回)-(2020/07/02(Thu) 14:00:18)
> DoWorkAsync()内の「await Task.Delay(1000);」ではbutton1_Click()は待っていることにならないのでしょうか?

ループの場合は面倒なので単純に
await Task.Delay(1000);
label1.Text = "OK";
というコードで説明しますが、これは大雑把には以下のようなコードになります。

TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task task1 = Task.Delay(1000);
Task task2 = task1.ContinueWith(_ => label1.Text = "OK", scheduler);
return task2;

御覧のように、awaitは実際には「待っている」わけではなくて、単に最初の仕事が終わった後の次の仕事を
Taskに知らせてるだけなのです。
知らせるだけなので処理はすぐ返ります。
このDoHeavyWorkの処理の後に何かしたいなら、task2に対してContinueWithする(awaitする)必要があります。
Taskを返せばそれも可能ですが、async voidであれば最後のtask2がreturnされず、ContinueWithできません。

つまり、DoHeavyWork内にawaitがあろうが、そのDoHeavyWorkは同期化されるわけではありません。
awaitはあくまで非同期処理を手続き的に記述するためのものなのです。

>>基本的に、async voidになるのはイベントハンドラだけです。
>>つまり、button1_Clickメソッド以外は、必ずasync Task(返値を返す場合はasync Task<T>)にします。
>>DoWorkAsyncはasync Task DoWorkAsyncです。
> つまりは末端の関数でasync/awaitをやる場合は、呼び出し関数全てでasync/awaitが必要になるのでしょうか?
> 全てのパターンではないかも知れませんが、通常は。

awaitする必要がないメソッドについてはasyncは要りませんが、返値をTaskにするのは必要です。
awaitする必要がないというのは、例えば以下のようにそのままTaskをreturnできるケースなど。
async Task DoHeavyAsync() {
    await Task.Delay(1000);
}
Task DoHeavyAsync() {
    return Task.Delay(1000);
}

引用返信 編集キー/
■95231 / inTopicNo.6)  Re[4]: サブルーチンから呼び出すawait/asyncの動きについて
□投稿者/ ほにゃ太郎 (3回)-(2020/07/02(Thu) 15:13:27)
ぶなっぷさん、Hongliangさん、

返信いただきありがとうございます。

> 問題になるのは、awaitの行に到達すると、いったんreturnするということです。
これが目からウロコでした。始めの投稿の※1にあるように、
私はブロックされたような状態になるものと思っていましたが、
いったんreturnするのですね。
よって呼び出し元の関数は処理を継続する。と。なるほど・・・。


> 1) DoWorkAsync()メソッド内で完結
>  private void button1_Click(object sender, EventArgs e)
>  {
>   label1.Text = "実行中・・・";
>   var task = DoWorkAsync();
>   label1.Text = "完了"; // B
>  }
>
>  private async Task DoWorkAsync()
>  {
>   for (int i = 0; i < 10; i++)
>   {
>    await Task.Delay(1000); // @
>    label1.Text = i.ToString(); // A
>   }
>  }
これでもできるのですね。taskで戻りを取っており、task.Resultで戻り値を参照するとブロックされました。(<int>に変えて。)
1)よりも2)を使う場合の方が一般的と思いましたがいかがでしょうか。
awaitで一目瞭然だし、result参照するとブロックされるし。

> 2) イベントハンドラで待機
>  private async void button1_Click(object sender, EventArgs e)
>  {
>   label1.Text = "実行中・・・";
>   await DoWorkAsync();
>   label1.Text = "完了"; // B
>  }
>
>  private async Task DoWorkAsync()
>  {
>   for (int i = 0; i < 10; i++)
>   {
>    await Task.Delay(1000); // @
>    label1.Text = i.ToString(); // A
>   }
>  }
こちらの実装で、さらに質問させてください。
興味で、これらの関数がどのスレッドで動作しているか確認したところ、
DoWorkAsync()もbutton1_Click()もUIスレッドでした。
button1_Click()からawaitでDoWorkAsync()が実行され、
非同期で別スレッドで実行されていると思ったのですが、
同じスレッドで動作しているようです。
DoWorkAsync()内の「await Task.Delay(1000);」は別スレッドで動作している認識ですが、
なぜ同じスレッドで動作しているのでしょうか。
引用返信 編集キー/
■95233 / inTopicNo.7)  Re[5]: サブルーチンから呼び出すawait/asyncの動きについて
□投稿者/ Hongliang (1056回)-(2020/07/02(Thu) 15:56:00)
> こちらの実装で、さらに質問させてください。
> 興味で、これらの関数がどのスレッドで動作しているか確認したところ、
> DoWorkAsync()もbutton1_Click()もUIスレッドでした。
> button1_Click()からawaitでDoWorkAsync()が実行され、
> 非同期で別スレッドで実行されていると思ったのですが、
> 同じスレッドで動作しているようです。
> DoWorkAsync()内の「await Task.Delay(1000);」は別スレッドで動作している認識ですが、
> なぜ同じスレッドで動作しているのでしょうか。

非同期とスレッドは別物です。
非同期は「呼んだら呼んだ先の処理が完了する前に呼び出し元に返ってくる」です。
// 多くのケースで、その実現のためにスレッドを使用しますが。

button1_Clickは当然UIスレッドで開始され、まずDoWorkAsyncメソッドが呼び出されます。
この時点ではただのメソッド呼び出しなのでスレッドは移動しません。
DoWorkAsyncメソッドはまずTask.Delayを呼び出します。
Task.DelayはTaskオブジェクトを返し、awaitでTaskの完了を待ちます。
// ちなみにReferenceSourceによるとSystem.Threading.Timerを使っているようです。
https://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs,5fb80297e082b8d6

WinFormの場合、UIスレッドでawaitすると、続きの処理はUIスレッドで実行されます。
// 先の投稿で書いたTaskScheduler.FromCurrentSynchronizationContext()の働きです。
待っているのはUIスレッドなので、デバッガでもUIスレッドで待っているように見えるでしょう。
Task.Delayの完了後、awaitの続きが実行されますが、当然UIスレッドです。

Task.Delayではなく、以下のようなコードで、DoWorkAsyncからDoWorkまで
ステップインしていってみてはいかがでしょうか。
Task.Runであれば明示的にスレッドを起動します。

async Task DoWorkAsync() {
    await Task.Run(() => DoWork());
    label1.Text = "完了";
}
void DoWork() {
    for (int i = 0; i < 3; i++) { }
}

引用返信 編集キー/
■95234 / inTopicNo.8)  Re[6]: サブルーチンから呼び出すawait/asyncの動きについて
□投稿者/ ぶなっぷ (229回)-(2020/07/02(Thu) 16:28:06)
スレッドの話については、Hongliangさんが答えてくれているので省略します。

以下については、場合によりけりですね。
> 1)よりも2)を使う場合の方が一般的と思いましたがいかがでしょうか。
> awaitで一目瞭然だし、result参照するとブロックされるし。

例えば、プリンタに印刷する非同期メソッドを例にします。
(戻り値boolは印刷に成功したかどうか?)
async Task<bool> PrintStr(string msg);

リアルタイムでデータを取得し、取得できたものから印刷なんて処理を考えます。
この場合、2)を使うと、
bool ret = await PrintStr();
となって、1回印刷するたびに、印刷待ち待機状態になっていまい、
リアルタイム処理じゃなくなります。

なので、
Task<bool> task = PrintStr();
とだけして、どんどん印刷します。
taskの値は、どっかに記憶しておきます(List<Task>とか)。

手すきになったときに、task.IsCompletedを参照して、
「あ、終わってる」
みたいな感じです。
引用返信 編集キー/
■95237 / inTopicNo.9)  Re[6]: サブルーチンから呼び出すawait/asyncの動きについて
□投稿者/ ほにゃ太郎 (4回)-(2020/07/02(Thu) 16:50:35)
Hongliang さん、

返信ありがとうございます。

> button1_Clickは当然UIスレッドで開始され、まずDoWorkAsyncメソッドが呼び出されます。
> この時点ではただのメソッド呼び出しなのでスレッドは移動しません。
awaitで呼び出しているが、普通の関数コールのようになっているのですね。
awaitを付けないと、await Task,Delay()の後に待たずに一気に処理されるためawaitは必要と。。

> // ちなみにReferenceSourceによるとSystem.Threading.Timerを使っているようです。
> https://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs,5fb80297e082b8d6
よってこのタイマ処理は別スレッドで動いているのですね。
そしてそのスレッドが動いている間はUIスレッドはアイドルなので、
label1.Textを更新できると。。。

> Task.Delayではなく、以下のようなコードで、DoWorkAsyncからDoWorkまで
> ステップインしていってみてはいかがでしょうか。
> Task.Runであれば明示的にスレッドを起動します。
まさにやってみました。UIスレッドとは別スレッドで、label1.Textの更新でエラーでした。


なんとなくわかってきましたが、まだ物にできた感は薄いです。
ぶなっぷさんからの指摘にもありましたがC言語の従来型スレッドの知識が逆にジャマしています。

取っ掛かりのところは理解できましたので解決とさせていただきます。
後は実践で。
大変助かりました。丁寧かつ長い間お付き合いいただきありがとうございました。

解決済み
引用返信 編集キー/
■95238 / inTopicNo.10)  Re[7]: サブルーチンから呼び出すawait/asyncの動きについて
□投稿者/ ほにゃ太郎 (5回)-(2020/07/02(Thu) 16:54:22)
ぶなっぷさん、

> 例えば、プリンタに印刷する非同期メソッドを例にします。
大変わかりやすいたとえです。
大変参考になります。
一概にどちらとは言えず使い分けなのですね。

C#は負の遺産と呼ばれるものが多数で(特にスレッド関係)
教えていただいた「var task = DoWorkAsync()」の方法も負の遺産かと思いましたが
こちらは現役でした^^;
(この負の遺産が、私が中々理解できない要因にもなっている気がします。

解決済みとさせていただきました。
長い間お付き合いいただきありがとうございました。
引用返信 編集キー/

このトピックをツリーで一括表示


トピック内ページ移動 / << 0 >>

このトピックに書きこむ