(19)タスク非同期:タスク作成、戻り値、例外キャプチャ、タスクキャンセル、一時変数

    

1.タスクタスクの作成


   1.インターフェイス ボタンと情報の 4 つの方法で作成します。
      


        コード

        private void BtnStart_Click(object sender, EventArgs e)
        {
            Task t = new Task(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]new Task.---1");
            });
            t.Start();

            Task.Run(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]Task.Run.---2");
            });

            TaskFactory tf = new TaskFactory();
            tf.StartNew(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]TaskFactory.---3");
            });

            Task.Factory.StartNew(() =>
            {
                DisplayMsg($"[{Environment.CurrentManagedThreadId}]Task.Factory.---4");
            });
        }

        private void DisplayMsg(string s)
        {
            if (TxtInfo.InvokeRequired)
            {
                BeginInvoke(new Action(() =>
                {
                    TxtInfo.AppendText($"{s}\r\n");
                }));
            }
            else
            {
                TxtInfo.AppendText($"{s}\r\n");
            }
        }


    
    
    2.Environment.CurrentManagedThreadId と Thread.CurrentThread.ManagedThreadID の違いは何ですか?
        回答: つまり、Environment.CurrentManagedThreadId を使用します
            。Environment.CurrentManagedThreadId と Thread.CurrentThread.ManagingThreadID は両方とも、現在のスレッドの一意の識別子 (スレッド ID) を取得するために使用されますが、いくつかの違いがあります。
            (1)Environment.CurrentManagedThreadId はどこからでもアクセスできる静的プロパティですが、Thread.CurrentThread.ManagedThreadID は現在のスレッドのインスタンス上でのみアクセスできるインスタンス プロパティです。
            (2)Environment.CurrentManagedThreadId は .NET Framework 4.0 以降で導入されましたが、Thread.CurrentThread.ManagingThreadID はそれ以前のバージョンで導入されました。
            (3)Environment.CurrentManagedThreadId は int 型のスレッド ID を返し、Thread.CurrentThread.ManagedThreadID は IntPtr 型のスレッド ID を返します。
            全体として、どちらも現在のスレッドの一意の識別子を取得するために使用できますが、特にマルチスレッド プログラミングでは、Environment.CurrentManagedThreadId の方が便利で使いやすいです。
            
            警告します:上記の 4 つはすべてデリゲートであるため、DisplayMsg にパラメータ {Environment.CurrentManagedThreadId} を置かないでください。抽出されたスレッドは現在のスレッドです。デリゲートされると、実行は UI スレッド上で行われます。Display に配置されると、すべてのスレッドが実行されます。 3. 上記 4 つの非同期
    
    
    操作の違いは何ですか?
        回答: タスクを作成および開始するには 4 つの方法があり、違いは次のとおりです:
            (1) new Task(); task.Start();:これはタスクを作成および開始する最も基本的な方法です。まず、new Task() を使用して新しいタスク オブジェクトを作成し、次に Start() メソッドを呼び出してタスクを開始します。この方法は低レベルの手動方法であり、タスクのステータスとスケジュールを手動で管理する必要があります。
            (2)task.Run():これは、タスクを作成および開始するために .NET Framework 4.5 以降で導入された簡略化された方法です。Task.Run() メソッドを使用すると、Start() メソッドを手動で呼び出さずにタスクを直接作成して開始できます。この方法はより簡潔で使いやすく、ほとんどの状況に適しています。
            (3)Task.Factory.StartNew():. NET Framework 4.0以降で導入されたタスクの作成・開始メソッドです。Task.Factory.StartNew() メソッドは、デリゲートをパラメーターとして受け入れ、デリゲートを実行するタスクを作成して開始できます。このメソッドには、タスクのスケジューラ、タスクのタイプなどの指定など、いくつかの追加オプションも提供されます。
            (4)f = 新しい TaskFactory(); f.StartNew():これは、TaskFactory クラスを使用してタスクを作成および開始する方法です。まず、TaskFactory オブジェクトを作成し、次に StartNew() メソッドを呼び出してタスクを作成して開始します。TaskFactory クラスは、タスクのスケジューラの設定、タスクのキャンセルなど、より多くのオプションと柔軟性を提供します。
            概要: Task.Run() は、タスクを作成および開始するために最も簡潔で一般的に使用されるメソッドであり、ほとんどの状況に適しています。Task.Factory.StartNew() と TaskFactory.StartNew() は、より多くの制御が必要な状況に備えて、より多くのオプションと柔軟性を提供します。new Task(); task.Start(); は比較的低レベルの手動メソッドであり、特別なニーズがない限り、通常は推奨されません。
            
            
            Task.Run と TaskFactory.StartNew はどちらも新しいタスクの作成と開始に使用できます。違いは次のとおりです:
            (1) 静的メソッドの呼び出し方法が異なります。Task.Run
            は静的メソッドを通じてタスクを作成し、開始します。
            TaskFactory.StartNew は、インスタンス メソッドを通じて TaskFactory オブジェクトの StartNew メソッドを呼び出すことにより、タスクを作成して開始します。
            (2) デフォルトのスケジューラと新しいスケジューラの作成方法が異なります。Task.Run は、
            現在のスレッド プール内のデフォルトのスケジューラを使用して、タスクの実行を計画します。
            TaskFactory.StartNew は、現在のスレッド プールの既定のスケジューラを使用するか、カスタム タスク スケジューラを使用してタスクの実行を計画するかを選択できます。
            (3)戻り値の型が異なります。
            Task.Run は、非同期で実行できる操作を表す汎用の Task<TResult> オブジェクトを返し、結果を返します。
            TaskFactory.StartNew は、非同期で実行できるが結果を返さない操作を表す Task オブジェクトを返します。
            
            
    
    4. Task.Factory の概要
        Task.Factory は、タスク (Task) ファクトリの作成と管理に使用される .NET Framework のクラスです。これは Task クラスの静的プロパティであり、タスクを作成および管理するためのいくつかのメソッドとプロパティを提供します。
        Task.Factory には、一般的に使用される次のメソッドが用意されています。
        (1) Task.Factory.StartNew(Action アクション):指定されたアクション (Action) を実行するタスクを作成して開始します。
        (2)Task.Factory.StartNew(Action<object> action, object state):指定されたアクションを実行し、オブジェクトをパラメーターとして渡すタスクを作成して開始します。
        (3)Task.Factory.StartNew(Func<TResult> function):指定された関数 (Func<TResult>) を実行して結果を返すタスクを作成して開始します。
        (4)Task.Factory.StartNew(Func<object, TResult> function, object state):指定された関数を実行し、オブジェクトをパラメータとして渡し、結果を返すタスクを作成して開始します。

        private static void Main(string[] args)
        {
            Task.Factory.StartNew(Test, 3);
            Task.Factory.StartNew(Test1, 3);
            Console.ReadKey();
        }

        private static void Test(object obj)
        {
            int n = (int)obj;
            Console.WriteLine(2 * n);
        }

        private static int Test1(object obj)
        {
            int n = (int)obj;
            Console.WriteLine("111");
            return 2 * n;
        }      

 
        
        除了以上的方法,Task.Factory还提供了一些其他的方法和属性,例如:
            - Task.Factory.ContinueWhenAll():创建一个任务,该任务在指定的一组任务全部完成后继续执行。
            - Task.Factory.ContinueWhenAny():创建一个任务,该任务在指定的一组任务中的任何一个完成后继续执行。
            - Task.Factory.FromAsync():将一个异步操作(如BeginXXX和EndXXX方法)转换为一个任务。
            - Task.Factory.CancellationToken:获取一个取消标记(CancellationToken),用于取消任务的执行。
        使用Task.Factory可以更方便地创建和管理任务,它提供了一些便捷的方法和属性,使得任务的创建和控制更加灵活和高效。
    


二、异步返回值的认识


    1、异步任务没有返回值时,类型为Task,而不是void
        
        在异步方法中使用Task作为返回类型,即Task或Task<T>,是为了表示异步操作的完成状态和结果。

        异步方法通常会执行一些耗时的操作,例如网络请求、文件读写或计算密集型任务。为了避免阻塞主线程或UI线程,我们可以将这些耗时的操作放在一个异步方法中,并使用await关键字等待操作完成。

        非同期メソッドに戻り値がない場合は、戻り値の型を Task に設定できます。このように、await キーワードを使用して非同期操作の完了を待機し、次の操作を実行する前に非同期メソッドが完了していることを確認できます。例: 時間のかかる操作を実行しても結果を返さない非同期メソッド DoSomethingAsync があるとします。

        public async Task DoSomethingAsync()
        {
            await Task.Delay(1000); // 模拟一个耗时的操作

            Console.WriteLine("操作完成");
        }


        DoSomethingAsync メソッドを呼び出すときに、await キーワードを使用して、非同期操作の完了を待つことができます。このようにして、メイン スレッドや UI スレッドをブロックすることなく、次のステップを実行する前に非同期メソッドの実行を完了することができます。

        public async Task RunAsync()
        {
            Console.WriteLine("开始执行异步操作");

            await DoSomethingAsync();

            Console.WriteLine("异步操作完成");
        }


        上記の RunAsync メソッドは DoSomethingAsync メソッドを呼び出し、await キーワードを使用して非同期操作の完了を待ちます。非同期操作が完了すると、次の操作が続行されます。

        戻り値の型を Task に設定することで、非同期メソッドの性質を明確に表現し、await キーワードを使用して非同期操作の完了を待つことができます。こうすることで、非同期操作の実行順序と結果をより適切に管理できるようになります。
        
        
        人間の言葉で言うと、await には 2 つの機能があり、1 つはタスクの実行の完了を待つ機能、もう 1 つは完了したタスクの内部値を取得する機能です。
        この関数は Task または Task<T> タイプの条件で実行する必要があるため、戻り値がない場合は Task タイプもマークする必要があります。
        
    
    2. 戻り値を 2 つの部分に分けて表示すると、型は Task<int> として識別されるのに、値が int になるのはなぜですか?

        private static async Task<int> Main(string[] args)
        {
            Task<int> task = Task.Run(() =>
            {
                return 32;
            });
            
            Console.ReadKey();
            return await task;
        }  

 
        上の例では、タスクの型は Task<int> で、await タスクの後は int である必要があるため、return 後も int のままですが、Task<int> はメソッドの前に使用されます。
        
        これは、タイプと値の 2 つの部分に分けて表示されます。
        戻り値の型と戻り値を分離すると、メソッドの非同期の性質と実際に返される値をより明確に表現できるようになります。

        メソッドの戻り値の型で Task<int> を使用することで、このメソッドが非同期メソッドであることを明示的に示し、Task<int> オブジェクトを返し、非同期操作の結果が int 型の値であることを示します。これにより、呼び出し元は、このメソッドが非同期メソッドであること、およびその戻り値を非同期で処理する必要があることを明確に知ることができます。

        メソッド内では、await キーワードを使用して Task<int> オブジェクトの完了を待ち、その結果を取得することで、通常の同期操作と同じように、この int 型の値を便利に使用できます。このようにして、非同期操作の結果を Task<int> オブジェクトにパッケージ化せずに、メソッドの戻り値として直接使用できます。
        
        戻り値の型と戻り値を別々に扱うこの方法により、メソッドの非同期の性質をより明確に表現でき、非同期操作の結果を使いやすくなります。同時に、他の開発者がこのメソッドをよりよく理解し、使用するのにも役立ちます。
        await が削除された場合、返される Task<int> はメソッドの前の型 Task<int> と一致するはずですが、実際にはエラーが報告されます。


        したがって、戻り値と戻り値の型に関しては、なぜそのように設計されているのかを理解する必要があります。一般的に使用される場所は、メソッド、return、および Result 属性の前です。
    
        同様に、Task<string> と string の場合:
        場合によっては、Task<string> を文字列として使用できます。これは、Task<string> が非同期操作を表し、その結果が string 型の値であるためです。

        await キーワードを使用して Task<string> オブジェクトを待機すると、現在のメソッドの実行が一時停止され、タスクが完了するまで待機します。タスクが完了すると、await 式はタスクの結果 (文字列型の値) を返します。

        この場合、通常の文字列値と同様に、await 式の結果を直接使用できます。たとえば、これを文字列型の変数に代入したり、文字列型のパラメータを受け入れるメソッドにメソッド パラメータとして渡すことができます。

        Task<string> を文字列として使用できますが、それらは異なるタイプであることに注意してください。Task<string> は非同期操作を表し、string は同期操作の結果を表します。したがって、同期操作と非同期操作を明確に区別する必要がある場合には、この 2 つの違いに注意して正しく使用する必要があります。
        
        また、メソッドの戻り値の型が Task の場合、メソッドは return を記述する必要はありませんが、メソッドに return を記述すると void として解釈されてエラーとなります。同時に、その結​​果も使用できません。つまり、task.Result 属性がありません。
        メソッドの戻り値の型が Task<string> の場合、一致するように戻り値の文字列を記述する必要があります。このとき、文字列型の値であるtask.Resultを使用して結果を利用することができます。
    


3. 非同期例外キャッチ


    主な参考文献: https://blog.csdn.net/weixin_46879188/article/details/118018895、非常によくまとめられています。
    
        例外をキャッチするには、try..catch... を使用します。 Try catch には次の特徴があります:
        
    1. 同期メソッドで例外をキャッチすることは確実に可能です。
        try-catch ステートメントは主に例外をキャッチするために使用され、スレッドの同期に限定されません。try ブロックは、例外をスローする可能性のあるコードを含む領域です。try コード ブロック内のコードが例外をスローした場合、システムは catch コード ブロックにジャンプし、対応する例外処理ロジックを実行します。
        同期スレッドであっても非同期スレッドであっても、コード ブロック内のコードが例外をスローする限り、try-catch はそれをキャッチして処理できます。したがって、try-catch を使用すると、同期スレッドによってスローされた例外をキャッチでき、非同期スレッドからの例外を処理することもできます。
        
        非同期例外を理解する例:

        private static async Task Main(string[] args)//a
        {
            try
            {
                await TestAsync();//b
            }
            catch (Exception)
            {
                Console.WriteLine("调用方捕捉到异常。");//c
            }
            Console.ReadKey();
            //return 0;//f
        }

        private static async Task TestAsync()//d
        {
            //try
            //{
            //    await Task.Delay(1000);
            //    throw new Exception("被调用方发生异常.");
            //}
            //catch (Exception)
            //{
            //    Console.WriteLine("异步中发生异常。");
            //}
            await Task.Delay(1000);//e
            throw new Exception("被调用方发生异常.");
        }   

 
        (1) a のコンソールの戻り値は一般的に void または int ですが、void と書くとエラーになります。以前の async は通常 void と一致しないため、例外をキャッチできなくなります (void と一致する特殊なケースは winform のコントロール イベントであり、特別な例外処理の問題があります)。ここでタスクを実行します。もちろん、Task<int> にすることもできます。その場合は f で連携する必要があります。
            非同期メソッドでは、async void などの明確な戻り値の型がない場合、await を使用してメソッドの完了を待つことも、メソッド内で発生する可能性のある例外を直接キャッチすることもできません。これにより、呼び出し元が非同期メソッドの実行結果を正しく処理または追跡できなくなる可能性があります。
            さらに、Task 戻り値の型を使用すると、他の非同期メソッドと簡単に組み合わせて連結できます。タスクを返すことにより、非同期操作への依存関係を簡単に確立し、より表現力豊かで読みやすい非同期コードを作成できます。
            コンソール アプリケーションでは、戻り値の型は省略できます (async void Main など)。Main メソッドはアプリケーションのエントリ メソッドであり、主にアプリケーションの実行の開始と調整に使用されます。Main メソッドの戻り値の型として async Task<int> を使用すると、非同期操作と例外をより適切に処理して、アプリケーションの終了時に適切な終了コードを提供できるようになります。では、Task 戻り値の型を使用すると、非同期操作の待機と例外処理のサポートが強化され、非同期コードがより読みやすく、保守しやすく、信頼性が高くなります。
        
        (2) d と e のマッチング記述は非同期操作です。e がなくても、d に async が明示的に記述されているため、非同期操作になります。
            TestAsync方法是一个异步方法,并且使用async关键字进行标记。即使没有显式使用Task.Run或者类似的调用进行异步执行,await Task.Delay(1000)语句仍然会在异步上下文中执行。因此,异步方法会将该任务(即Task.Delay)放入任务队列中,然后立即返回给调用方,而不会等待任务的完成。
            即使在异步方法中没有任何await语句,该方法仍然被认为是异步的。在异步方法中,使用async关键字声明方法为异步方法,表示该方法可能会执行异步操作。即使方法体内没有await语句,该方法仍然可以被视为异步方法。
            如果没有await语句,该异步方法会立即执行完毕,并返回一个已完成的Task对象。这个Task对象表示异步操作已经完成,但实际上并没有异步操作发生。这种情况下,异步方法的执行方式与普通的同步方法类似,只是在方法签名上使用了async关键字。它不会创建新的线程或者引入异步操作。注意,这种情况下的异步方法可能会有一些行为变得不明确,因为没有异步操作发生。通常情况下,我们应该在异步方法中使用await语句来等待异步操作的完成,以确保异步方法的正常执行和正确的行为。
            
            因此,即使没有e处,因为异步原因,出现的异常不会在本方法内中断,它会通过返回Task"传染"到主程序调用方,所以在主调方用try会捕捉到异常。
            
        (3)d处,异步方法内部发生异步,并没有让异步方法中断。
            异步方法中的异常不会立即中断整个程序的执行,而是通过Task对象将异常传播到调用方,直到被捕获或者传播到顶层方法。使用try-catch块可以有效地捕获和处理异步方法中的异常,以确保程序的正常执行。
            
    
    2. 2. Task メソッド内の try...catch は、
        スレッド内で発生する例外をキャッチできるため、優先される処理方法は、Task 内で try...catch... を使用して例外を処理することです。
        try...catch は、try ブロック内で発生する例外のみをキャッチできます。try ブロックの外側のコードで例外が発生した場合、それをキャッチすることはできません。したがって、コードを記述するときは、例外をキャッチして時間内に処理できるように、例外を引き起こす可能性のあるコードを try ブロックに配置する必要があります。catch ブロックの順序が重要であることに
        
        注意してください。親クラス例外の catch ブロックをサブクラス例外の catch ブロックの前に配置した場合、サブクラス例外の catch ブロックがすでに例外をキャッチしているため、親クラス例外の catch ブロックは実行されません。したがって、コードを記述するときは、特定の要件と例外の継承関係に基づいて catch ブロックの順序を決定する必要があります。

        try
        {
            // 异步方法调用
            await MyAsyncMethod();
        }
        catch (ChildException ex)
        {
            // 捕捉子类异常
        }
        catch (ParentException ex)
        {
            // 捕捉父类异常
        }


    
    3. Taskメソッドの内部例外がハンドリングされない(例外が宇宙空間に消える)のですが、外部でキャッチするにはどうすればよいですか?

        private static void Main(string[] args)
        {
            try
            {
                Task task = Task.Run(() => TestAsync());
                task.Wait();//a
            }
            catch (Exception)
            {
                Console.WriteLine("捕捉到异步中的异常。");
            }
            Console.ReadKey();
        }

        private static Task TestAsync()//d
        { throw new Exception("异步中抛出异常"); }


        上記のステートメント a を追加しないと、タスクによって非同期に発生する例外がキャッチされず、この例外は「宇宙」に消えてしまい、この「宇宙」が UI である場合、UI がクラッシュする可能性があります。
        上記の a の時点で、Task の Wait メソッドを使用して非同期タスクが完了するのを待機するときに、非同期タスクが例外をスローした場合、例外は AggregateException にカプセル化され、すぐにはスローされません。この AggregateException が明示的に処理されない場合、呼び出しスタックの先頭に渡され、最終的にプログラムがクラッシュする原因になります。
        a 点の task.Wait() が追加されていない場合、非同期タスク TestAsync() によってスローされた例外の処理が間に合わず、例外はキャッチされません。で追加された task.Wait() メソッドは、現在のスレッドをブロックし、非同期タスクの完了を待ちます。同時に、非同期タスクによってスローされた例外は再スローされ、例外をキャッチできるようになります。 try...catch ブロック。
        
        注:
        非同期操作で例外がスローされると、その例外は Task オブジェクトにカプセル化されます。例外は、タスクが完了するまで待機する前に、呼び出し元のスレッドにすぐには渡されません。代わりに、呼び出し元が (待機式またはその他の手段を通じて) タスクの完了を明示的に待機するまで、Task オブジェクト内に残ります。
        
        したがって、タスク task= では例外はキャッチされませんが (タスク内に非同期情報はありますが)、wait が表示されると例外がスローされ、try でキャッチされます。
    
    4. スレッド例外の捕捉方式
        スレッド例外の捕捉方式には、ブロッキングキャプチャと非同期キャプチャの 2 種類があります。
        ブロッキング タイプの例外の捕捉は次のとおりです。1
            つ以上のスレッドが開始されると、すべてのスレッドの実行が完了するまで待機し、これらのスレッドで例外が発生するかどうかを判断してから、後続のコードを実行します。
        例外を非同期にキャッチする方法は次のとおりです。
            スレッドが開始された後、その実行が完了するのを待たずに、後続の他のコードを直接実行します。スレッドで例外が発生した場合、例外情報が自動的に返されるか、必要に応じてスレッド例外情報が能動的に取得されます。        (1) Wait()、Result、GetAwaiter().GetResult() (キャプチャのブロック化)
            Task でスローされた例外は直接キャプチャできませんが、Wait() メソッドが呼び出されたとき、または Result プロパティにアクセスされたときにキャプチャできます。例外が取得された場合は、まず AggregateException 型として例外がスローされ、AggregateException 例外が存在しない場合は、Exception として例外がスローされます。GetAwaiter().GetResult() メソッドは、Exception として例外をスローします。
            例については、上記 3 のポイント a での待機を参照してください
            
            。
            回答: GetAwaiter() は、Task タイプおよびその他の待機可能なオブジェクトのメソッドであり、非同期操作の完了を待機するために使用できる TaskAwaiter オブジェクトを返します。
                TaskAwaiter 型は、await 演算子をサポートするように設計されています。これは、非同期操作を待機するときに、より詳細な制御とアクセスを行うための特別なメソッドとプロパティをいくつか提供します。
                GetAwaiter() メソッドを呼び出すと、対応する TaskAwaiter オブジェクトを取得でき、そのオブジェクトが提供するメソッドとプロパティを使用できるようになります。
                TaskAwaiter の一般的に使用されるメソッドとプロパティ:
                    IsCompleted: 非同期操作が完了したかどうかを示すブール値を取得します。
                    GetResult(): 非同期操作の結果を取得します。非同期操作がまだ完了していない場合、このメソッドは操作が完了するまで現在のスレッドをブロックします。
                    OnCompleted(Action continue): 継続操作を設定し、非同期操作が完了したときに指定されたデリゲートを呼び出します。これにより、タスクの完了後に追加のロジックを実行できるようになります。

        private static void Main(string[] args)
        {
            try
            {
                Task task = Task.Run(() => TestAsync());
                task.GetAwaiter().GetResult();//a
            }
            catch (Exception)
            {
                Console.WriteLine("捕捉到异步中的异常。");
            }
            Console.ReadKey();
        }

        private static Task TestAsync()
        { throw new Exception("异步中抛出异常"); }


                ポイントa: 非同期操作の結果は、TaskオブジェクトのGetAwaiter().GetResult()メソッドを呼び出すことで取得できます。このメソッドは、非同期操作が完了して結果を返すまで、現在のスレッドをブロックします。
                非同期操作の戻り値の型が void の場合、GetResult() メソッドは void 値を返します。これは、取得する結果がないことを意味します。
                非同期操作の戻り値の型が Task または Task<T> の場合、GetResult() メソッドは非同期操作の結果を返します。非同期操作が完了すると、GetResult() メソッドは結果をすぐに返します。非同期操作が完了していない場合、GetResult() メソッドは、非同期操作が完了して結果を返すまで現在のスレッドをブロックします。
                注:
                GetResult() メソッドを使用して非同期操作の結果を取得する場合、非同期操作で例外がスローされると、その例外は AggregateException にカプセル化され、GetResult() メソッドが呼び出されたときに再スローされます。
    
    
        (2) ContinueWith を使用して例外をキャプチャします (非同期キャプチャ) (推奨)
            しかし、結果が返されない場合、または Wait() メソッドを呼び出したくない場合は、どうすれば例外を取得できますか? ContinueWith を使用してください。

            Task<int> task = Task.Run(async () =>
            {
                await Task.Delay(300);
                Console.WriteLine("异步进行中...");
                throw new Exception("发生异常."); //a
                return 4; //b
            });
            task.ContinueWith(t =>
            {
                Console.WriteLine(t.Exception.Message.ToString());
            }, TaskContinuationOptions.OnlyOnFaulted);//c


            非同期的に a で例外をスローします。非同期スレッドでは、b は再度実行されずに戻ります。c で continuewith が使用されている場合、エラーが発生したときに内部のコードが実行されます (非同期のキャッチと同等)。
            これは、非同期操作中に発生するエラーを処理するための一般的な方法です。ContinueWith メソッドを使用すると、タスクが完了したとき、またはエラーが発生したときに、特定のフォローアップ アクションを実行できます。
            
            元の記事の作成者は、簡単にするために 2 つの拡張メソッドを作成し、直接使用できるようにしました。

            public static Task Catch(this Task task)
            {
                return task.ContinueWith<Task>(delegate (Task t)
                        {
                            if (t != null && t.IsFaulted)
                            {
                                AggregateException exception = t.Exception;
                                Trace.TraceError("Catch exception thrown by Task: {0}", new object[]
                                {
                                     exception
                                });
                            }
                            return t;
                        }).Unwrap();
            }

            public static Task<T> Catch<T>(this Task<T> task)
            {
                return task.ContinueWith<Task<T>>(delegate (Task<T> t)
                        {
                            if (t != null && t.IsFaulted)
                            {
                                AggregateException exception = t.Exception;
                                Trace.TraceError("Catch<T> exception thrown by Task: {0}", new object[]
                                {
                                        exception
                                });
                            }
                            return t;
                        }).Unwrap<T>();
            }     

       
            マスターが違います unwrap をさらに学ぶために、たくさんのリファレンスを読みました...
            
            
    5. ContinueWith で注意すべき問題
        . Task.Run(...) は通常、すぐに実行を開始するように手配します。
        
        Task.Run(...).ContinueWith(...) は、前の実行が実行された直後に continuewith の実行を続行します。await または Result の最終的な使用方法は、タスク チェーン全体が完了したことを確認することです。したがって、タスク タスク チェーン全体は、表示されるとすぐに実行を開始します。
        
        リンクとは違います。Linq は、タスクの完了後に実行される操作を定義する単なるプランまたは操作チェーンです。タスクと操作チェーンの実行は、実際に使用されるまでトリガーされません。この遅延実行機能により、柔軟性と操作性が向上します。コードの柔軟性 効率、タスク、および操作は、結果が必要な場合にのみ実際に実行されます。

        Task<int> task1 = Task.Run<int>(() =>
        {
            Console.WriteLine("第一个任务完成");
            return 3;
        }).ContinueWith(t =>
        {
            Console.WriteLine("第二个任务完成");
            return t.Result + 2;
        });

        await Task.Delay(500);
        Console.WriteLine("最后任务已经完成.");


        結果:
            最初のタスクが完了し、
            2 番目のタスクが完了し、
            最後のタスクが完了します。
            
        注意: ContinueWith() の後には通常、Action<Task<TResult>> または Func<Task<TResult>> が 1 つのパラメーターとともに続きます。パラメータは、実行が完了した後に返される前の Run A タスクです。
        
            
    6. UnWrap() とは何ですか?
            
        ( 1) Unwrap メソッドは、入れ子になったタスクを展開するために使用される Task クラスのメソッドですタスクが別のタスクを返す場合、それをネストされたタスクと呼びます。Unwrap メソッドを呼び出すと、入れ子になったタスクの結果を外部タスクの結果にラップ解除できます。
        具体的には、Task<Task<T>> に入れ子になった内部タスクがある場合、Unwrap メソッドを呼び出すと内部タスクが展開され、Task<T> 型の結果が返されます。このようにして、内部タスクに手動でアクセスする必要なく、ネストされたタスクの結果をより簡単に処理できるようになります。
        鮮やかな比喩は、ネストされた Task オブジェクトは、内部に別のボックスがあるボックスとみなすことができるということです。Unwrap() メソッドの機能は、最も外側のボックスを開いて内側のボックスを取り出し、内側のボックスを直接処理できるようにすることです。このようにして、内側のボックスが別のボックスにネストされているかどうかを気にせずに、より便利に操作できるようになります (つまり、外側のタスクは必要ありません)。
        Unwrap() メソッドは主に、非同期の入れ子になったタスクを処理してコードを簡素化し、読みやすさを向上させるために使用されます。同期プログラミングには適用されません。
        (2) Unwrap()メソッドを使用する目的は、内部タスクの結果を簡単に取得することです。、より多くのコンテキストと読みやすさを提供するために外側のタスクを保持しながら。このようにして、内部タスクの結果を取得できるだけでなく、必要に応じて外部タスクをさらに処理することもできます。

        private static void Main(string[] args)
        {
            NestedTaskUnwrap();
            Console.ReadKey();
        }

        private static async void NestedTask()
        {
            //Task返回Task<Task<string>>
            //第一个await后result类型为Task<string>
            Task<string> result = await Task.Run<Task<string>>(() =>
            {
                var task = Task.Run<string>(() =>
                {
                    Task.Delay(1000).Wait();
                    return "Mgen";
                });
                return task;
            });
            Console.WriteLine(await result);//第二个await后才会返回string
        }

        private static async void NestedTaskUnwrap()
        {
            //Task返回Task<Task<string>>
            //await后类型为Task<string>,Unwrap后result类型为string
            string result = await Task.Run<Task<string>>(() =>
            {
                var task = Task.Run<string>(() =>
                {
                    Task.Delay(1000).Wait();
                    return "Mgen";
                });
                return task;
            }).Unwrap();
            Console.WriteLine(result); //不需要await,result已经是string
        }


        
        内部タスクと外部タスクの関係を以下に示します。

        Task<Task<int>> nestedTask = Task.Run<Task<int>>(() =>
                                      {
                                          return Task.Run(() =>
                                          {
                                              return 42;
                                          });
                                      });
        Console.WriteLine(nestedTask.Result);
        Console.WriteLine(nestedTask.Result.Result);//42

        Task<int> innerTask = nestedTask.Unwrap();
        Console.WriteLine(innerTask.Result);//42


        
        (3) Unwrap() メソッドは、Task<T> と内部型 T の間の特殊な関係を処理するように設計されています。

        非同期プログラミングでは、タスク内で別の Task<T> タイプのタスクを返すことがあります。この場合、通常は、ラップされたタスク自体ではなく、内部タスクの結果を取得する必要があります。

        Unwrap() メソッドの主な目的は、この問題を解決することです。入れ子になった Task<T> を展開し、入れ子になったタスクを返す代わりに、内部タスクの結果を返します。このようにして、ネストされたタスク構造を扱うことなく、内部タスクの結果を直接取得できます。

        Unwrap() メソッドは、入れ子になった Task<Task<T>> 構造にのみ適用できることに注意してください。ネストされたタスクがない場合、またはネストされたタスクのタイプが Task<Task<T>> ではない場合、Unwrap() メソッドを呼び出しても効果はありません。
            
            
    7. 非同期メソッドでの例外のキャッチ
        
        (1) async...await は例外をキャッチできます

        private static async Task TestAsync()
        {
            try
            {
                await Task.Run(() =>
                {
                    throw new Exception("异步中抛出异常。");
                });
            }
            catch (Exception)
            {
                await Console.Out.WriteLineAsync("捕捉到异常");
            }
        }    

    
        
            
        (2)C# 异步方法,尽量避免使用async void而是要用async Task
        try-catch 块只能捕获当前代码块中引发的异常,并不能捕获在 SynchronizationContext 中引发的异常。
        当在 async void 方法中引发异常时,该异常将直接在调用线程的 SynchronizationContext 上引发,而不会传递到调用方代码中。这意味着无法使用 try-catch 块捕获从 async void 方法引发的异常,而且该异常可能会导致程序崩溃。
        所以避免使用async void。
        
        注意:
        在 WinForms 控件中,可以在 async void 方法中捕获异常的原因是因为 WinForms 控件自身实现了一个 SynchronizationContext,称为 WindowsFormsSynchronizationContext。这个特定的 SynchronizationContext 允许在 async void 方法中捕获异常并将其传递到调用方代码中。
        WindowsFormsSynchronizationContext 会捕获从 async void 方法引发的异常,并将其封装在一个特殊的异常对象中,然后将其传递到调用方的异常处理代码中。这样,就可以在 WinForms 控件中使用 try-catch 块来捕获并处理从 async void 方法引发的异常。
        


四、线程取消


    1. 8 つのスレッドが同時に非同期で実行され、1 つが非同期で停止すると、残りも同時に停止します。
        8つのスレッドが並行して動作している場合、先頭に信号灯が設置されており、異常があれば点灯し、他のスレッドがその光を確認すると全て停止します。
        インターフェース: ボタン、テキストボックス
     


        プログラム:

        private void BtnRun_Click(object sender, EventArgs e)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken ct = cts.Token;
            for (int i = 0; i < 8; i++)
            {
                string key = $"Key_{i}";
                Task.Run(async () =>
                {
                    await Task.Delay(new Random().Next(100, 300));
                    if (ct.IsCancellationRequested)
                    { throw new Exception($"[{key}]异常,线程取消 "); }
                    Display($"[{key}] 正常开始...\r\n");

                    if (key.Equals("Key_6"))
                    {
                        Display($"[{key}] 异常。\r\n");
                        cts.Cancel();
                        { throw new Exception($"[{key}]异常,线程取消 "); }
                    }

                    if (ct.IsCancellationRequested)
                    { throw new Exception($"[{key}]异常,线程取消 "); }
                    Display($"[{key}] 正常结束.\r\n");
                });
            }
        }

        private void Display(string s)
        { Invoke(new Action(() => { TxtInfo.AppendText(s); })); }


        
    2、上面没有使用ct来作第二参数监视,而是使用IsCancellationRequested.
        分别在异步线程的开始与结束进行监视。
        
        缺点,如果是控制台将不断中断程序,在winform中没有此现象
        
        
        问:结果中在发生异步提示“[Key_6] 异常。”后,后面仍然有两个正常结束,为什么?
        答:当Key_6发生异步取消时,在异步代码执行到ct.ThrowIfCancellationRequested()之前,Key_3和Key_1任务已经开始执行,并且在执行到ct.ThrowIfCancellationRequested()之后,检查到了取消请求,并抛出了OperationCanceledException异常,但是由于被抛出的异常在异步线程中被捕获处理,不会影响到其他任务的执行。
            因此,Key_3和Key_1的任务会继续执行,直到完成并显示正常结束。这是因为ct.ThrowIfCancellationRequested()只是一个检查取消请求的方法,它不会强制立即终止任务。
            而且线程的启动,取消,传递信息等有延迟,就算立即进行取消,这个取消的传递,线程的停止(汽车刹车),其它线程(汽车)的查看信号灯及取消都有些许的延迟,有时需要综合考虑最后的结果。
        
        
        问:为什么线程的启动会有延迟?
        答:C#线程的启动可能会有延迟的原因有多种。
            (1)调度延迟:线程的启动需要操作系统进行线程调度,而操作系统可能有其他任务正在执行,因此可能会有一定的延迟。
            (2)线程池延迟:在使用线程池启动线程时,线程池可能已经达到了最大线程数限制,需要等待其他线程完成后才能启动新线程,导致延迟。
            (3)优先级调度:操作系统可能会根据线程的优先级来进行调度,如果有其他高优先级的线程正在执行,低优先级的线程可能会有延迟。
            (4)线程同步:如果在启动线程前需要进行一些准备工作或者等待某些条件满足,这些操作可能会引起线程启动的延迟。
            注意,延迟是不确定的,具体的延迟时间取决于多种因素,包括操作系统的调度算法、系统负载、线程优先级等等。
        
        
        
    3、优化一下:

        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken ct = cts.Token;
        for (int i = 0; i < 8; i++)
        {
            string key = $"Key_{i}";
            Task.Run(async () =>
            {
                await Task.Delay(new Random().Next(100, 300), cts.Token);
                ct.ThrowIfCancellationRequested();

                Display($"[{key}] 正常开始...\r\n");

                if (key.Equals("Key_6"))
                {
                    Display($"[{key}] 异常。\r\n");
                    cts.Cancel();
                    throw new Exception($"[{key}] 异常。");
                }

                ct.ThrowIfCancellationRequested();
                Display($"[{key}] 正常结束.\r\n");
            });
        }  

         
        (1)await Task.Delay(new Random().Next(100, 300), cts.Token) は、ランダムな遅延時間だけ待機し、キャンセル要求に応答して適切なタイミングでタスクの実行を終了します。
            この操作は指定された時間待機しますが、受信した cancelToken にも応答します。遅延期間中に cancelToken がキャンセルされた場合 (つまり、cts.Cancel() が呼び出された場合)、Task.Delay は TaskCanceledException をスローし、非同期タスクがキャンセル状態になります。
            
        (2)ct.ThrowIfCancelRequested()は、非同期タスク実行時のキャンセル要求を確認するために使用されます。cts.Cancel() の呼び出し後に CancelToken がキャンセルされた場合、ct.ThrowIfpaymentRequested() は OperationCanceledException をスローします。
        
            OperationCanceledException とは何ですか? プログラムはクラッシュしますか?
            OperationCanceledException は、リクエストのキャンセルを示すために非同期プログラミングでよく使用される特別な例外です。スレッド全体が中断されることも、プログラム全体がクラッシュすることもありません。
            OperationCanceledException がスローされると、例外処理メカニズムでキャッチして処理できる通常の例外オブジェクトになります。例外が適切に処理されない限り、非同期タスクの OperationCanceledException によってプログラム全体が自動的にクラッシュすることはありません。
            通常情况下,在异步任务中抛出 OperationCanceledException 后,你可以在适当的位置捕获该异常并采取相应的处理措施,比如释放资源、清理操作或进行适当的回滚操作。你可以使用 try-catch 块来捕获并处理该异常,以控制程序的行为。
            如果在异步任务中没有正确处理 OperationCanceledException,它可能会传播到调用异步任务的代码中,直到被捕获或引发未处理的异常处理机制。所以确保在适当的地方对 OperationCanceledException 进行捕获和处理,以适应你的代码逻辑和需求。
            
            
        (3)为什么没有在异步方法的参数列表中添加第二个参数 ct,更精细地监视取消?
            如果你已经在异步方法内部使用了 ct.ThrowIfCancellationRequested() 来检查取消标记并抛出异常,那么在参数列表中再添加第二个参数 ct 是没有必要的。再添加第二个参数 ct 只会造成重复的监视,没有额外的好处,反而会影响代码的可读性和性能。
            
        (4)用抛出异步来中断异步是否是浪费?
            答:是的。可以再优化:

            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken ct = cts.Token;
            for (int i = 0; i < 8; i++)
            {
                string key = $"Key_{i}";
                Task.Run(async () =>
                {
                    await Task.Delay(new Random().Next(100, 300), ct);
                    if (ct.IsCancellationRequested)
                    {
                        Display($"[{key}] 取消请求已触发,任务被取消。\r\n");
                        return;
                    }

                    Display($"[{key}] 正常开始...\r\n");

                    if (key.Equals("Key_6"))
                    {
                        Display($"[{key}] 异常。\r\n");
                        cts.Cancel();
                        return;
                    }

                    if (ct.IsCancellationRequested)
                    {
                        Display($"[{key}] 取消请求已触发,任务被取消。\r\n");
                        return;
                    }
                    Display($"[{key}] 正常结束.\r\n");
                });
            }


            return ステートメントを使用すると、非同期メソッドの実行を直接中断し、すぐに戻ります。これは、例外をスローしてタスクをキャンセルする代わりに使用できます。これにより、リソースの不必要な浪費が回避され、コードの効率が向上します。
            
            
    4. 上記には小さなバグがあり、スレッドがキャンセルされてもスレッドが開始されていない場合、
        スレッドは開始されますが、例外により終了します。
        最適化: string key = $"Key_{i}"; の前に if (ct.IspaymentRequested) ブレークを追加して、スレッドを再度開始せずにループから直接抜け出せるようにします。
        または、2 番目のパラメータを追加します。

        Task.Run(async () =>
        {
            await Task.Delay(new Random().Next(100, 300), ct);
            if (ct.IsCancellationRequested)
            { throw new Exception($"[{key}]异常,线程取消 "); }
            Display($"[{key}] 正常开始...\r\n");

            if (key.Equals("Key_6"))
            {
                Display($"[{key}] 异常。\r\n");
                cts.Cancel();
                { throw new Exception($"[{key}]异常,线程取消 "); }
            }

            if (ct.IsCancellationRequested)
            { throw new Exception($"[{key}]异常,线程取消 "); }
            Display($"[{key}] 正常结束.\r\n");
        },ct);//a   

         
        上記の a 点に 2 番目のパラメータを追加して、キャンセル ステータスがあるかどうかを判断します。
        
        質問: 2 番目のパラメータ ct の役割は何ですか?
        回答: 最大の効果は、スレッドが開始されていないときに ct をチェックすると、開始前にタスクをキャンセルできることです。
            タスクがキャンセルされた場合は、タスクコード内で直接 ct.IspaymentRequested や ThrowIfpaymentRequested() を使用することでキャンセル要求を判定するなど、キャンセルを細かく制御することができます。
            実際、現時点では追加しても追加しなくてもあまり効果はないようですが、個人的には先ほどの機能以外はあまり意味がなく、単に「監視」を追加しただけでタスクがキャンセルされる可能性があると感じています。
 


5. 一時変数


    1. 次のプログラムの結果はどうなりますか?

        for (int i = 0; i < 20; i++)
        {
            Task.Run(() =>
            {
                Console.Write($"{i},");
            });
        }


    
        答えは次のとおりです: 20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,
    
    
    2. なぜそれらはすべて20?
        上記はクロージャであり、Task.Run は外部変数 i にアクセスします。クロージャ参照:
            https://blog.csdn.net/dzweather/article/details/132741575?spm=1001.2014.3001.5501
        
        for ループは 1 ミリ秒未満で実行され、i は 20 になり、各タスクには遅延が発生します。 i 変数を使用してスレッドに適用する場合、正常に適用された場合、i はすでに 20 になっており、出力は 20 のみになります。それでは、遅延を追加して見てみましょう:

        for (int i = 0; i < 20; i++)
        {
            Thread.Sleep(1);
            Task.Run(() =>
            {
                Console.Write($"{i},");
            });
        }

        
        結果は
        次のようになります: 2,3,3,4,4,6,6,8,9,10,10,11,12,13,14,16,16,18,18,19。 late は完全ではありませんが、細かく数えてみるとまだ 20 個、最終的にいくつかのスレッドが実行されて戻ってくるときに i が変わっているので完全な数ではないので、Thread などの待ち時間を増やしてください
        
        。 Sleep(100)、および上記の数値がより完全であり、大きいほど完全です。
        
    
    3. 再度最適化します。Thread.Sleep の代わりに t.Wait() を使用します。

        for (int i = 0; i < 20; i++)
        {
            Task t = Task.Run(() =>
            {
                Console.Write($"{i},");
            });
            t.Wait();
        }


        スレッドが適用、実行、完了するまでにスリープする時間を決定できないため、上記のスリープよりも優れています。
        これにより、0 ~ 19 のすべての数字が直接表示されます: 0、1、2、3、4、5、6、7、8、9、10、11、12、13、14、15、16、17、18 、19。
        続行する前に各非同期結果を待機する必要があるため、結果は順番に出力されます。
    
    
    4. 従来のアプローチ: 一時変数を使用する (推奨)
        . 上記の表示は正常ですが、wait は次のスレッドに進む前に各スレッドの実行が完了するまで待つ必要があり、時間がかかります。

        for (int i = 0; i < 20; i++)
        {
            int j = i;
            Task t = Task.Run(() =>
            {
                Console.Write($"{j},");
            });
        }


        結果: 0,2,1,3,4,6,9,10,11,12,8,14,15,16,7,18,19,5,17,13、
        上記の結果は完全で完全に反映されていますスレッドには遅延があり、同時に実行されるため、時間は比較的速くなります。
        j は、ループの反復ごとに再割り当てされるローカル変数です。各タスクは j の現在値を参照するため、異なる値を出力します。これは、各タスクが現在のループ反復で j の値をキャプチャする Task.Run メソッド内にクロージャを作成するためです。
        
        変数 i については、ループの外で宣言されるため、各タスクは同じ i 変数を共有します。タスクはループが終了する前に実行を開始する可能性があるため、同じ値、つまりループ終了時の i の最終値が出力されます。
    
    
 

おすすめ

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