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

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

新規登録して質問してみよう
ただいま回答率
85.35%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

2回答

3101閲覧

await Task.Run(...)で待っている時、呼び出し元スレッドは何をしているのか?

退会済みユーザー

退会済みユーザー

総合スコア0

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

0グッド

1クリップ

投稿2020/08/22 08:07

私はC++が専門でC#はそれほど詳しくないのですが、委託先が作成したC#のコードを動かしたところ問題が発生しているので、原因の調査をしています。そこで、ぶち当たった疑問について質問させて下さい。

await Task.Run(...)すると、呼び出し側スレッドはそのTaskが終わるの待つを思いますが、待っている間呼び出し側スレッドは何をしているのでしょうか?

  1. 何もせず、ただTaskの完了を待っている(Taskからのイベントやメッセージなどを待っているだけ)
  2. 呼び出し元でTimerが定義されている場合、時間が来るとawait中でもタイマー関数が呼ばれる
  3. 呼び出し元スレッドが何らかの非同期処理(ネットワーク処理やディスクI/Oなど)を実行していた場合、それらの完了イベントが実行される
  4. 呼び出し元がUIを持つスレッドの場合(メインスレッドなど)、メッセージループが回っていてWindowsへのメッセージが処理される
  5. その他・・・

どうぞよろしくお願いします。

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Zuishin

2020/08/22 08:14

await Task.Run の呼び出し前、呼び出し中、呼び出し後に Thread.CurrentThread.ManagedThreadId を表示すればわかります。
退会済みユーザー

退会済みユーザー

2020/08/22 08:24

ThreadIdを見ると何が分かるというのでしょうか? await Task.Run呼び出し中に、呼び出されたTaskの中で何が行われているか?という質問ではなく、呼び出し中に呼び出したスレッド自体が何をしているのかという質問なので、「呼び出し中」で待っている時にManagedThreadIdを表示させることは不可能です。 呼び出し前と呼び出し後であればManagedThreadIdを見ることはできますが、それを見ることで「何が」分かるのかをご教示くださいませんか?
Zuishin

2020/08/22 08:26

意味が分かりませんか? using System; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp1 { class Program { static async Task Main(string[] args) { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); await Task.Run(() => { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); }); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); } } } これを実行すればわかります。
退会済みユーザー

退会済みユーザー

2020/08/22 08:46

提示いただいたコードから分かることは、呼び出し前と呼び出し後のスレッドは同じであること、Task.Runの中で実行されているスレッドはそれとは異なるスレッドあること、この2点だと思います。 少し言い換えます、Task.Runが別スレッドで実行されている間、呼び出し元スレッドは単にそのスレッドの終了を待っているだけなのか、他に何かやっていることはあるのではないか?という質問です。 なので私の質問では、提示していただいたコードのawait Task.Runの中にあるConsole.WriteLine処理(および同じ関数内でのすべての処理)も、await Task.Runの外にある2つのConsole.WriteLine部分の処理についても関心がなく、尋ねてもいません。
Zuishin

2020/08/22 08:54 編集

違います。あなたの思っているような実行結果にはなりません。実行してから言ってください。
Zuishin

2020/08/22 08:52

実行したらわかると言いましたが、実行せず頭の中で想像してわかるとは誰も言っていません。
退会済みユーザー

退会済みユーザー

2020/08/22 08:55

違わないと思います。あなたにコードを提示してもらう以前にそういうことは確認しています。 何が「違う」のか教えて下さい。
Zuishin

2020/08/22 08:56

実行結果を書いてください。そのうえでわからないとしたら、一度寝て目を覚ましてから行ったほうがいいと思います。そうとう寝ぼけています。
Zuishin

2020/08/22 08:59

と言っても絶対実行しないと決めているようなので、結果を書きましょうか。 await 実行前のスレッドはそこで終了し、実行中のスレッドと実行後のスレッドは同じものになります。 これでもまだ終了を待つとかどうとかいう発想になるのであれば、本気で寝たほうがいいと思います。
Zuishin

2020/08/22 09:17

Windows Forms で、イベントハンドラの中で呼んだ場合、呼び出し元のスレッドはイベントハンドラから戻ってメッセージループに戻ります。ユーザーコードから離れることを tamoto さんは「フリーになる」と表現しています。
退会済みユーザー

退会済みユーザー

2020/08/23 02:02

コンソールアプリとUIでは、何かのモデルが違ってて、awaitの内部的な動きも違ってたような気がしますけど...具体的な話でなくて申し訳ありませんが... これ結構難しい話なので、コードベースでないと話が噛み合わないような気がしますよ
退会済みユーザー

退会済みユーザー

2020/08/23 05:12

Zuishin さんが上のコメントに書かれたコードを試してみました。 > 実行中のスレッドと実行後のスレッドは同じものになります。 時々 1 ⇒ 3 ⇒ 4 と言うようにすべて違うスレッドになることもあるようです。
guest

回答2

0

こんにちは。

「問題が発生している」の問題を質問して頂けた方が実のある回答が得られる気がしますが。

await Task.Run(..) の書き方であれば、その行に到達した時点で呼び出し元スレッドは終了し、フリーになります。
そして、Task が完了した後は続きの処理を同じスレッドにスケジュールします。

質問にある番号だと 1 は間違いで、4 は正しいですね。
それ以外は単にフリーのスレッドをどう使うかという話なので、質問自体が成り立ってないものと思います。

投稿2020/08/22 08:17

tamoto

総合スコア4252

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2020/08/22 08:38

ありがとうございます。 大事なことを言い忘れていました。呼び出し元はUIを処理しているメインスレッドという前提でお願いします。 もし私が書いた「4」が正しいのであれば、await Task.Run中はUI操作ができるということになると思いますが、そういう理解でよろしいでしょうか? > 「問題が発生している」の問題を質問して頂けた方が実のある回答が得られる気がしますが。 すみません、私がいま問題としているのはawait Task.Runで待っている間にも、呼び出し元スレッドで設定したTimer処理が呼び出されているのではないか?ということです。この点についてはいかがですか? この疑問が現在の課題ですが、後学のため、await Task.Runで待っている間に裏で行われていることはどんなことがあるのかを知りたくてこういう質問となりました。Win32 APIでゴリゴリ書いていると、await Task.Runみたいな便利な機能はありませんので、自分でスレッド起動&待ちのループやイベント待ちのコードを書くので、待ち時間に何をするかは自分の実装次第なのですが、こういう便利な機能の裏側がブラックボックスのような気がして、勉強したいと思った次第です。 > await Task.Run(..) の書き方であれば、その行に到達した時点で呼び出し元スレッドは終了し、フリーになります。 「元スレッドは終了」というのは、ちょっと語弊を招くのではありませんか?スレッド自体が終了するのではなく、呼び出し元スレッドの処理を一時中断してCPUのタイムスライスを別のスレッドに明け渡すという理解でよいですか? そして、「フリーになります」とはどういう意味ですか? > そして、Task が完了した後は続きの処理を同じスレッドにスケジュールします。 これはそう思います。 よろしくお願いします。
tamoto

2020/08/22 11:49

呼び出し元スレッド == UI スレッドということですね。 はい。await Task.Run 中は UI 操作ができます。 await Task.Run(..) の「(..) の処理」を実行している間、UI スレッドは完全に空き状態になるため、イベントループは回り、UI スレッドにスケジュールされているタイマーは働き、全ての処理が行われます。 「(..) の処理」が完了すると、呼び出し元で await 以降に書かれている「続きの処理」が UI スレッドにスケジューリングされ、UI スレッドが空き次第、UI スレッド上で動作します。 「元スレッドが終了」と書いたのは良くなかったですね。 正しくは、「UI スレッド上で実行される処理は最初の await まで」です。 await の行に到達した時点で、UI スレッドを利用した計算は終わり、他の UI 処理が可能になります。 「処理を一時中断」しているわけでは無く、非同期メソッドは「メソッドの処理が予めぶつ切りにされていて、最初の await までだけが UI スレッドにスケジュールされる」が正しいです。
guest

0

C++でstd::futureの利用経験はありますでしょうか?
C#のTaskstd::futureと同じくFutureパターンを実現する機構です。
async/awaitを用いると、『await以降がFutureパターンにラップされる』と考えると理解しやすいかと思います。

ひとつ例を見てみます。
以下のようにHogeFugaAsyncPiyoAsyncという形で非同期メソッドを呼び出します。
ここで、HogeFugaAsyncの結果をスレッドブロックして待機、FugaAsyncPiyoAsyncをawaitで待機しています。

C#

1using System; 2using System.Threading; 3using System.Threading.Tasks; 4 5class Program 6{ 7 static void Main(string[] args) 8 { 9 Hoge(); 10 } 11 12 13 static void Hoge() 14 { 15 Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Hoge 1"); 16 FugaAsync().Wait(); 17 Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Hoge 2"); 18 } 19 20 static async Task FugaAsync() 21 { 22 Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Fuga 1"); 23 var piyoTask = PiyoAsync(); 24 Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Fuga 2"); 25 26 await piyoTask; 27 28 Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Fuga 3"); 29 } 30 31 static async Task PiyoAsync() 32 { 33 Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Piyo 1"); 34 await Task.Delay(1000); 35 Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Piyo 2"); 36 } 37} 38

このプログラムの実行結果は例えば以下のようになります。

[1] Hoge 1 [1] Fuga 1 [1] Piyo 1 [1] Fuga 2 [4] Piyo 2 [4] Fuga 3 [1] Hoge 2

実行結果を見ると、Hoge1Fuga1Piyo1Fuga2となっており、await以降のコードを削除して同期処理させた場合と同じふるまいをしています。
一方でFuga2Piyo2よりも先に処理が完了していることから、PiyoAsyncawait以降のコードはFutureオブジェクトにラップされ別スレッドで処理が進行していることがわかります。

この一連の流れをシーケンス図で描くと以下のようになります。

イメージ説明

ここではメインスレッドを.Wait()してブロックしましたが、通常のUI処理ではブロック処理をせず現在のメソッドを終了するでしょう。


さて、await以降の処理がFuture化され別スレッドで実行されると言いましたが、ここで使われるスレッドは実は条件次第で異なります。

  • 多くの場合はフレームワークがスレッドプールの中からよしなに実行スレッドを選択します。
    上記の例でも、await以降の処理がスレッド4に移されていることがわかります。
  • 呼び出し元のスレッドが同期コンテキスト(≒メッセージループ)を持っており、かつawait時にConfigureAwait(false)設定していない場合、呼び出し元スレッドで実行します。例えばWin32であればメッセージキューにawait以降の処理を積み直すということをします。
    この場合、例のように.Wait()すると、メッセージループをブロックしたまま後続メッセージを待機するという処理になってしまい、結果デッドロックを生じます。

GUIアプリケーションではawait以降の処理がUIスレッドで実行されるかワーカースレッドで実行されるかを常に意識するようにするとコードを理解しやすくなるのではと思います。

投稿2020/08/24 10:40

編集2020/08/24 10:54
GlassGrass

総合スコア52

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

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

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問