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

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

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

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

Q&A

解決済

3回答

6766閲覧

C#にて並列かつ非同期でWebRequestを使用する方法について

Gowemon

総合スコア2

C#

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

1グッド

0クリップ

投稿2021/06/16 02:41

現在、C# でコンソールアプリケーション(.NET Framework 4.8)を作っています。
このプログラムでは複数のURLへ並列・非同期で問い合わせを行いたいのですが、
並列で処理をすることができずに困っています。

C#のコードは以下のようになります

c#

1static async Task Main(string[] args) 2{ 3 var encoding = new UTF8Encoding(false); 4 var tasks = new List<Task>(); 5 6 // 5つの問い合わせを行う 7 foreach (var i in Enumerable.Range(0, 5)) 8 { 9 var task = Task.Run(async () => 10 { 11 // 応答までに10秒かかるURL 12 var uri = "https://example.com/sample.php"; 13 var request = (HttpWebRequest)WebRequest.Create(uri); 14 using (var response = await request.GetResponseAsync()) 15 using (var stream = response.GetResponseStream()) 16 using (var memory = new System.IO.MemoryStream()) 17 { 18 await stream.CopyToAsync(memory); 19 var text = encoding.GetString(memory.ToArray()); 20 Console.WriteLine(text); 21 } 22 }); 23 24 tasks.Add(task); 25 } 26 27 // すべて終わるまで待機 28 await Task.WhenAll(tasks); 29 30 Console.WriteLine("Finish"); 31 Console.ReadLine(); 32}

サーバー側はテストのため下記のようなコードを設置しています

php

1<?php // https://example.com/sample.php 2sleep(10); 3echo "OK";

上記の場合、感覚的には5つの問い合わせは合計10秒超で終わるイメージなのですが、
コンソールへの出力を見ると、順不同ですが結果はおおよそ10秒ずつ表示され、
すべて終わるまでに合計で50秒かかってしまいます。

上記の処理を並列・非同期で実行する方法についてご存知の方がいらっしゃいましたらご助言をいただけないでしょうか?

dodox86👍を押しています

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

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

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

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

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

guest

回答3

0

ベストアンサー

もしサーバー側に問題がなければServicePointManager.DefaultConnectionLimitを変更してみてください。

投稿2021/06/16 04:58

neconekocat

総合スコア443

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

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

Gowemon

2021/06/16 05:22

回答ありがとうございます。 Mainメソッドの先頭に ServicePointManager.DefaultConnectionLimit = 10; と追記したところ見事に並列動作しました。 ありがとうございました。
neconekocat

2021/06/16 05:53

答えておいてあれですが、MSDNにもある通りそもそも非推奨なクラスのため、特段の理由がない限りはHttpClientでやった方がいいかと思います。
guest

0

【追記・訂正】

下の回答で「期待通り 5 件の要求が並列処理され、5 件の応答が約 10 秒で返ってきます」と書きましたが、それは要求先が localhost だったからでした。

HTTP 1.1 仕様で同時接続は 2 つまでとなっています。HttpWebRequest を使った場合にもその制約が適用されるようになっているようですが、ユーザーが ServicePointManager.DefaultConnectionLimit の設定を変えない場合かつ localhost への要求の場合は制約が外れます(Int32.MaxValue になる)。

ソースコードを見ると(URL 下記)そのコメントに "3. If ServicePoint.DefaultConnectionLimit is set, then take that value" "4. If ServicePoint is localhost, then set to infinite (TO Should we change this value?)" と書いてありますが、試してみるとその通りの動きをしました。

https://referencesource.microsoft.com/#System/net/System/Net/ServicePoint.cs
https://referencesource.microsoft.com/#System/net/System/Net/ServicePointManager.cs

ServicePoint クラスの ConnectionLimit プロパティの中の getter の return 文のところで同時接続数を Int32.MaxValue に設定していました。

ただし、localhost か否かの判定はどのタイミングでどのようにしているのか、何がどのタイミングで ConnectionLimit プロパティの getter を呼び出して同時接続数を Int32.MaxValue に設定しているのかはソースコードからは読み切れてません。

試しに、hosts ファイルで 127.0.0.1 にそれらしいホスト名を設定して、そのホスト名を url に設定した場合は同時接続数は 2 になりました。(と言うことは IP アドレスではなくて localhost という名前で判定?)

しかし、Fiddler を通してみると、Fiddler は IP アドレス 127.0.0.1 のプロキシなのですが、どこかで IP アドレスから localhost と判定されて、同時接続数は制限されないという結果になりました。

依然として ServicePointManager.DefaultConnectionLimit や ServicePoint.ConnectionLimit の値など、いろいろ不可解な動きに見えるのですが、とにかく localhost はデフォルトでは同時接続数は Int32.MaxValue になるということは間違いなさそうです


質問者さんのコードをコピペし、スレッド ID と開始/終了時間が分かるようにコードを追加して試してみましたが、期待通り 5 件の要求が並列処理され、5 件の応答が約 10 秒で返ってきます。

イメージ説明

なのて、tamoto さんが言われるようにサーバー側の問題のようです。サーバー側をチェックしてみてください。

ちなみに、サーバー側は ASP.NET MVC5 で開発マシンの IIS Express で動かしているもので、10 秒待って OK を返すものです。コードは以下の通りです。

public async Task<ActionResult> Sample() { await Task.Delay(10000); return Content("OK"); }

クライアント側は質問者さんのコードをコピペして使いました。それに以下のようにコメントで「// 質問者さんのコードに追加」というコードを、スレッド ID と開始/終了時間が分かるように追加しています。

using System; using System.Threading.Tasks; using System.Collections.Generic; using System.Net; using System.Linq; using System.Text; using System.Threading; namespace ConsoleAppAsync4 { class Program { static async Task Main(string[] args) { var encoding = new UTF8Encoding(false); var tasks = new List<Task>(); // 5つの問い合わせを行う foreach (var i in Enumerable.Range(0, 5)) { var task = Task.Run(async () => { // 質問者さんのコードに追加 int id = Thread.CurrentThread.ManagedThreadId; string start = $" / ThreadID = {id}, start: {DateTime.Now:ss.fff}, "; // 応答までに10秒かかるURL var uri = "https://localhost:44365/Home/sample"; var request = (HttpWebRequest)WebRequest.Create(uri); using (var response = await request.GetResponseAsync()) using (var stream = response.GetResponseStream()) using (var memory = new System.IO.MemoryStream()) { await stream.CopyToAsync(memory); var text = encoding.GetString(memory.ToArray()); // 質問者さんのコードに追加 string end = $"end: {DateTime.Now:ss.fff}"; text += start + end; Console.WriteLine(text); } }); tasks.Add(task); } // すべて終わるまで待機 await Task.WhenAll(tasks); Console.WriteLine("Finish"); Console.ReadLine(); } } }

【追記】

下の 2021/06/16 15:01 の私のコメントで「後で結果の画像を回答欄に貼っておきます」と書いた件です。

試してみましたが、自分のケースでも「ASP.NET でホストされるアプリケーションの場合は10、それ以外の場合は2」のそれ以外の場合に該当し 2 になっていました。でも、そのために秒単位で待たされるということなないです。どのように確認したかというと、

上のコードはそのまま、ServicePointManager.DefaultConnectionLimit の値を調べるため、以下の画像の赤枠の一行を追加。

イメージ説明

結果は以下のようになります。質問者さんのケースのように秒の単位で待たされるということはないです。

イメージ説明

tamoto さんの回答のコメントにある tamoto さんが試した結果も問題なかったようですし、ちょっと不可解ですね。

私の回答のコメントでも書きましたが、Fiddler などのキャプチャツールを使って、5 件の要求が一度に出て行っているかも調べてはいかがですか?

投稿2021/06/16 04:43

編集2021/06/17 07:03
退会済みユーザー

退会済みユーザー

総合スコア0

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

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

Gowemon

2021/06/16 05:04

回答ありがとうございます。 サンプルコードと結果まで掲載していただきありがとうございます。 サーバーは Linux なので apache か nginx が動いていると思われます。 担当者に詳しい設定値を確認してみてもらいます。
退会済みユーザー

退会済みユーザー

2021/06/16 05:18

Fiddler などのキャプチャツールを使って、5 件の要求が一度に出て行っているかも調べてはいかがですか? neconekocat さんの指摘も気になるところです。質問者さんが tamoto さんの回答へのコメントで書かれた結果(2 件ずつ処理されているように見える)も気になるところですので。 ちなみに自分は上のコードを Web アプリがある開発マシンでデフォルトで実行しました(なので、ServicePointManager.DefaultConnectionLimit とかの設定は何もしておらず、関係ないかも知れませんが)。
Gowemon

2021/06/16 05:27

nekonekocat さんにご指摘された ServicePointManager.DefaultConnectionLimit を追記したところ想定通りの挙動となりました。様々な情報をご教示いただきありがとうございました。 SurferOnWww さんの環境で問題なく動作した件ですが、 microsoftのドキュメントに ASP.NET の場合はデフォルト値が10、それ以外は2になると記述がありましたので、SurferOnWww さんの環境ではデフォルト値が10で動作していて、私の環境では2で動作していたのかもしれません。 https://docs.microsoft.com/ja-jp/dotnet/api/system.net.servicepointmanager.defaultconnectionlimit
退会済みユーザー

退会済みユーザー

2021/06/16 05:40

> microsoftのドキュメントに ASP.NET の場合はデフォルト値が10、それ以外は2になると記述がありましたので Microsoft のドキュメントには「既定の接続数は、ASP.NET でホストされるアプリケーションの場合は10」と書いてあるのですが、自分が試したコードでは要求を出す方は普通の .NET のコンソールアプリで ASP.NET でホストされているわけではないです。tamoto さんの回答のコメントでも問題なかったようですし、ちょっと不可解ですね。
退会済みユーザー

退会済みユーザー

2021/06/16 06:01

試してみましたが、自分のケースでも「ASP.NET でホストされるアプリケーションの場合は10、それ以外の場合は2」のそれ以外の場合に該当し 2 になっていました。でも、そのために待たされるということなないです。後で結果の画像を回答欄に貼っておきます。
dodox86

2021/06/16 06:15 編集

Gowemonさんの[2021/06/16 14:27]のコメントより: > microsoftのドキュメントに ASP.NET の場合はデフォルト値が10、それ以外は2になると記述がありましたので、SurferOnWww さんの環境ではデフォルト値が10で動作していて、私の環境では2で動作していたのかもしれません。 回答、コメントはしませんでしたが興味深かったので私の方でも試していました。サーバー側をJavaサーブレット(tomcat9+apache2)で試していたのですが、同時接続数が最大で2だったのでおかしいと思い、私もサーバー側、tomcat側の問題だろうと思って調べていたのですが、ServicePointManager.DefaultConnectionLimit の変更で対応できました。ASP.NET以外では本当に「2」のようです。(一部修正済)
退会済みユーザー

退会済みユーザー

2021/06/16 06:20

dodox86 さん> > 本当に「2」のようです。 自分が試したのはクライアント側は ASP.NET に関係なくて、回答欄に追記したように「2」なのですが、秒単位で待たされるということは無かったです。自分の環境では現象が再現できないので調べようがないのですが・・・
neconekocat

2021/06/16 06:35

逆にクライアント側のソースで最初に2を設定したら接続数が制限されませんか?ソース忘れたのですが明示的に変更しない場合は無視される的な事を聞いた記憶が。。。
dodox86

2021/06/16 06:39 編集

@SurferOnWwwさん 私のコメントは「microsoftのドキュメントに ASP.NET の場合はデフォルト値が10、それ以外は2になる」とのコメントがあったので、そのことを受けて当方で試したことの事実を述べただけのことでした。特段、SurferOnWwwさんへの意見やコメントのつもりではありませんでした。混乱させたのであればごめんなさい。 明確にするためにこちらで試した状況を示しておきます。 a. サーバー側 GETリクエストで10秒待機したのち、HTTPレスポンスを返すJavaサーブレットを用意。(リモートサーバーはtomcat9/apache2) b. クライアント側: 質問者さんのコード(Visual Studio 2019でビルド、Windows10上で動作) ・ServicePointManager.DefaultConnectionLimit をセットしない場合 --> サーバー側には2つずつしか同時刻に接続してこない。この接続(リクエスト~レスポンス)が終わらない限り次のリクエストは来ない。 ・ServicePointManager.DefaultConnectionLimit に20をセット --> サーバー側には同時に5つ接続してくる。なので、ほぼ10秒で同時に全部終わる。ちなみにServicePointManager.DefaultConnectionLimit にセットする前に読み出してみると、2が返る。(ドキュメント通り)
退会済みユーザー

退会済みユーザー

2021/06/16 07:34

neconekocat さん> > 逆にクライアント側のソースで最初に2を設定したら接続数が制限されませんか? その通りでした。 自分の Windows 10 環境の .NET 4.8 コンソールアプリでは、ServicePointManager.DefaultConnectionLimit で取得できる値は 2 なのですが、実は 2 ではなくて(少なくとも 5 以上)、そのため待たされるとことがなかったということのようです。 ServicePointManager.DefaultConnectionLimit = 2; と明示的に設定するとホントに 2 になって、結果、同時要求数は 2 に制限され、2 を超えた分は先の応答が返ってきてからでないと要求が出せず、上のサンプルコードでは 2 要求毎に 10 秒ずつ待たされるという結果になりました。 同時接続は 2 つまでというのは HTTP 1.1 仕様で、それゆえ ServicePointManager.DefaultConnectionLimit はデフォルトで 2 ということのようですが、明示的にコードで 2 に設定しないとデフォルトのはずの 2 にならないというのが解せませんが・・・ .NET のバージョンアップでそのあたりが違ってきたのでしょうかね。
退会済みユーザー

退会済みユーザー

2021/06/16 07:48 編集

dodox86 さん> 上の neconekocat さんへのレスで書きましたが、自分の Windows 10 環境の .NET 4.8 コンソールアプリでは、ServicePointManager.DefaultConnectionLimit = 2 と明示的に設定しないと、実はデフォルトのはずの 2 ではなくて少なくとも 5 以上になっていたようです。 dodox86 さんの場合は何も設定しなくとも 2 になっていたのですよね。違いがあるとすると .NET のバージョンぐらいしか思い当たりませんが、dodox86 さんの場合の .NET のバージョンを教えていただけませんか。
dodox86

2021/06/16 08:01

> dodox86 さんの場合の .NET のバージョンを教えていただけませんか。 コンソールアプリのプロジェクトの設定、対象のフレームワークで「.NET Framework 4.8」を設定しています。
退会済みユーザー

退会済みユーザー

2021/06/16 08:04

返答いただきありがとうございます。.NET のバージョンは自分と同じですね。とすると何が違うのか、もう少し調べてみます。
neconekocat

2021/06/16 08:38

軽く調べた感じだと、ローカルアドレス相手だと無制限(int.MaxValue)を初期値としてそうな感じでした。
退会済みユーザー

退会済みユーザー

2021/06/16 09:34

レスをありがとうございます。このスレッドの URL で試してみましたがデフォルトの制限 2 は有効のようで、Localhost は制限が外れるということのようですね。
guest

0

こんにちは。

クライアント側のコードは問題ないように思います。
質問のコードは意図している通りに5件のリクエストを並列に処理しています。
なので、もしこれで50秒かかるのであれば、サーバ側に原因がある可能性が高いです。
PHP は全く詳しくないのですが、リクエストを一つずつ処理している可能性はないでしょうか。
サーバ側のリクエスト・レスポンスのログを確認してみてはいかがでしょうか。

投稿2021/06/16 03:16

tamoto

総合スコア4228

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

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

Gowemon

2021/06/16 04:39

回答ありがとうございます。 サンプルコードとして提示したPHPは並列動作していることを確認しやすくするために用意したもので、 これがhtmlファイルでも画像ファイルであっても同様に並列動作をしてくれません。 試しにこの質問ページ(https://teratail.com/questions/344276)に差し替えても動作はかわりませんでした。
Gowemon

2021/06/16 04:50

改めてサーバーサイドの設定などを見直してみます。失礼しました。
tamoto

2021/06/16 04:58

質問のコードに開始時/終了時のメッセージ出力だけ追加し、この質問ページを読み込んでみましたが、以下のように並列にリクエストが処理されていることが確認できました。参考までに。 Req[2] Started at: 06/16/2021 13:55:21 Req[3] Started at: 06/16/2021 13:55:21 Req[1] Started at: 06/16/2021 13:55:21 Req[0] Started at: 06/16/2021 13:55:21 Req[4] Started at: 06/16/2021 13:55:21 Req[1] Ended at: 06/16/2021 13:55:23 Req[3] Ended at: 06/16/2021 13:55:23 Req[4] Ended at: 06/16/2021 13:55:23 Req[0] Ended at: 06/16/2021 13:55:23 Req[2] Ended at: 06/16/2021 13:55:23 Finish
Gowemon

2021/06/16 05:07 編集

ありがとうございます。 SurferOnWww さんからご提示いただいたコードに書き換えた状態ですが私の場合は下記のように結果が表示されます。 OK / ThreadID = 4, start: 31.172, end: 34.591 OK / ThreadID = 6, start: 31.173, end: 34.591 OK / ThreadID = 5, start: 31.173, end: 37.613 OK / ThreadID = 8, start: 31.173, end: 37.613 OK / ThreadID = 3, start: 31.143, end: 40.614 Finish ※テストのためsleepを2秒にしました。
Gowemon

2021/06/16 05:24

nekonekocat さんにご指摘された ServicePointManager.DefaultConnectionLimit を追記したところ想定通りの挙動となりました。様々な情報をご教示いただきありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.37%

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

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

質問する

関連した質問