C# の async/await のスレッド ID の変更

    

1. 簡単スタート

    Console.WriteLine($"主线程开始ID:{Thread.CurrentThread.ManagedThreadId}");//a
    await Task.Delay(100);//c
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//b


    結果:
        メインスレッド開始 ID: 1
        メインスレッド終了 ID: 4        
    
    1. 質問: async/await は新しいスレッドを作成しますか?
        回答: async と await は新しいスレッドを直接作成しませんが、非ブロッキングの非同期操作を実現するために非同期メカニズムを使用します。
        
            C# の async および await キーワードは新しいスレッドを作成しません。これらは実際には、非同期プログラミング用の糖衣構文です。

            async キーワードを使用してメソッドを変更すると、そのメソッドを非同期メソッドとして扱うことができます。非同期メソッド内で await キーワードを使用すると、他の非同期操作の完了を待つことができます。

            await キーワードが出現すると、非同期メソッドはスレッドをブロックせずに一時的に中断し、現在のスレッドの制御を放棄します。待機していた非同期操作が完了すると、非同期メソッドは実行を再開し、結果を返します。

            ほとんどの場合、非同期操作は新しいスレッドを作成しませんが、I/O 完了ポートまたはその他の非同期メカニズムを利用して非同期操作を実装します。これにより、追加のスレッドの作成が回避され、プログラムのパフォーマンスとリソースの使用率が向上します。

           Task.Run などのメソッドを使用して同期ブロック操作をラップすると、その操作が新しいスレッドで実行される可能性があることに 注意してください。この目的は、メインスレッドのブロックを避けるために、ブロッキング操作を非同期操作に変換することです。


    2. 質問: 非同期メソッドが一時的にハングするとはどういう意味ですか?
        回答: await Task.Delay(100) が発生すると、非同期メソッドは一時的に中断され、現在のスレッドの制御を放棄します。ここでの一時停止は、スレッドが一時停止またはブロックされることを意味するのではなく、非同期メソッドが一時的に実行を停止し、それを呼び出したスレッド (メイン スレッド) に制御を返すことを意味します。
        
            await Task.Delay(100) が見つかると、指定された時間 (ここでは 100 ミリ秒) 後に完了する遅延タスクが作成されます。次に、非同期メソッドは、タスクの完了後に次のステップに進むようにタスクに指示するコールバック関数を登録します。
            一時停止期間中、非同期メソッドはスレッド リソースを占有せず、スレッドが他のタスクを実行できるようにします。これにより、プログラムの同時実行性とリソースの使用率が向上します。
            
            遅延タスクが完了すると、非同期メソッドが起動され、後続のコードの実行を継続します。このとき、遅延操作を実行する新しいスレッドを作成する代わりに、非同期メカニズムを使用してノンブロッキング遅延操作が実装されます。
            
            具体的には、非同期メソッドが await Task.Delay(100) に遭遇すると、遅延したタスクを .NET ランタイムのタスク スケジューラ (Task Scheduler) に渡して管理します。タスク スケジューラは、遅延したタスクを待機キューに入れ、他のタスクの実行を続けます。
            
            指定された時間 (100 ミリ秒) が経過すると、タスク スケジューラは遅延タスクを完了としてマークし、準備完了キューに追加します。スケジューラがタスクをスケジュールすると、非同期メソッドに実行を継続し、元のスレッド (メイン スレッド) に戻るように通知します。
            
            つまり、非同期メソッドの一時停止は、スレッドの一時停止やブロックではなく、実行を一時的に停止し、現在のスレッドの制御を放棄することです。一時停止中、スレッドは他のタスクを実行できます。非同期メソッドは、非同期メカニズムを通じてノンブロッキングの遅延操作を実装し、現在のスレッドの制御を放棄し、遅延タスクの完了後に実行を継続します。
        
        
    3. 質問: await に遭遇したときにメインスレッドは何をしていますか? 泥の中で遊んでいますか?
        回答: はい、ブロックしませんが、他のことを実行します。
        
            await キーワードに遭遇すると、メイン スレッドは一時的に中断されます (中断ポイント)。これにより、メイン スレッドの実行はブロックされませんが、現在のスレッドの制御が放棄され、メイン スレッドが他のタスクを実行できるようになります。
            
            同時に、await キーワードは非同期操作をタスク スケジューラに渡して管理します。タスク スケジューラは、現在のスレッド プールのステータスとスケジュール ポリシーに基づいて、非同期操作を適切なスレッドに割り当てて実行します。
            
            非同期操作が完了すると、タスク スケジューラは非同期メソッドに実行を継続するように通知します。このとき、スレッド切り替えが発生する可能性があり、残りのコードを実行するスレッドは、以前に非同期操作を実行したスレッドである場合もあれば、別のスレッドである場合もあります。
            
            このメカニズムにより、非ブロック方式で非同期メソッドを実行できるようになり、メインスレッドは非同期操作が完了するまで待機している間も他のタスクの実行を継続できるため、プログラムの同時実行性と応答性が向上します。非同期メソッドの一時停止と再開はタスク スケジューラによって管理および制御され、特定のスレッドのスケジューリングと切り替えメカニズムは .NET ランタイムによって処理されることに注意してください
            
            。開発者はスレッドの作成と管理に明示的に焦点を当てる必要はありませんが、async と await を使用することで簡潔で明確な非同期コードを作成できます。

    
    4. 質問: await もスレッドを開く必要がありますか?
        回答: 必ずしもそうではありません。ほとんどの場合、非同期操作では新しいスレッドは作成されませんが、非同期メカニズム (I/O 完了ポートなど) を使用して、ノンブロッキングの非同期操作が実装されます。
        
            非同期メカニズムを使用すると、新しいスレッドを作成せずに、ブロッキング I/O 操作を非同期操作に変換できます。これにより、スレッドの作成と破棄が回避され、プログラムのパフォーマンスとリソースの使用率が向上します。
            
            スレッド プール内のスレッドに加えて、スケジューラはイベント トリガーやタイマーなどの他の実行コンテキストを使用して非同期操作を実行することもできます。この場合、スケジューラは非同期操作をイベント キューまたはタイマー キューに追加し、適切な時間にイベントまたはタイマーをトリガーして非同期操作を実行します。
            
            ただし、非同期操作によって新しいスレッドが作成される場合があります例えば:

            (1) Task.Run などのメソッドを使用する:
            Task.Run などのメソッドを使用して同期ブロッキング操作をラップすると、新しいスレッドで実行される可能性があります。この目的は、メインスレッドのブロックを避けるために、ブロッキング操作を非同期操作に変換することです。

            (2) カスタム スレッド プール:
            場合によっては、開発者はスレッド プールをカスタマイズして、非同期操作の実行を制御できます。これには、特定のニーズを満たすためのスレッドの作成と管理が含まれる場合があります。

            新しいスレッドを作成すると、システム リソースのオーバーヘッドが増加する可能性があり、スレッドの同期と管理が必要になることに注意してください。したがって、非同期操作を設計および実装する場合は、実際の状況に基づいて適切なアプローチを選択し、パフォーマンス、リソース使用率、コードの複雑さのバランスをとることが重要です。
            
            要約すると、ほとんどの場合、非同期操作では新しいスレッドは作成されませんが、非ブロッキング操作を実現するために非同期メカニズムが使用されます。ただし、場合によっては、特定のニーズを満たすために、非同期操作を実行する新しいスレッドの作成が必要になる場合があります。
    
    
    5. 質問: await が完了した後、メインスレッドの ID が 1 にならない場合がありますか?
        答え: はい。
        
            await キーワードに遭遇すると、メイン スレッドは一時的に中断されます (中断ポイント)。これにより、メイン スレッドの実行はブロックされませんが、現在のスレッドの制御が放棄され、メイン スレッドが他のタスクを実行できるようになります。
            
            同時に、await キーワードは非同期操作をタスク スケジューラに渡して管理します。タスク スケジューラは、現在のスレッド プールのステータスとスケジュール ポリシーに基づいて、非同期操作を適切なスレッドに割り当てて実行します。
            
            非同期操作が完了すると、タスク スケジューラは非同期メソッドに実行を継続するように通知します。このとき、スレッド切り替えが発生する可能性があり、残りのコードを実行するスレッドは、以前に非同期操作を実行したスレッドである場合もあれば、別のスレッドである場合もあります。
            
            このメカニズムにより、非ブロック方式で非同期メソッドを実行できるようになり、メインスレッドは非同期操作が完了するまで待機している間も他のタスクの実行を継続できるため、プログラムの同時実行性と応答性が向上します。
            
            具体的
            には、非同期操作が完了すると、タスク スケジューラは非同期メソッドに実行を継続するよう通知します。具体的には、実行権を前のスレッドから元の一時停止ポイントのコードに切り替えてから、次のコードの実行を継続します。
            
            非同期メソッドでは、await キーワードが出現すると、await の後のコードが継続としてカプセル化され、非同期操作の完了イベントに登録されます。
            
            非同期操作が完了すると、タスク スケジューラは継続を準備完了キューに追加し、スケジュールされた実行を待ちます。スケジューラが継続をスケジュールすると、非同期メソッドに実行を継続し、元の一時停止ポイントに戻るように通知します。
            この通知は、スレッドの切り替えおよびスケジューリングのメカニズムを通じて実装されます。具体的には、タスク スケジューラは利用可能なスレッド (以前に非同期操作を実行したスレッドである場合もあれば、別のスレッドである場合もあります) を選択し、そのスレッドに実行権を譲渡します。このようにして、非同期メソッドは await 後もコードの実行を続けることができます。
            
            非同期メソッドの継続はすぐには行われず、スケジューラがスレッドを選択して割り当てた後に行われることに注意してください。特定のスレッドのスケジューリングおよび切り替えメカニズムは、.NET ランタイムおよびタスク スケジューラによって処理されるため、開発者はそれを明示的に管理および制御する必要はありません。
            
            つまり、非同期操作が完了すると、タスク スケジューラはスレッド切り替えおよびスケジューリング メカニズムを通じて実行を元の一時停止ポイントに戻し、次のコードの実行を継続するように非同期メソッドに通知します。これにより、ノンブロッキングの非同期操作とコードの順次実行が可能になります。
    

    6. 質問: 上記の await に対する答えはありますか?
        回答: はい、上記のことから、c でハングし、メイン スレッドが混乱に陥っていることがわかります。この遅延はタスク スケジューラに渡されます (必ずしも作成スレッドである必要はなく、イベント コールバック メカニズムである場合もあります)。遅延が完了すると、遅延が戻り、タスクがスケジュールされます。サーバーは利用可能なスレッドを選択します (メイン スレッドである可能性もあれば、前の非同期操作スレッドである可能性もあり、別の新しいスレッドである可能性もあります。それは誰にもわかりません)。 、リーダーに従います)、ポイント c の後ろでコードの実行を続けます。したがって、d の ID はランダムであり、誰も確実に知ることはできません。
    
    
    7. 質問: コンテキストとは何ですか?
        回答: 人間の用語では、コンテキストとは、コードが実行される環境と状態を指します。これには、スレッド スケジューラ、同期コンテキスト、同期コンテキスト フローなどの実行関連の情報が含まれています。
            たとえば、仕事をする場所やシーン、環境などです。コンピューター、ペン、机、オフィスなどがあります。
    
    
    8. 質問: すべてのスレッドにはコンテキストがありますか?
        回答: 非同期プログラミングでは、スレッドの実行時にコンテキストが存在します。コンテキストは、スレッド スケジューリング、同期コンテキスト、同期コンテキスト フローなどを含む実行環境とステータスを提供します。
            ことわざにあるように、すべての魚には独自の生活環境があります。
    
    
    9. 質問: コンテキストの切り替えはリソースを消費しますか?
        答え: そうです。
            スレッド コンテキストの切り替えには、スレッドの切り替え、コンテキストの保存と復元などのオーバーヘッドがかかる場合があります。これは、スレッドが異なれば実行環境や状態も異なる可能性があり、正しい実行を保証するために追加の操作が必要になるためです。
            よく言われるように、もともと場所 A で働いていて、現在場所 B に移動した場合、当然、移動、整理、掃除などが必要になり、必ず時間がかかります。
        
  


2. 別の非同期を追加します

    Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
    Task task = Task.Run(() =>
    {
        Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
        Thread.Sleep(10);//c
        Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
    });
    await Task.Delay(100);//e
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f
    Console.ReadKey();    


    結果:
        メインスレッド開始 ID: 1
        非同期スレッド ID 開始: 3
        非同期スレッド ID 終了: 3
        メインスレッド終了 ID: 4    
    a のメインスレッド ID は 1 で、新しい非同期スレッドタスクを開き、フラッシュで e を実行します。別の非同期スレッドですが、待機があります。したがって、f はランダムであり、1 になることも 4 になることもありますが、現時点では 3 が c によって占有されているため、3 になることはできません。
    b と d については、c が同期スレッドであるため、b と d は同じスレッドで実行され、ID は 3 と同じになります。
    
    
    修正 1: C の 10 ミリ秒を 1000 ミリ秒に変更します。

    Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
    Task task = Task.Run(() =>
    {
        Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
        Thread.Sleep(1000);//c
        Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
    });
    await Task.Delay(100);//e
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f
    Console.ReadKey();    


    結果:
        メインスレッド開始 ID: 1
        非同期スレッド ID 開始: 3
        メインスレッド終了 ID: 4
        非同期スレッド ID 終了: 3    
    同様に、b と d は依然として同じスレッド内にあり、同じである必要があるため、両方とも 3 になります。
    e の後の f の ID はランダムですが、3 にすることはできません。c では 3 がまだ占有されているためです。
    
    
    修正 2: e を thread.Sleep(1000) に変更します。

    Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
    Task task = Task.Run(() =>
    {
        Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
        Thread.Sleep(10);//c
        Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
    });
    Thread.Sleep(1000);//e
    Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f


    結果:
        メインスレッド開始 ID: 1
        非同期スレッド ID 開始: 3
        非同期スレッド ID 終了: 3
        メインスレッド終了 ID: 1    
    bcd は 3 と同じです
    。e は同期です。メインスレッドの実行IDは1なので、後続のfも1になります。
    


3. 新しく追加された非同期で待機する


    1. 2 つのスレッド。1 つ目は非同期で、2 つ目は同期です。

        Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
        Task task = Task.Run(async () =>
        {
            Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
            await Task.Delay(10);//c
            Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
        });
        Thread.Sleep(1000);//e
        Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f    


        結果:
            メインスレッド開始 ID: 1
            非同期スレッド ID 開始: 3
            非同期スレッド ID 終了: 4
            メインスレッド終了 ID: 1
        e の同期スレッドはメインスレッド内にあるため、f に到達するとメインスレッドになります。
        b で Task.Run によって適用されたスレッドの ID は 3 です。c の後、元の ID 3 は一時停止されます。遅延が完了した後、後続のコードが再開されると、スケジューラは後続の d を実行するスレッドを選択します。したがって、d の ID はランダムですが、1 にすることはできません。 2.
        
        
    2 つのスレッド。最初のスレッドは非同期で、2 番目のスレッドは非同期で待機します。

        Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
        Task task = Task.Run(async () =>
        {
            Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
            await Task.Delay(10);//c
            Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
        });
        await Task.Delay(1000);//e
        Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f


        結果: 
            メインスレッド開始 ID: 1
            非同期スレッド ID 開始: 3
            非同期スレッド ID 終了: 5
            メインスレッド終了 ID: 3    
        b の非同期スレッド ID は 3 です。C を通過すると、d はランダムで 5 と表示されます
        。戻るとき、b と d が使用していたスレッド (3 と 5) はスレッド プールに戻されています。つまり、スレッド プールは再び e の後ろに 1、3、5 などを割り当てる可能性があるため、ここでの表示は3. 上記を変更し
        
    
    、遅延を調整して位置 c を長くします。

        Console.WriteLine($"主线程开始ID:{Environment.CurrentManagedThreadId}");//a
        Task task = Task.Run(async () =>
        {
            Console.WriteLine($"异步线程ID开始:{Environment.CurrentManagedThreadId}");//b
            await Task.Delay(1000);//c
            Console.WriteLine($"异步线程ID结束:{Environment.CurrentManagedThreadId}");//d
        });
        await Task.Delay(10);//e
        Console.WriteLine($"主线程结束ID:{Environment.CurrentManagedThreadId}");//f


        結果:
            メインスレッド開始 ID: 1
            非同期スレッド ID 開始: 3
            メインスレッド終了 ID: 5
            非同期スレッド ID 終了: 6
        b で割り当てられた ID は 3 なので、c で待機し、d で割り当てられた ID をランダムに取得します。 1000 ミリ秒後、つまりすべてのタスクが実行された後、それのみが実行されるため、そのランダムな割り当ては 3、5、6 などの可能性があります。
        e の後、戻るとき、b は (中断されていても) 3 を占有するため、スケジューラーが f でスレッド ID をランダムに 3 に割り当てることは不可能であるため、上で割り当てられたものは 5 になります。


    
4.Configureawaitの無効化

    Console.WriteLine($"主线程开始ID:[{Environment.CurrentManagedThreadId}]");//a
    await Task.Run(async () =>
    {
        Console.WriteLine($"异步线程开始ID:[{Environment.CurrentManagedThreadId}]");//b
        await Task.Delay(1000);//c
        Console.WriteLine($"异步线程结束ID:[{Environment.CurrentManagedThreadId}]");//d
    }).ConfigureAwait(true);//e
    Console.WriteLine($"主线程结束ID:[{Environment.CurrentManagedThreadId}]");//f


    結果:
        メインスレッド開始 ID: [1]
        非同期スレッド開始 ID: [3]
        非同期スレッド終了 ID: [4]
        メインスレッド終了 ID: [4]
    上記 b の ID の結果は 3 で、c の次は d になります。ランダムに割り当てられます。前回
    の await により、位置 f もランダムに割り当てられますが、最後に実行されるため、ID は任意の可能性があります (スケジューラの割り当てによって異なります)。
    
    ここでは、Configure が True であっても False であっても、await で終了してメインスレッドのコンテキストに戻る必要があるため、「失敗」することが説明されています。
    

    Task.Run は非同期操作をスレッド プールに入れて実行しますが、await は非同期操作が完了する前にメイン スレッドをブロックします。非同期操作が完了すると、待機後にメイン呼び出しスレッドに戻ってコードを実行しようとします。e のパラメータが true か false かに関係なく、非同期操作が完了すると、メイン呼び出しスレッドに戻り、回復中に d または f でコードを実行しようとします。
    
    
    
    質問: d と f のスレッドのほとんどが同じなのはなぜですか?
    回答: これは絶対的なものではありません。最適化の確率によれば、これが当てはまる可能性があります。
        スケジューラは通常、実行権限を完了したばかりの非同期スレッドに戻し、最初に中断されたコードの実行を継続しようとします。このアプローチにより、スレッド切り替えとコンテキスト切り替えのコストが削減され、実行効率が向上します。
        
        非同期タスクが完了すると、スケジューラは次の要素を考慮して、実行権限を完了したばかりの非同期スレッドに戻すかどうかを決定します。

        (1) 非同期スレッドの可用性:
        完了したばかりの非同期スレッドがまだ使用可能な場合、スケジューラはスレッド切り替えのオーバーヘッドを回避できるため、後続のコードを優先して実行します。

        (2) 非同期スレッドの負荷:
        完了したばかりの非同期スレッドが現在他のタスクを実行している場合、スケジューラは負荷を分散するためにアイドル状態のスレッドを選択して後続のコードを実行することがあります。

        (3) コンテキスト切り替えのコスト:
        完了したばかりの非同期スレッドのコンテキストに切り替える方が、他のスレッドのコンテキストに切り替えるよりも安価な場合、スケジューラは後続のコードを実行することを優先する可能性があります。

        特定のスケジューリング ポリシーと動作は、スケジューラの実装と構成によって異なることに注意してください。スケジューラが異なれば、最適化戦略や動作も異なる場合があります。したがって、実際のアプリケーションでは、完了したばかりの非同期スレッドに実行権がすぐに戻されない例外が発生する可能性があります。
    
    
    
    最適化を変更し、待機を追加します。

    Console.WriteLine($"主线程开始ID:[{Environment.CurrentManagedThreadId}]");//a
    await Task.Run(async () =>
    {
        Console.WriteLine($"异步线程开始ID:[{Environment.CurrentManagedThreadId}]");//b
        await Task.Delay(1000);//c
        Console.WriteLine($"异步线程结束ID:[{Environment.CurrentManagedThreadId}]");//d
    }).ConfigureAwait(true);//e
    await Task.Delay(1000);
    Console.WriteLine($"主线程结束ID:[{Environment.CurrentManagedThreadId}]");//f    


    結果:
        メインスレッド開始 ID: [1]
        非同期スレッド開始 ID: [3]
        非同期スレッド終了 ID: [4]
        メインスレッド終了 ID: [3]    
    f が 3 になります。デバッガは常に現在の Youxian の ID と比較しているようです (おおよそ1位、3位、4位指名だと思われます)。
    
    そして、最も珍しいのは、約 10 回実行した後、最終的に珍しいスクリーンショットを見つけたことです。
  


    最適化にはルールがあるため、勝つ確率は高いですが、絶対的なものではないことを再度証明します。
    

おすすめ

転載: blog.csdn.net/dzweather/article/details/132818166