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

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

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

async/awaitの使い方

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

■91647 / inTopicNo.1)  async/awaitの使い方
  
□投稿者/ こまお (1回)-(2019/07/17(Wed) 16:42:41)

分類:[C#] 

こんにちは。
重たい処理をするのに↓のようなことをしているのですが、
(1)を書くとなぜか(2)に来なくなってしまいます。
何が良くないのでしょうか。
よろしくお願いします。

using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace Sketch {

  public partial class MainWindow : Window {

    public MainWindow() {
      InitializeComponent();
    }

    class AsyncTest {
      private bool busy = false;

      public bool IsBusy() {
        return busy;
      }

      public async void AsyncMethod() {
        busy = true;
        await Task.Run(() => {
          HeavyWeightMethod();
        });
        busy = false; //(2)ここへ来なくなる
      }

      public void HeavyWeightMethod() {
        Thread.Sleep(1000);
      }
    }

    private void Button_Click(object sender, RoutedEventArgs e) {
      var asyncTest = new AsyncTest();

      asyncTest.AsyncMethod();

      while(asyncTest.IsBusy()) { //(1)これを書くと...
        Thread.Sleep(100);
      }

      MessageBox.Show("Done");
    }
  }
}

引用返信 編集キー/
■91649 / inTopicNo.2)  Re[1]: async/awaitの使い方
□投稿者/ Hongliang (843回)-(2019/07/17(Wed) 16:53:02)
肝心のUI側でSleepしてたら台無しです。
まずUI側をasync/awaitで書く必要があります。

private async void Button_Click(object sender, RoutedEventArgs e)
{
    var asyncTest = new AsyncTest();
    await asyncTest.MethodAsync(); // awaitableなメソッドはAsync接尾辞がお約束
    MessageBox.Show("Done");
}

await MethodAsyncするにはMethodAsyncがTaskを返す必要があるので、
MethodAsyncはこんな感じになるでしょう。

public async Task MethodAsync()
{
    await Task.Run(() => {
        HeavyWeightMethod();
    });
    // または単純に、asyncを外したうえで
    // return Task.Run(() => HeavyWeightMethod());
}

引用返信 編集キー/
■91650 / inTopicNo.3)  Re[1]: async/awaitの使い方
□投稿者/ 魔界の仮面弁士 (2244回)-(2019/07/17(Wed) 17:28:01)
No91647 (こまお さん) に返信
> 何が良くないのでしょうか。

UI スレッド側で Sleep したり、ループ監視したりしている点が一番良くないです。

それと、"async void" は基本的に使いません。async Task に書き換えましょう。
(async void が必要になるのは、Button_Click 等のイベントハンドラに指定する場合ぐらいです)



もし、AsyncTest クラスの実装を書き換えず、
MainWindow 側だけで無理矢理対処するとしたら…。


private void Button_Click(object sender, RoutedEventArgs e)
{
  var asyncTest = new AsyncTest();
  asyncTest.AsyncMethod();

  while (asyncTest.IsBusy())
  {
    // Thread.Sleep(100);

    var frame = new DispatcherFrame();
    Dispatcher.CurrentDispatcher.BeginInvoke(
      DispatcherPriority.Background,
      new DispatcherOperationCallback(o =>
      {
        ((DispatcherFrame)o).Continue = false;
        return null;
      }),
      frame);
    Dispatcher.PushFrame(frame);
  }

  MessageBox.Show("Done");
}
引用返信 編集キー/
■91651 / inTopicNo.4)  Re[2]: async/awaitの使い方
□投稿者/ 魔界の仮面弁士 (2245回)-(2019/07/17(Wed) 17:50:19)
No91650 (魔界の仮面弁士) に追記
> UI スレッド側で Sleep したり、ループ監視したりしている点が一番良くないです。

UI スレッド側で待機させたら意味がありませんので、
No91649 の Hongliang さんの実装に一票。


> もし、AsyncTest クラスの実装を書き換えず、
> MainWindow 側だけで無理矢理対処するとしたら…。

MainWindow 側の実装が No91647 のままであるとして、
かつ、AsyncMethod を void に据え置くなら、こんな感じ。

public async void AsyncMethod()
{
  busy = true;
  await Task.Run(() =>
  {
    HeavyWeightMethod();
  }).ContinueWith(t => { busy = false; }, TaskScheduler.Current);
}
引用返信 編集キー/
■91653 / inTopicNo.5)  Re[3]: async/awaitの使い方
□投稿者/ ぶなっぷ (198回)-(2019/07/17(Wed) 18:47:55)
たぶんですが、こまおさんの疑問は、別スレッドの終了待機を自身で行う必要があると
勘違いしてるところから来ているのでしょう。
> while(asyncTest.IsBusy()) { //(1)これを書くと...
> Thread.Sleep(100);
> }

違うんです。
await Task.Run(() =>
とすれば、Task.Run()で起動したスレッドが終了するまで自動待機です。
自身で待機する必要はありません。

引用返信 編集キー/
■91657 / inTopicNo.6)  Re[4]: async/awaitの使い方
□投稿者/ こまお (2回)-(2019/07/18(Thu) 09:25:29)
2019/07/18(Thu) 09:27:52 編集(投稿者)
2019/07/18(Thu) 09:26:47 編集(投稿者)
2019/07/18(Thu) 09:26:38 編集(投稿者)

<pre><pre>みなさんありがとうございます。
魔界の仮面弁士さんの2番目ので思った動きになりました。

これに関して教えてほしいことが3つあります。
1) 私のコードで(1)をすると(2)へ来なくなる理由(デッドロック?)
2) 上記に関係すると思いますが、仮面弁士さんの2番目の様にするとうまくいく理由
3) ContinueWith()に渡すAction<Task>の引数tとは何か

お手数ですが、よろしくお願いします。

public async Task AsyncMethod() {
  busy = true;
  var task = Task.Run(() => {
    HeavyWeightMethod();
  });
  await task.ContinueWith(t => {
    busy = false;
  });
}
</pre></pre>

引用返信 編集キー/
■91659 / inTopicNo.7)  Re[5]: async/awaitの使い方
□投稿者/ kiku (122回)-(2019/07/18(Thu) 10:15:31)
No91657 (こまお さん) に返信
> 1) 私のコードで(1)をすると(2)へ来なくなる理由(デッドロック?)
> 2) 上記に関係すると思いますが、仮面弁士さんの2番目の様にするとうまくいく理由
> 3) ContinueWith()に渡すAction<Task>の引数tとは何か

1)と2)についての参考になると思い
検索した結果を貼っておきます。
https://qiita.com/mounntainn/items/3f39e0c57412c48508bf

質問者さんは、自分のコードがどのような順番に
どのように動作するのか箇条書きで説明頂ければ
どこの理解が間違っているのか皆さんが指摘しやすいと思いました。
引用返信 編集キー/
■91660 / inTopicNo.8)  Re[5]: async/awaitの使い方
□投稿者/ 魔界の仮面弁士 (2246回)-(2019/07/18(Thu) 10:26:44)
2019/07/18(Thu) 10:55:18 編集(投稿者)

以下、自分の認識です。
おかしな所があったら突っ込み歓迎。


No91657 (こまお さん) に返信
> 1) 私のコードで(1)をすると(2)へ来なくなる理由(デッドロック?)

No91647 のコードで言うと、フィールド変数 busy を書き換えている箇所は、
UI スレッド上で動作することになります。(Task 部は別スレッド)

つまり、「busy = false に戻す処理」と「Button_Click 内でのループ監視」が、
いずれも UI スレッドであることが問題になります。busy = false; に書き戻そうにも、
UI 側で、Button_Click がまだ終わっていない状態なので、処理を差し戻せません。


一つのスレッドで複数の処理を同時には実行することはできませんので、
busy = false に戻す処理は、Button_Click の処理完了まで待たされることになります。
しかし Button_Click は、ループ監視によって、busy = false; への変化を
待ち続けている状態なので、デッドロックします。

No91650 のコードでは、処理を差し戻す余地を与えるため、DispatcherFrame を利用しています。
WinForms でいうところの DoEvents のようなものです。
https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.threading.dispatcherframe


> 2) 上記に関係すると思いますが、仮面弁士さんの2番目の様にするとうまくいく理由

No91651 のコードで言うと、「busy = true;」までは UI スレッド。
「Task.Run」はワーカースレッド A。
「ContinueWith」がワーカースレッド B。

スレッド B は A の後に実行されるようになっていますが、
それぞれは UI スレッドとは独立しています。

busy = false; にする処理を、UI スレッドに行わせるのではなく、
ワーカースレッドで行う事になるので、 No91647 のように阻害されることがありません。

ただし、TaskScheduler.Current を TaskScheduler.FromCurrentSynchronizationContext() に
変更した場合は、ContinueWith 部が UI スレッドのコンテキストとなるため、
No91647 と同じ道を辿ることになります。 No91650 を併用しない限りは。


それぞれの処理部で Thread.CurrentThread.ManagedThreadId を
Debug.WriteLine してみると、どのスレッドが呼ばれているかを
確認できるのではないでしょうか。


> 3) ContinueWith()に渡すAction<Task>の引数tとは何か

文字通りの Task です。

AsyncTest クラス内に
 private void Done(Task t) { busy = false; }
を用意して、
 await Task.Run(〜).ContinueWith(Done, TaskScheduler.Current);
と書くのと同じことです。


なお、await 処理のところを
 Task taskObj = Task.Run(〜);
 await taskObj.ContinueWith(Done, TaskScheduler.Current);
に書き換えた場合、Done の引数 Task t に渡されるインスタンスは、
上記の taskObj と同一の物となります。
引用返信 編集キー/
■91661 / inTopicNo.9)  Re[6]: async/awaitの使い方
□投稿者/ kiku (123回)-(2019/07/18(Thu) 10:47:46)
2019/07/18(Thu) 11:02:37 編集(投稿者)
No91660 (魔界の仮面弁士 さん) に返信
> 以下、自分の認識です。
> おかしな所があったら突っ込み歓迎。

魔界の仮面弁士さんはいつも完璧なので
突っ込みなどないのですが、
同じことの箇条書き説明してみました。

01  class AsyncTest {
02    private bool busy = false;
03    public bool IsBusy() {
04      return busy;
05    }
06    public async void AsyncMethod() {
07      busy = true;
08      await Task.Run(() => {
09        HeavyWeightMethod();
10      });
11      busy = false; //(2)ここへ来なくなる
12    }
13    public void HeavyWeightMethod() {
14      Thread.Sleep(1000);
15    }
16  }
17  private void Button_Click(object sender, RoutedEventArgs e) {
18    var asyncTest = new AsyncTest();
19    asyncTest.AsyncMethod();
20    while(asyncTest.IsBusy()) { //(1)これを書くと...
21      Thread.Sleep(100);
22    }
23    MessageBox.Show("Done");
24  }

UIスレッド
 18,19,07,08,20,04,21,22(ループ)

スレッドA(08から)
 09,14

08のawaitの機能でスレッドAの処理が終了したら、
08はUIスレッドで実行されたので
11もUIスレッドで実行しようとするが、
UIスレッドは20から22のループ中であるため、
UIスレッドに切り替えができない(デットロック)。

引用返信 編集キー/
■91665 / inTopicNo.10)  Re[1]: async/awaitの使い方
□投稿者/ WebSurfer (1861回)-(2019/07/18(Thu) 11:24:34)
No91647 (こまお さん) に返信

プログレスを知りたいということで busy を使っているような気がしますが、
そうであれば、以下の記事のようにして望むことができないですか?

WPF/Windowsフォーム:時間のかかる処理をバックグラウンドで実行するには?
https://www.atmarkit.co.jp/ait/articles/1512/02/news019.html
引用返信 編集キー/
■91666 / inTopicNo.11)  Re[6]: async/awaitの使い方
□投稿者/ kiku (124回)-(2019/07/18(Thu) 11:24:36)
No91660 (魔界の仮面弁士 さん) に返信
> No91651 のコードで言うと、「busy = true;」までは UI スレッド。
> 「Task.Run」はワーカースレッド A。
> 「ContinueWith」がワーカースレッド B。
> スレッド B は A の後に実行されるようになっていますが、
> それぞれは UI スレッドとは独立しています。
> busy = false; にする処理を、UI スレッドに行わせるのではなく、
> ワーカースレッドで行う事になるので、 No91647 のように阻害されることがありません。

まさしくその通りと思います。

今回は該当しませんが蛇足です。
busyがアトミックなboolでない場合には、注意が必要です。
UIスレッドと、スレッドBから同じインスタンスにアクセスすることに
なるので、排他制御を考える必要が出てきます。

引用返信 編集キー/
■91667 / inTopicNo.12)  Re[6]: async/awaitの使い方
□投稿者/ ぶなっぷ (199回)-(2019/07/18(Thu) 12:32:43)
kikuさんの行番号を借りて、ソースコードの実行順序を表すと以下の通りです。

ボタンクリックによって、18行目に移行します。
以降、
  18 → 19  → 06 → 07 → 08 → 09 → 20 → 03 → 04 → 05 → 20 (以降、無限ループ)
                                 ↓(別スレッド)
                                 13 → 14 → 15

HeavyWeightMethod()が終わったら、11行目へ移行したいわけですが、
そのためには同一スレッドで動作するButton_Click()の終了を待つ必要があります。
しかし、20行目のループが終了しない(無限ループ)ので、移行できません。

つまり、スレッド待ちによるデッドロックではなく、無限ループによるフリーズが起きて
います。
デバッガで20行目からステップインするとその様子が分かります。

01  class AsyncTest {
02    private bool busy = false;
03    public bool IsBusy() {
04      return busy;
05    }
06    public async void AsyncMethod() {
07      busy = true;
08      await Task.Run(() => {
09        HeavyWeightMethod();
10      });
11      busy = false; //(2)ここへ来なくなる
12    }
13    public void HeavyWeightMethod() {
14      Thread.Sleep(1000);
15    }
16  }
17  private void Button_Click(object sender, RoutedEventArgs e) {
18    var asyncTest = new AsyncTest();
19    asyncTest.AsyncMethod();
20    while(asyncTest.IsBusy()) { //(1)これを書くと...
21      Thread.Sleep(100);
22    }
23    MessageBox.Show("Done");
24  }

引用返信 編集キー/
■91668 / inTopicNo.13)  Re[2]: async/awaitの使い方
□投稿者/ こまお (3回)-(2019/07/18(Thu) 15:25:03)
みなさんありがとうございます。
とても良く判りました。

WebSurferさん、その通り。
処理の進捗が知りたかったです。今回の私の目的には合いませんでしたが、
意図をくみ取ってもらいうれしかったです。

(2)に来なくなる理由も判ってすっきりしたので
これにて閉じさせていただきます。

ありがとうございました。

解決済み
引用返信 編集キー/

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


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

このトピックに書きこむ