WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

タスク並列ライブラリ (TPL) その 2

by WebSurfer 2021年7月20日 14:11

注意: 以下は .NET 5.0 のコンソールアプリの例です。.NET 6.0 ではスレッドプールからのスレッドの取得に改善があったようで結果が大幅に異なります (Parallel Link は改善なし)。.NET 6.0 での結果を下の方に追記しておきます。

先の記事「タスク並列ライブラリ (TPL)」で、TPL に代えて、複数のタスクをスレッドプールで実行するように設定し、並列化については OS 任せにするコードを紹介しました。

TPL を使った場合は "使用可能なすべてのプロセッサを最も効率的に使用するようにコンカレンシーの程度を動的に拡大" するそうですが、それとの違いを調べてみました。(独断&自分流の調べ方なのでハズレがあるかも)

(1) TPL, PLINK を使わない場合

下の画像は、TPL, PLINK は使わないで、Task.Delay(3000).Wait() で 3 秒遅延するコードを含む同期メソッドを、Task.Run メソッド を使って 100 個キューに配置し、終了を await Task.WhenAll(...) で待機した結果です。

並列化は OS 任せ

コードはこの記事の下の方に記載したサンプルコードを見てください。その中で、実行対象の同期メソッドが Work、それを 100 個キューに置いてスレッドプールで実行するのが TaskRunAsync メソッドです。

環境は Windows 10 Pro 64-bit、Core i7-9700K 8 コア 8 論理プロセッサ、Visual Studio 2019、.NET 5.0 のコンソールアプリです。

で、TPL を使った場合との比較ですが、同じ同期メソッド Work を 100 個 Parallel.Invoke, Parallel.For, Parallel.ForEach で実行した結果と比べると、全体の実行時間はどれも 25 秒前後でほとんど違いはなかったです。(なぜか Parallel LINK は後述するように期待外れでした)

唯一気になった違いは、上の画像の n = 2, n = 4, n = 18 のように必要以上の時間スレッドを解放できないケースがあるということです。TPL にはそれは無かったです。全体の実行時間が TPL と比べて 1 ~ 2 秒遅かったのはそのせいかもしれません。問題の種を含んでいるということなのでしょうか。

ほかに興味深かったのは、PC のコア数(画像の minWorker: 8 がそれ)まではスレッドプールから一気にスレッドを取得できるが(n = 0 ~ 7)、さらにスレッドを取得しようとすると少し時間がかかる(n = 8, 9, 10, 11)ということでした。

それは CLR スレッドプールの仕様らしいです。ネットで見つけた記事「ThreadPool Growth: Some Important Details」に書いてありましたが 500ms かかるとのことです。(Microsoft の公式文書は見つけられていませんが結果を見る限り間違いなさそう)

500ms の制限は TPL を使用しても同じらしいです。なので、TPL を使用するしないにかかわらず、スレッドプールのスレッドをバースト的に多数使用する場合は設定を変更するのが良さそうです。(設定方法は上に紹介した記事に書いてあります)

以下に、Parallel.Invoke メソッド、Parallel.For メソッド、Parallel.ForEach メソッド、Parallel LINK での結果の画像を貼っておきます。どのようにしたかは下に記載したサンプルコードを見てください。それぞれ実装が違うようで、結果もそれぞれ異なっています。(結果の違いが判るだけで、具体的に中の動きがどう違った結果そうなるのかは分かりませんが)

(2) Parallel.Invoke メソッド

Parallel.Invoke メソッド

(3) Parallel.For メソッド

Parallel.For メソッド

(4) Parallel.ForEach メソッド

Parallel.ForEach メソッド

(5) Parallel LINK

Parallel LINK の場合、全体の実行時間が 42 秒前後となり、TPL と比べて 7 割弱増えてしまいました。実行中の挙動を見ていると、途中で一旦結果が表示されて止まってしまい、何秒かののち再開されて最後まで実行されるという感じです。

Parallel LINK

TPL とは実装が大きく異なるのでしょうか? 前のスレッドで書いたように await Task.Run(() => ... を使っても UI スレッドがブロックされるのは避けられませんでしたし。それとも自分の使い方が間違っているのでしょうか?

以下に上に書いた検証に使用したサンプルコードを記載しておきます。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleAppWenAllParallelFor
{
    class Program
    {
        static async Task Main(string[] args)
        {
            int minWorker; // ワーカースレッドの最小数(PC のコア数と同じ)
            int minIOC;    // 非同期 I/O スレッドの最小数
            ThreadPool.GetMinThreads(out minWorker, out minIOC);
            Console.WriteLine($"minWorker: {minWorker}, minIOC: {minIOC}");

            int maxWorker; // ワーカー スレッドの最大数
            int maxIOC;    // 非同期 I/O スレッドの最大数
            ThreadPool.GetMaxThreads(out maxWorker, out maxIOC);
            Console.WriteLine($"maxWorker: {maxWorker}, maxIOC: {maxIOC}");

            var prog = new Program();

            await prog.TaskRunAsync();
            //await prog.ParallelInvokeAsync();
            //await prog.ParallelForAsync();
            //await prog.ParralelForEachAsync();
            //await prog.PLinkAsync();
        }

        // 同期メソッド
        public string Work(int number)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            DateTime start = DateTime.Now;
            string retunVlaue = $"n = {number}, ThreadID = {id}" +
                                $", start: {start:ss.fff}, ";

            // ここで 3 秒遅延
            Task.Delay(3000).Wait();

            DateTime end = DateTime.Now;
            TimeSpan diff = start - end;
            retunVlaue += $"end: {end:ss.fff}, timespan: {diff:s\\.fff}";
            return retunVlaue;
        }

        // タスク (この記事の例では同期メソッド Work) を 100 個 Task.Run
        // メソッドでキューに配置する。OS がスレッドプールから適宜スレッ
        // ドを取得してキューのタスク実行。await Task.WhenAll ですべての
        // タスクの完了を待機する
        public async Task TaskRunAsync()
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            DateTime start = DateTime.Now;
            Console.WriteLine($"Main Thread ID = {id}, " +
                $"TaskRunAsync 開始: {start:ss.fff}");
            string[] stringResults = new string[100];
            var taskList = new List<Task>();
            for (int i = 0; i < 100; i++)
            {
                int n = i;
                taskList.Add(Task.Run(() => stringResults[n] = Work(n)));
            }

            await Task.WhenAll(taskList);

            foreach (string result in stringResults)
            {
                Console.WriteLine(result);
            }

            DateTime end = DateTime.Now;
            TimeSpan diff = start - end;
            Console.WriteLine($"Main Thread ID = {id}, " +
                $"終了: {end:ss.fff}, 所要時間: {diff:s\\.fff}");
        }

        // タスク (この記事の例では同期メソッド Work) を Parallel.Invoke
        // を使って 100 個実行。Parallel.Invoke の機能により可能な限り
        // 並列実行されるはず
        public async Task ParallelInvokeAsync()
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            DateTime start = DateTime.Now;
            Console.WriteLine($"Main Thread ID = {id}, " +
                $"ParallelInvokeAsync 開始: {start:ss.fff}");
            string[] stringResults = new string[100];
            Action[] actions = new Action[100];
            for (int i = 0; i < 100; i++)
            {
                int n = i;
                actions[n] = () => stringResults[n] = Work(n);
            }

            await Task.Run(() => Parallel.Invoke(actions));

            foreach (string result in stringResults)
            {
                Console.WriteLine(result);
            }

            DateTime end = DateTime.Now;
            TimeSpan diff = start - end;
            Console.WriteLine($"Main Thread ID = {id}, " +
                $"終了: {end:ss.fff}, 所要時間: {diff:s\\.fff}");
        }

        // Parallel.For を使って 100 個実行
        public async Task ParallelForAsync()
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            DateTime start = DateTime.Now;
            Console.WriteLine($"Main Thread ID = {id}, " +
                $"ParallelForAsync 開始: {start:ss.fff}");
            string[] stringResults = new string[100];

            await Task.Run(() => Parallel.For(0, 100, 
                                 (n) => stringResults[n] = Work(n)));

            foreach (string result in stringResults)
            {
                Console.WriteLine(result);
            }

            DateTime end = DateTime.Now;
            TimeSpan diff = start - end;
            Console.WriteLine($"Main Thread ID = {id}, " +
                $"終了: {end:ss.fff}, 所要時間: {diff:s\\.fff}");
        }

        // Parallel.ForEach を使って 100 個実行
        public async Task ParralelForEachAsync()
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            DateTime start = DateTime.Now;
            Console.WriteLine($"Main Thread ID = {id}, " +
                $"ParallelForEachAsync 開始: {start:ss.fff}");
            string[] stringResults = new string[100];

            await Task.Run(() => Parallel.ForEach(Enumerable.Range(0, 100),
                                 (n) => stringResults[n] = Work(n)));

            foreach (string result in stringResults)
            {
                Console.WriteLine(result);
            }

            DateTime end = DateTime.Now;
            TimeSpan diff = start - end;
            Console.WriteLine($"Main Thread ID = {id}, " +
                $"終了: {end:ss.fff}, 所要時間: {diff:s\\.fff}");
        }

        // PLINK を使って 100 個実行
        public async Task PLinkAsync()
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            DateTime start = DateTime.Now;
            Console.WriteLine($"Main Thread ID = {id}, " +
                $"PLinkAsync 開始: {start:ss.fff}");

            var results = await Task.Run(() =>
                          Enumerable.Range(0, 100).AsParallel()
                          .Select(n => Work(n)));

            foreach (string result in results)
            {
                Console.WriteLine(result);
            }

            DateTime end = DateTime.Now;
            TimeSpan diff = start - end;
            Console.WriteLine($"Main Thread ID = {id}, " +
                $"終了: {end:ss.fff}, 所要時間: {diff:s\\.fff}");
        }
    }
}

2022/8/9 追記

.NET 6.0 で上のコードと同じアプリを作成し「(1) TPL, PLINK を使わない場合」と「(2) Parallel.Invoke メソッド」を実行した結果を以下に載せておきます。

Parallel.For, Parallel.ForEach の結果もほぼ同じです。Parallel Link は、理由不明ですが、改善なしでした。

スレッドプールから一度に取得できるスレッド数とそれを超えて追加でスレッドを取得する際に要する時間が改善されたようで、全体の実行時間が .NET 5.0 では 25 秒前後かかっていたものが .NET 6.0 では 9 秒前後と大幅に短縮されています。

(1) TPL, PLINK を使わない場合 (.NET 6.0)

並列化は OS 任せ (.NET 6.0)

(2) Parallel.Invoke メソッド (.NET 6.0)

Parallel.Invoke メソッド (.NET 6.0)

Tags: , , , , ,

.NET Framework

try - catch で OperationCanceledException を捕捉できない

by WebSurfer 2021年7月17日 14:53

タスク並列ライブラリ (TPL) の Parallel.For メソッドの複数の並列処理をキャンセルするコードを書いているときにハマって悩んだので、今後そういうことがないよう備忘録を書いておきます。

Parallel.For ループのキャンセル

ちなみにコードはこの記事の下の方に記載したもので、Microsoft のドキュメント「方法: Parallel.For または ForEach ループを取り消す」を参考にキャンセル処置を実装しました。Windows Forms アプリなので UI スレッドをブロックしないようにしている点が違いますが、基本的には同じです。

で、何にハマったかと言うと、Visual Studio から[デバッグ(D)]⇒[デバッグの開始(S)]でアプリを実行すると try - catch で OperationCanceledException を捕捉できないということです

下の画像を見てください。try - catch 構文で try 句内で発生した OperationCanceledException を catch 句があるにもかかわらず捕捉できていません。(実はそこは思い違いだったのですが。詳細後述)

OperationCanceledException

Visual Studio から[デバッグ(D)]⇒[デバッグなしで開始(H)]で実行すれば上の画像のようなことは起こらず、catch 句で OperationCanceledException を捕捉できます。ということは、先の記事「不正なクロススレッドコールの捕捉」の話と同様にデバッグ実行でないと検出できない不正な何かがあると思い込んでいました。

なので、Unhandled OperationCanceledException when thrown from Parallel.ForEach に書いてあるように ThrowIfCancellationRequested メソッドを try - catch で囲ったり、await Task.Run( async () => ... と async を付与してデバッグ実行でも catch できるように対応してみました。

でも、実はそんなことをする必要はなかったです。(汗) 上の画像は、Visual Studio がデバッグ時にユーザーに便宜(?)を図るために、例外が発生した場所で一旦実行を止めて知らせたのだそうです。続行すれば catch 句まで進んで OperationCanceledException を補足できます。

そのことは Microsoft のドキュメント「例外処理(タスク並列ライブラリ)」の「注意」に以下のように書いてありました:

"[マイ コードのみ] が有効になっている場合、Visual Studio では、例外をスローする行で処理が中断され、"ユーザー コードで処理されない例外" に関するエラー メッセージが表示されることがあります。このエラーは問題にはなりません。 F5 キーを押して続行し、以下の例に示す例外処理動作を確認できます。 Visual Studio による処理が最初のエラーで中断しないようにするには、 [ツール] メニューの[オプション]、[デバッグ] の順にクリックし、[全般] で [マイ コードのみを有効にする] チェック ボックスをオフにします"

試してみましたが確かにその通りでした。

なお、すべてのケースで例外が発生した場所で一旦実行を止めて知らせるというわけではなくて、ある条件の時に限るようです。ある条件とは、多分、呼び出し元と別のスレッドで実行されているタスクで例外がスローされたが、その例外を呼び出し元で catch できるか不明な時ではないかと思われます (だから async を付与すると解決した?)。

検証に使った Windows Forms アプリのコードを以下に載せておきます。デバッグ実行してキャンセルをかけると上の画像のように例外が発生した場所で一旦止まります。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WinFormsTPL
{
    public partial class Form2 : Form
    {
        private int currentProgress = 0;
        private CancellationTokenSource cts;

        public Form2()
        {
            InitializeComponent();

            toolStripStatusLabel1.Text = "";
            toolStripProgressBar1.Value = 0;
        }

        // 進捗をプログレスバーとラベルに表示するコールバック。UIスレッド
        // で呼び出される
        // 【注】Parallel.For は同期メソッドなので、下のコード例のように
        // await Task.Run(() => Parallel.For ...  を使って UI スレッドを
        // ブロックしないようにすること。でないと ShowProgress はキューに
        // 溜るだけで、Parallel.For が終了してから一気に 100% になりプロ
        // グレス表示にならない。
        private void ShowProgress(int percent)
        {
            currentProgress += percent;
            toolStripStatusLabel1.Text = currentProgress + "%完了";
            toolStripProgressBar1.Value = currentProgress;
        }     

        // Parallel.For で複数並列に実行する同期メソッド
        private string  WorkProgress(int number, IProgress<int> progress)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            string retunVlaue = $"n = {number}, ThreadID = {id}" +
                                $", start: {DateTime.Now:ss.fff}, ";

            // ここで 3 秒中断
            Thread.Sleep(3000);

            // 進捗をプログレスバーとラベルに表示
            progress.Report(1);

            retunVlaue += $"end: {DateTime.Now:ss.fff}\r\n";
            return retunVlaue;
        }

        // 画像の [ParallelForProgress] クリックのハンドラ
        // 上の WorkProgress メソッドを Parallel.For で 100 並列実行
        private async void ParallelForProgress_Click(object sender, EventArgs e)
        {
            currentProgress = 0;
            toolStripStatusLabel1.Text = "";
            toolStripProgressBar1.Value = 0;

            int id = Thread.CurrentThread.ManagedThreadId;
            label1.Text = $"UI Thread ID = {id}\r\n";

            // CancellationToken を ParallelOptions 経由で Parallel.For
            // に渡すため、ParallelOptions を初期化
            var option = new ParallelOptions();

            // WorkProgress の戻り値を保持する配列の定義と初期化
            string[] results = new string[100];

            using (cts = new CancellationTokenSource())
            {
                option.CancellationToken = cts.Token;
                var p = new Progress<int>(ShowProgress);

                try
                {
                    // Parallel.For は同期メソッドであることに注意。
                    // UI スレッドをブロックしないよう await Task.Run
                    // を用いてスレッドプールで Parallel.For を実行
                    await Task.Run(() => Parallel.For(0, 100, option,
                        (n) => {
                            results[n] = WorkProgress(n, p);

                            // 以下は無くてもキャンセルされるが、Microsoft
                            // のドキュメントに従って入れておく
                            option.CancellationToken
                                  .ThrowIfCancellationRequested();

                        }), cts.Token);
                }
                catch (OperationCanceledException)
                {
                    toolStripStatusLabel1.Text = "キャンセル";
                }
            }

            foreach (string result in results)
            {
                label1.Text += result;
            }

            // using を抜けて CancellationTokenSource が Dispose されても
            // すぐには null にならないので、再度キャンセルボタンをクリック
            // すると cts.Cancel() で例外がスローされる。その対応
            cts = null;
        }

        // 画像の [Cancel] クリックのハンドラ
        private void Cancel_Click(object sender, EventArgs e)
        {
            if (cts == null) return;

            cts.Cancel();
        }
    }
}

環境は Windows 10 v21H1, Visual Studio Cummunity 2019 v16.10.3 で .NET Framework 4.8 および .NET 5.0 の両方で試しました。

Tags: , , ,

.NET Framework

localhost への同時接続数

by WebSurfer 2021年6月18日 15:03

.NET Framework を使って Web にアクセスする WinForms などのアプリ開発時に、検証用に自分の開発マシンに Web アプリを作ってアクセスすることがあると思います。その際、localhost への要求の場合は同時接続数の制約が外れることに注意が必要という話を書きます。(検証したのは HttpWebRequest のみで HttpClient は未検証。Core はすべて未検証です)

localhost の場合

(元の話は teratail のスレッド「C#にて並列かつ非同期でWebRequestを使用する方法について」です)

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

上の画像がその例です。要求先はローカルの IIS Express で動く検証用の ASP.NET Web アプリで、要求を受けて 10 秒後に OK という文字列を返します。その URL を、この記事の下の方に記載したサンプルコードにあるように、マルチスレッドで HttpWebRequest を使って同時に 5 回要求した結果です。end の時間が 5 つとも同じになっているのに注目してください。

同時接続が 2 つまでに制限されていれば、一度にサーバーに要求が出るのは 2 つまでで、3 つ目以降の要求は 10 秒経って先の応答が戻ってこないと出ないので、2 つおきに end の時間が 10 秒ずつ遅れるという結果になるはずです。そうなっていないのは、localhost への要求なので同時接続数の制限が外れたからです (ServicePoint.ConnectionLimit が Int32.MaxValue になっています)。

自分が使っているレンタルサーバー (当然 localhost ではない) で動く ASP.NET Web Forms アプリに、要求を受けて 10 秒後に Hello World という文字列を返す HTTP ジェネリックハンドラを作って、それを下のサンプルコードで同時に 5 回要求した場合は下のようになります。

surferonwww.info の場合

デフォルトの同時接続数 2 は有効になっていて (ServicePoint.ConnectionLimit が 2 になっています)、2 を超えた分は先の応答が返ってきてからでないと要求が出ず、上の画像の end を見ると分かりますが 2 要求毎に 10 秒ずつ待たされるという結果になりました。

ただし、Fiddler を使うと話が違ってきます。Fiddler は IP アドレス 127.0.0.1 のプロキシなのですが、どこかで IP アドレスから localhost と判定されているようで、同時接続数は制限されないという結果になりました。下の画像がその結果です。

Fiddler 経由で surferonwww.info を要求

ServicePoint.ConnectionLimit を取得するとデフォルトの同時接続数 2 となっていますが、実際の動きを見るとそれは無視されていて (Fiddler の画面を見ていると一度に 5 つ要求が出るのが分かります)、応答が返ってきた時間(end の時間)が 5 つとも同じになっています。

開発時に Fiddler で要求・応答をキャプチャして見るということはよく行うと思いますが、Fiddler の有無で同時接続数が異なるというのも要注意と思いました。

もう一つ、開発マシンのローカル IIS で動く ASP.NET Web Forms アプリに、hosts ファイルで 127.0.0.1 に websiteproject.com というホスト名を付けて、そのホスト名で呼び出せるようにし、それを下のサンプルコードで同時に 5 回要求した場合はどうなるを試してみました。結果は以下の画像の通りです。

websiteproject.com の場合

画像の end を見るとデフォルトの同時接続数 2 は有効になっているようで、2 要求毎に 10 秒ずつ待たされるという結果になっています。要求先の IP アドレスは 127.0.0.1 なのですが、.NET Framework ライブラリは IP アドレスでではなくて localhost という名前で判定しているのでしょうか。

ただし、ServicePoint.ConnectionLimit が Int32.MaxValue になっているのが解せません。

さらに、Fiddler を使うとどうなるかと言うと、上のケースと同様に同時接続数は制限されないという結果になりました。下の画像がその結果です。

Fiddler 経由で websiteproject.com を要求

この時は、ServicePoint.ConnectionLimit が何故か 2 になるのですが、これも解せません。

ソースコードを Microsoft のサイト ServicePoint.csServicePointManager.cs で見ることができるのですが、localhost か否かの判定はどのタイミングでどのようにしているのか、何がどのタイミングで ConnectionLimit プロパティの getter を呼び出して同時接続数を Int32.MaxValue に設定しているのか等々は読み切れませんでした。

ServicePointManager.DefaultConnectionLimit や ServicePoint.ConnectionLimit の値など、いろいろ不可解な動きに見えるのですが、ServicePoint.cs のソースコードのコメントに、

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?)

・・・と書いてある通りで、とにかく localhost はデフォルトでは同時接続数は Int32.MaxValue になるということは間違いなさそうです。

最後に、検証に使った HttpWebRequest のコンソールアプリのコードを載せておきます。Visual Studio 2019 のテンプレートで .NET Framework 4.8 で作ったものです。

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)
        {
            // localhost
            // ローカルの IIS Express で動くASP.NET MVC5 アプリの
            // アクションメソッド。
            // 要求を受けて 10 秒後に OK という文字列を返す
            var uri = "https://localhost:44365/Home/sample";

            // surferonwww.info
            // レンタルサーバーの IIS で動く ASP.NET Web Forms ア
            // プリの HTTP ジェネリックハンドラ。
            // 要求を受けて 10 秒後に Hello World という文字列を返す
            //var uri = "http://surferonwww.info/Test/Sample.ashx";

            // websiteproject.com
            // ローカル IIS で動く ASP.NET Web Forms アプリの HTTP
            // ジェネリックハンドラ。
            // hosts ファイルで 127.0.0.1 に websiteproject.com と
            // いうホスト名を付けたのでそのホスト名で呼び出せる。
            // 要求を受けて 10 秒後に Hello World という文字列を返す
            //var uri = "http://websiteproject.com/Sample.ashx";

            Console.WriteLine($"ServicePointManager.DefaultConnectionLimit = " +
                              $"{ServicePointManager.DefaultConnectionLimit}");
            
            var sp = ServicePointManager.FindServicePoint(new Uri(uri));
            Console.WriteLine($"ServicePoint.ConnectionLimit = " +
                              $"{sp.ConnectionLimit}");

            var encoding = new UTF8Encoding(false);
            var tasks = new List<Task>();

            // 同じ URL を 5 回同時に要求する。実際に Web サーバーに
            // いくつ同時に要求が出るかは ConnectionLimit による (はず)
            foreach (var i in Enumerable.Range(0, 5))
            {
                var task = Task.Run(async () =>
                {
                    // ThreadId と開始時刻
                    int id = Thread.CurrentThread.ManagedThreadId;
                    string start = $" / ThreadID = {id}, " +
                                   $"start: {DateTime.Now:ss.fff}, ";
                    
                    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());

                        // 終了時刻と ServicePoint.ConnectionLimit
                        string end = $"end: {DateTime.Now:ss.fff}";
                        string limit = $", limit: {sp.ConnectionLimit}";
                        text += start + end + limit;

                        Console.WriteLine(text);
                    }
                });

                tasks.Add(task);
            }

            await Task.WhenAll(tasks);

            Console.WriteLine("Finish");
            Console.ReadLine();
        }
    }
}

Tags: , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar