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

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

詳細はこちら
C#

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

並列処理

複数の計算が同時に実行される手法

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

Q&A

解決済

1回答

3650閲覧

[C#]別々の型を戻り値とするTaskを並列実行し、結果を組み合わせて計算する

woria

総合スコア36

C#

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

並列処理

複数の計算が同時に実行される手法

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

0グッド

0クリップ

投稿2020/12/25 02:02

前提・実現したいこと

C#初心者です。
C#でマスタとデータを組み合わせた結果を利用します。
マスタとデータの取得は完全に別個の処理なので、これらの取得を並列Taskで実行して処理の高速化を目指します。

該当のソースコード

C#

1var dataRows = await InputData.GetRowsAsync(); // データ取得Task<List<InputData.Row>>(並列したい) 2var mstRows = await Master.GetRowsAsync(); // マスタ取得Task<List<Master.Row>>(並列したい) 3 4// 二つを組み合わせる 5var query = from d in dataRows 6   join m in mstRows 7 on r.parts_name equals m.parts_name into mJoin 8 from mj in mJoin.DefaultIfEmpty() 9 Select new {...} 10 11// ↓↓↓queryを使った処理↓↓↓

試したこと

TaskをWhenAllし、Resultを利用することを考えました。

C#

1var dataRows = InputData.GetRowsAsync(); // データ取得Task<List<InputData.Row>> 2var mstRows = Master.GetRowsAsync(); // マスタ取得Task<List<Master.Row>> 3 4// 並列して実行するタスクを待機する 5await Task.WhenAll(dataRows, mstRows); 6 7// 二つを組み合わせる 8var query = from d in dataRows.Result 9   join m in mstRows.Result 10 on r.parts_name equals m.parts_name into mJoin 11 from mj in mJoin.DefaultIfEmpty() 12 Select new {...} 13 14// ↓↓↓queryを使った処理↓↓↓

ただし.Waitや.Resultはデッドロックの可能性があり、完全に理解してから使わないと危険と聞いています。
より安全な方法をご教授いただけますでしょうか。

補足情報(FW/ツールのバージョンなど)

.Net Framework 4.6.1

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

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

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

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

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

退会済みユーザー

退会済みユーザー

2020/12/25 02:30

> これらの取得を並列Taskで実行して処理の高速化を目指します。 マルチスレッドアプリだから高速になるということはなく、OS がスレッドを切り替えて処理を行うのでそのオーバーヘッドの分逆に遅くなるということになりますが、そこのところに思い違いはありませんか?
Zuishin

2020/12/25 02:50

query が走るタイミングでは既に処理が終わっているのでデッドロックにはなりません。
退会済みユーザー

退会済みユーザー

2020/12/25 04:47 編集

少し勘違いしてたので削除しました
退会済みユーザー

退会済みユーザー

2020/12/25 03:39 編集

そもそもの話として、相手が DB で Linq to Entities で処理するなら、Linq 式で JOIN した SELECT クエリが生成され、それを一本だけ DB に投げて目的のデータを取得できるのではないですか? であれば、クエリを 2 つに分けるより、その方が早いのではないですか?
guest

回答1

0

ベストアンサー

これらの取得を並列Taskで実行して処理の高速化を目指します。

以下の記事の「マルチスレッドの動作原理」セクションにある図の ② のようになると、OS がスレッドを切り替えて処理を行うのでそのオーバーヘッドの分逆に遅くなるということになりますので、③ のようにマルチコアを利用できる環境が必要で、さらにマルチコアを利用できるプログラミングを行うという話になると思います。

第1回 マルチスレッドはこんなときに使う (1/2)
https://www.atmarkit.co.jp/ait/articles/0503/12/news025.html

マルチコア プログラミングというと、タスク並列ライブラリ (TPL・・・Parallel.For とか Parallel.Invoke とか) を利用するという話になるかと思います。

質問者さんのコードの InputData.GetRowsAsync(), Master.GetRowsAsync() に同期バージョンがあればそれと Parallel.Invoke を利用するのがよさそうです。例えば同期バージョンのメソッドに InputData.GetRows(), Master.GetRows() というのがあるとすると、以下のような感じ。

List<InputData.Row> dataRows; List<Master.Row> mstRows; Parallel.Invoke(() => dataRows = InputData.GetRows(), () => mstRows = Master.GetRows());

【12/27 11:15 訂正】

・・・と思いましたが、非同期バージョンのメソッドしかない場合は TPL ではどうしようもないし、UI がブロックされるのは避けられないし、Task.WhenAll(...) に await を付与すればデッドロックになることはないので、Parallel.Invoke など使わないで、質問者さんの「試したこと」のコードのようにして並列化については OS に任せるのが良さそうと思い直しました。

ただ、下の太字で書いたそもそもの話があって、DB が相手であれば、join した SELECT クエリを作ってそれを一回だけ DB に投げてデータを取得するのが正解だと思います。

非同期版 InputData.GetRowsAsync(), Master.GetRowsAsync() を使って「試したこと」のコードのようにすると、非同期版メソッドの中で await を使っているでしょうから、それと Task.Result がお互い待機しあってデッドロックに陥るかもしれません。(コンソールアプリは仕組みが違っていてデッドロックにならないので注意) 【12/27 11:15 訂正】Task.WhenAll に await が付与されているのを見落としてました。削除します。

(非同期版メソッド InputData.GetRowsAsync(), Master.GetRowsAsync() に同期バージョンがあれば、それを使えば「試したこと」のコードのように WhenAll を使ったコードでもデッドロックの問題は無いと思います。下のサンプルコードの button5_Click メソッドを見てください。ただし、マルチコアが利用されるかどうかが分かりません。サンプルを作って動かしてみるとそんな感じがしますが、確信が持てないです)

ただ、そもそもの話として、相手が DB で Linq to Entities で処理するなら、Linq 式で JOIN した SELECT クエリを生成させて、それを一本だけ DB に投げて目的のデータを取得できるのではないですか? であれば、クエリを 2 つに分けて DB に 2 回クエリを投げるより、その方が早いのではないですか?

以下にご参考までに検証に使ったコードをアップしておきます。.NET Framework 4.8 の Windows Forms アプリです。Windows Forms アプリにしたのは、GUI アプリでないとデッドロックにならないからです。

Work が同期版、WorkAsync が非同期版メソッドです。button4_Click は 2 つの同期メソッドを Parallel.Invoke で実行、button5_Click は 2 つの同期メソッドを Task.Run で実行、button6_Click は同期/非同期を混ぜるとデッドロックになるという例です。

using System; using System.Windows.Forms; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; namespace WinFormsApp1 { public partial class Form11 : Form { public Form11() { InitializeComponent(); } // 同期バージョン private string Work(int n) { string retunVlaue = $"n={n}, TaskID={Task.CurrentId}, start:{DateTime.Now:ss.fff}, "; Thread.Sleep(3000); retunVlaue += $"end:{DateTime.Now:ss.fff}"; return retunVlaue; } // 非同期バージョン private async Task<string> WorkAsync(int n) { string retunVlaue = $"n={n}, TaskID={Task.CurrentId}, start:{DateTime.Now:ss.fff}, "; await Task.Delay(3000); retunVlaue += $"end:{DateTime.Now:ss.fff}"; return retunVlaue; } // 2 つの同期メソッドを Parallel.Invoke で実行。UI はブロックされる private void button4_Click(object sender, EventArgs e) { this.label1.Text = ""; string work0 = "", work1 = ""; Parallel.Invoke( () => work0 = Work(0), () => work1 = Work(1)); this.label1.Text += work0 + Environment.NewLine; this.label1.Text += work1 + Environment.NewLine; } // 2 つの同期メソッドを Task.Run で実行、Task.WhenAll で待機。UI はブロックされる private void button5_Click(object sender, EventArgs e) { this.label1.Text = ""; var work0 = Task.Run(() => Work(0)); var work1 = Task.Run(() => Work(1)); Task.WhenAll(work0, work1); this.label1.Text += work0.Result + Environment.NewLine; this.label1.Text += work1.Result + Environment.NewLine; } // 2 つの非同期メソッドを Task.Run で実行、Task.WhenAll で待機、Task.Result でデッドロック。 // Task.WaitAll ならそこでデッドロックになる。 private void button6_Click(object sender, EventArgs e) { this.label1.Text = ""; Task<string> work0 = WorkAsync(0); Task<string> work1 = WorkAsync(1); Task.WhenAll(work0, work1); // ここでデッドロックになる。コメントアウトを外すと分かる //this.label1.Text += work0.Result + Environment.NewLine; //this.label1.Text += work1.Result + Environment.NewLine; this.label1.Text += "デッドロックになるコードはコメントアウト"; } } }

button4_Click 実行結果:

イメージ説明

button6_Click でなぜデッドロックになるかは以下の記事を見て下さい。

await と Task.Result によるデッドロック
http://surferonwww.info/BlogEngine/post/2020/09/20/deadlock-caused-by-await-and-task-result.aspx

投稿2020/12/26 05:29

編集2020/12/27 02:18
退会済みユーザー

退会済みユーザー

総合スコア0

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

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

Zuishin

2020/12/26 23:35

これ、WhenAll を await してないので質問のコードと違いますよね?
退会済みユーザー

退会済みユーザー

2020/12/26 23:49 編集

button6_Click は質問のコードの「試したこと」の方と同じです。質問のコードの InputData.GetRowsAsync(), Master.GetRowsAsync() 中身は分かりませんが、たぶん中で await を使っていると思われるので、質問者さんのコードの dataRows.Result でデッドロックになると思います。
Zuishin

2020/12/26 23:50

こちらのコードは > Task.WhenAll(work0, work1); await せず投げっぱなしなので次の行がすぐ実行されるためにデッドロックします。 質問のコードは > await Task.WhenAll(dataRows, mstRows); await しているので、Result を呼び出す時には処理が終わっています。
退会済みユーザー

退会済みユーザー

2020/12/26 23:56

失礼しました。見落としてました。今試せる環境がないので、後で実際にサンプルを動かして確認します。
退会済みユーザー

退会済みユーザー

2020/12/27 01:34

Task.WhenAll(work0, work1); に await を付与すればデッドロックにはならないこと確認できました。後で質問欄を修正しておきます。ご指摘ありがとうございました。 Microsoft のドキュメントのタスク並列ライブラリ (TPL) の説明には "The TPL scales the degree of concurrency dynamically to most efficiently use all the processors that are available." とありますが、await Task.WhenAll(...) ではそのあたりは OS 任せになるということでしょうか。 TPL を使った場合 UI がブロックされるのは避けられないようですし、非同期バージョンのメソッドしかない場合は TPL ではどうしようもなさそうですし、await Task.WhenAll(...) を使って後は OS に良しなにやってもらうということにするべきなのかもしれませんね。
woria

2020/12/28 00:22

回答遅くなりました。 仰る通り、現在2つのメソッドはそれぞれSQL Serverに対して全データをSELECTするqueryになっております。 ただ、作成当初その2つのメソッドはSQL Serverを利用せず、元データのCSVやExcelを直接吸い上げたものをListとして活用するもので、SQL Serverに保存する処理を経由するようにした今は、1回のqueryで目的のデータを取得できることに気付いていませんでした。 様々な知見をいただき、ありがとうございます。今後のC#コーディングに活用させていただきます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.36%

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

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

質問する

関連した質問