🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
C#

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

Q&A

解決済

4回答

3441閲覧

基本的な質問ですが、Taskの結果がバラバラになります。

nankoko

総合スコア20

C#

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

0グッド

1クリップ

投稿2021/01/25 15:33

どうして毎回結果がバラバラなんでしょうか?
しかも6??とかどこから来るんでしょうか??

Taskの事を全然理解していない者です。
基本的な事かもしれませんが・・
調べるための検索キーワードになる、ヒントだけでも教えて頂けたら助かります。

using System; using System.Threading.Tasks; class prog { static void Main() { for(int i=0; i<6; i++){ Task.Run(() => Console.WriteLine(i)); } Console.ReadLine(); } } //結果(毎回バラバラ) //1 //2 //2 //5 //6 //6

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

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

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

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

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

guest

回答4

0

こちらの回答を参照してください。
C#のTask配列をforでセットしようとするとエラーになる

(追記)
もう少し丁寧な記事を書いてみました。
[C#] ラムダ式内でラムダ式外のループカウンタ変数を使用すると危険

投稿2021/01/25 16:59

編集2021/01/29 01:01
退会済みユーザー

退会済みユーザー

総合スコア0

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

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

nankoko

2021/01/29 03:20

お返事大変遅くなってスミマセン。 せっかく教えて頂いのにTaskの前に、前提知識が色々と足りなくて 全く理解が追いつかなくて時間がかかってしまいました。 URL大変参考になりました。 特に追記のほうで、理解が一気に進みました。 ありがとうございます!
fana

2021/01/29 04:51 編集

以下のような理解で合っていますでしょうか? --- 元の状態だと, ラムダの実態(っていうの?)が6個できて,その6個全員が同一の変数 i を参照(という言葉でいいのか?)している. iの値はforを実施しているスレッドの処理が進むにつれて変化していくから,各タスクがラムダを実行した際のiの値が出力される結果となる. なので,タスクの実行がforの繰り返しが完全に終わった後であった場合,6が表示される. 「タスク」の存在はこの話と本質的に無関係なので取っ払うと, var Delegs = new Action[6]; for( int i=0; i<6; ++i ){ Delegs[i] = ()=>{ Console.WriteLine( i ); }; } foreach( var D in Delegs ){ D(); } //全部 6 が表示される みたいな. --- 追記のリンク先に書かれている > forループ内でローカル変数countを宣言してループカウンタを割り当て… とした場合には, 6個のラムダの実態がそれぞれ個別の変数countを参照する形になるから,{0,1,2,3,4,5}が一回ずつ表示される.
退会済みユーザー

退会済みユーザー

2021/01/29 05:17 編集

> fanaさん >>ラムダの実態(っていうの?)が6個できて ラムダの実態というとなんか変な感じがしますが、ラムダ式を実行するTaskが6つですね。 全てのTaskが同じ変数(どの時点での値かは不明)を見ているのと、各Taskがそれぞれのループで専用に確保された変数にコピーされた値(コピーされた値なのでその後は不動)を見ているという違いです。
fana

2021/01/29 05:20

ありがとうございます. (私の勝手な感覚だと,値型である i はコピーされてほしいなぁ,とか微妙に思う気がしないでもない…)
退会済みユーザー

退会済みユーザー

2021/01/29 05:26 編集

今回はたまたまループカウンタを使用した例ですが、逆に変動する事が前提の変数を見ていると勝手にコピーされるとまずい、というケースはありそうです。
nankoko

2021/01/29 23:48

>radianさん Qiitaはradianさんが、わざわざ書いて頂いた記事とは気づきませんでした! ありがとうございます! 同じ変数を見ているのと専用に確保された変数にコピーされた値を見てる違いと言うのは分かりやすいですね。 Parallelクラスなんて言う並列ループを実現する仕組みがあるとは!
nankoko

2021/01/29 23:48

>fanaさん var Delegs = new Action[6]の例は面白いですね。 なるほどぉ、「タスク」と「参照渡し」と2つの原因が混ざっててややこしいです。 確かに値型のintが、参照渡しになってるのはちょっと・・
退会済みユーザー

退会済みユーザー

2021/01/30 00:57

参照渡しになる訳ではないです。
nankoko

2021/01/30 02:57

var Delegs = new Action[6]; for( int i=0; i<6; ++i ){ Delegs[i] = ()=>{ Console.WriteLine( i ); }; } foreach( var D in Delegs ){ D(); } //全部 6 が表示される これはiが、参照渡しだから6になるのでは無いのでしょうか?
nankoko

2021/01/30 07:24

教えて頂いたサイトで勉強しました。 匿名関数が、forループのローカル変数iをキャプチャしたので iは、ガベージコレクションされずに、最後は6になったまま残り続けた。 Taskは、呼び出し元のスレッドと同時(順不同?)に実行されるので 実行されたタイミングで、キャプチャしたiの値を見に行くので、上記のような結果になった これであっているでしょうか?
退会済みユーザー

退会済みユーザー

2021/01/30 09:00

そのキャプチャされる動作というのが、先程のリンク先の記事にある匿名関数のコンパイル結果を読み進めると、どういう挙動をするのか良く判ります。
nankoko

2021/02/01 04:37

Task.Runで、クロージャーが作成されたあとで 6になったiの値を見に行ってるのは、iの参照ではなく以下の理由なんですかね https://ufcpp.net/study/csharp/sp2_anonymousmethod.html >>「呼び出し元とクロージャ側とで、ローカル変数xの書き換え結果が共有される」 もう勝手にやってくれる事なので、 ここまで追わなくても良いかも知れませんけど難しいです。。
退会済みユーザー

退会済みユーザー

2021/02/01 08:18 編集

ただ使うだけであれば、こういう風に使うとこうなる、という結果だけ知っていればよいのですが、何故そうなるかという部分まで理解しようと思うと、結構深い部分まで追う必要があり、確かに難しいと思います。
nankoko

2021/02/01 08:14

元々タスクを勉強したかっただけなのに、気づけばいつの間にかクロージャとか思わぬ場所に笑 でもいつかジグソーパズルが繋がった瞬間みたいに、パッと理解が進む時がくる事を信じて勉強しておきます。
guest

0

既に質問は閉まっていますが、下記についてこうプログラムが動きうるであろうストーリーの1例をシーケンス図で表現してみたので、載せておきます。

//結果(毎回バラバラ)

//1
//2
//2
//5
//6
//6

結果について

下記のタイミングに注目してください。

  • Task.Run をするタイミング
  • 各タスクが i を取得するタイミング
  • i を Console.WriteLine で出力するタイミング

この図を通して伝えたいことは、まず Task を利用する事で各タスクやタスク外の処理がそれぞれ並列に動くことです。
そして、タスクに作業を配置(Task.Run)するタイミングと実際にタスクが処理(このプログラムでいうConsole.WriteLine)を行うタイミングは別であるということです。

少なくともそれ等のタイミングの間には隙間が存在するので、Task.Run したタイミングと Console.WriteLine したタイミングで i の値が変化することは至って普通の事象です。

また、タスクに委譲した作業が実際に処理されるタイミングはスレッドプールのみぞ知る、です。
スレッド プールとタスク

スレッドプールのスレッドが足りなくて新たなスレッドを内部で生成する場合なんかは時間がかかるかもしれません。スレッドの生成にはコストがかかります。
マネージド スレッド プール

スレッド プールの最小値に達すると、追加のスレッドが作成されるか、いくつかのタスクが完了するまで待機状態になります。

投稿2021/01/29 04:41

BluOxy

総合スコア2663

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

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

BluOxy

2021/01/29 04:50

補足ですが、書いた図はあくまでイメージ図であって、100回中100回図のような動作をするというものではありません。 それぞれのタスクに委譲された作業が行われるタイミングによって、i の値は変わります。 例えば、Console.WriteLineの手前に数秒遅延処理を加えれば大体の場合は下記のように出力されるでしょう。 //結果 //6 //6 //6 //6 //6 //6
nankoko

2021/01/29 23:35

うぉぉーーーこれは素晴らしいですねー! Taskの教科書に載せたいレベルのクオリティ笑 Console.WriteLine(2)や、Console.WriteLine(6)が続けて実行されるのが、この図ではとてもわかりやすいです! プログラムは必ずコード順に実行されるという固定概念があったので中々理解が出来ませんでした。 自分はスレッドとタスクの違いすら分からなかったんですが・・ スレッドを使い回すためにスレッドプールがあって、スレッドプールを使いやすくするためにタスクがあるんですね。 ありがとうございます!
guest

0

そもそも、別タスクを非同期実行させるためにTask.Runさせるんでしょう。
それを忘れてはいけません

() => Console.WriteLine(i)

のタスクは、Task.Runで実行されるわけでなく、ただ非同期実行させるために登録されるだけです
それが実際に実行されるのはいつか、ってのを考えてみればどうでしょう

投稿2021/01/25 22:36

y_waiwai

総合スコア88038

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

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

nankoko

2021/01/29 03:22

お返事遅くなってすみません。 「Task.Runで実行されるわけでなく、ただ非同期実行させるために登録されるだけ」と言うのは 大変分かりやすい説明でした。 非同期実行という事が今までの延長線で考えても全く理解出来なかったので、助かりました ありがとうございます!
guest

0

ベストアンサー

6が出力される原因はforループを抜けて最後にi++された後のiが参照されて出力されているからです。

Task.Run(() => ...)が実行された時点で別スレッドでの処理が開始され、Main()が動作しているスレッドではない別のスレッドで非同期的にConsole.WriteLine(i)が実行されます。

[追記]質問の方にTask/非同期以前に変数の生存期間に対する理解不足があると認識できてませんでした。以下の回答は取り消したいと思います。

Task.Runで非同期処理させた内容を待機するにはasync/awaitを利用することで非同期処理を同期的に扱うことができます。

class prog { static async Task Main() { for(int i=0; i<6; i++){ await Task.Run(() => Console.WriteLine(i)); } Console.ReadLine(); } }

というように非同期での出力処理をawaitで待機させることで0~5まで順番に出力させられます。

参考: async および await を使用した非同期プログラミング

投稿2021/01/26 05:03

編集2021/01/30 04:35
tor4kichi

総合スコア769

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

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

退会済みユーザー

退会済みユーザー

2021/01/26 06:02

出力結果自体は確かに順番にはなりますが、単なる同期処理になってしまうのでTaskを使用する意味がなくなり本末転倒です。
tor4kichi

2021/01/28 09:22

質問趣旨を「Taskの挙動がわからない」ということだろうと捉えて回答したんですが、本末転倒とまで言われるのは心外ですねぇ。
退会済みユーザー

退会済みユーザー

2021/01/29 02:02

気を悪くされたなら申し訳ないですが、4行目までの回答なら問題ないと思うのですが、その後間違った解決方法を提示しているからマイナスが付いてるのだと思います。(ちなみに私がマイナスを付けた訳ではないです)
Zuishin

2021/01/29 02:30

質問者さんが並列処理について学ぼうとしているのであれば、並列化できていない解決法は本末転倒と言われても仕方ないと思います。 クロージャについて学ぼうとしているのであれば回答になっていますが、クロージャについて理解できるだけの情報量はありません。 私自身は低評価はしていませんが、とりあえず動くコードを出しましたという回答にしか見えないので、高評価か低評価かと言われれば低評価になります。
nankoko

2021/01/29 03:34

お返事大変遅くなってすみません! 自分にとってTaskはかなり難しくて、教えていただい事をヒントに勉強してまして 追加で質問したい事もあったのですが、まだまだ時間もかかりそうなので先にお礼をお伝えします。tor4kichiさんのアドバイスは自分には十分参考になりました! とても丁寧な説明を頂けたので理解の浅い自分でも分かりやすかったです。 またこちらで質問させていただく事がありましたらよろしくお願い致します!
Zuishin

2021/01/29 04:10

このようなことが起きるのはクロージャの変数キャプチャで説明できます。Task 特有の性質ではなく、Task 生成に引数として渡されたデリゲートに属する問題です。 この回答で本当に理解できたのなら私の書いていることがわかると思いますが、おそらくはチンプンカンプンだろうと思います。 質問者さんは大きく道を外れたにも関わらず「完全に理解した」と思ってしまったため、有害な回答として低評価します。
nankoko

2021/02/01 04:42

Zuishinさん、クロージャと、変数キャプチャを教えて頂きありがとうございます! ちょっと勉強しましたが、分かったような分からないよな 内部で自動でやってくれてる事なので理解しても忘れてしまいそうですが・・ Taskと含めて勉強して行こうと思います。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.36%

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

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

質問する

関連した質問