一、启动线程的多种方式
1、New Task()
2、Task.Run();
3、TaskFactory.StartNew();
4、Task.Factory.StartNew();
//方式一Task task = new Task(() =>{Console.WriteLine($"ID:[{Environment.CurrentManagedThreadId}]线程启动...");});task.Start();//方式2Task.Run(() =>{Console.WriteLine($"ID:[{Environment.CurrentManagedThreadId}]线程启动...");});//方式3TaskFactory tf = new TaskFactory();tf.StartNew(() =>{Console.WriteLine($"ID:[{Environment.CurrentManagedThreadId}]线程启动...");});//方式4Task.Factory.StartNew(() =>{Console.WriteLine($"ID:[{Environment.CurrentManagedThreadId}]线程启动...");});
二、Thread.Sleep()与Task.Delay()的区别
Thread.Sleep() 和 Task.Delay() 是两种不同的方法,用于在代码中添加延迟或暂停的操作。
1. Thread.Sleep() 方法
是在当前线程上阻塞指定的时间。它会使当前线程进入休眠状态,不会执行任何其他操作,直到指定的时间过去。这个方法通常在单线程或多线程的情况下使用。
Thread.Sleep(1000);//使当前线程休眠 1 秒
2. Task.Delay() 方法
是一个异步方法,它会创建一个延迟指定时间的任务。它不会阻塞当前线程,而是返回一个表示延迟操作的 Task 对象。可以使用 await 关键字等待延迟操作完成。
await Task.Delay(1000);//将异步延迟 1 秒
或者,如果在同步上下文中使用,可以使用 Task.Delay().Wait() 等待延迟操作完成:
Task.Delay(1000).Wait();
使用 Task.Delay() 的好处是,它可以在异步编程模型中更好地与其他异步操作一起使用,而不会阻塞线程。这对于 UI 线程或其他需要响应性的情况特别有用。
总结来说,Thread.Sleep() 是一个同步方法,会阻塞当前线程,而 Task.Delay() 是一个异步方法,不会阻塞当前线程,可以更好地与异步编程模型一起使用。
3、Await Task.Delay()与Task.Delay().Wait()的区别
Task.Delay(1000).Wait() 和 await Task.Delay(1000) 在功能上是相同的,它们都会在延迟指定时间后继续执行代码,但在使用上有区别:
(1)Task.Delay(1000).Wait() 是一个阻塞的同步调用。它会阻塞当前线程,直到延迟操作完成。这意味着在调用 Wait() 之后,当前线程将无法执行其他操作,直到延迟时间过去。
(2)await Task.Delay(1000) 是一个异步调用。它会在延迟时间内返回一个未完成的任务,并允许当前线程继续执行其他操作。使用 await 关键字等待延迟操作完成后,代码将在延迟时间过去后继续执行。 注意:为了使用 await,它必须在一个异步方法中调用,或者使用 Task.Run() 创建一个新的线程来执行异步操作。但都需要前面的async配合。
async Task MyMethod(){// 在异步方法中使用 awaitawait Task.Delay(1000);// 延迟操作完成后继续执行}// 或者在新的线程中使用 Task.Run() 创建异步操作Task.Run(async () =>{await Task.Delay(1000);// 延迟操作完成后继续执行});
总结,Task.Delay(1000).Wait() 是一个阻塞的同步调用,而 await Task.Delay(1000) 是一个异步调用,可以在延迟时间内继续执行其他操作。使用 await 可以更好地与异步编程模型一起使用,并提供更好的响应性。
Task.Delay().Wait()的确是一种阻塞当前线程的写法,与异步编程的初衷相悖,这种写法在异步编程中通常是不推荐的,因为它会阻塞线程,导致资源的浪费和性能的降低。在异步编程中,我们通常使用 await Task.Delay() 来实现延迟操作,而不是使用 Task.Delay().Wait()。
三、多线程应用的场景
多线程可以在许多场景中应用,特别是在以下情况下:
1. **并行处理**:
当有一些相互独立的任务需要同时执行时,可以使用多线程来实现并行处理,以提高整体的执行效率。例如,对于大规模数据的处理、图像/视频处理、并行计算等场景,多线程可以将任务分配给多个线程并同时执行。
2. **响应性用户界面**:
在需要保持用户界面的响应性的情况下,可以使用多线程来处理耗时的操作,以避免阻塞用户界面。例如,在进行网络请求、数据库查询或其他耗时的操作时,可以将这些操作放在单独的线程中执行,使用户界面保持流畅。
3. **并发访问共享资源**:
当多个线程需要同时访问共享资源(如共享变量、文件、数据库等)时,可以使用多线程来实现并发访问。通过使用锁、互斥量、信号量等同步机制,可以确保多个线程对共享资源的访问是安全的。
4. **后台任务处理**:
在需要在后台执行长时间运行的任务时,可以使用多线程来执行这些任务,以避免阻塞主线程。例如,在进行文件下载、数据同步、定时任务等场景中,可以将这些任务放在后台线程中执行。
5. **并发编程**:
在需要处理大量并发请求或事件的情况下,可以使用多线程来实现并发编程。例如,在网络服务器、消息队列处理、并发事件处理等场景中,多线程可以同时处理多个请求或事件。
注意,多线程编程需要小心处理线程安全性、资源竞争、死锁等问题。合理地使用同步机制、线程池、异步编程等技术,可以帮助避免这些问题。此外,使用并发集合、并发字典等线程安全的数据结构,也可以简化多线程编程。
四、几个等待
1、复习:带Wait的等待
用于等待任务完成的方法,包括 Task.Wait()、Task.WaitAny()、Task.WaitAll() 和 Task.WaitOne()。这些方法的区别如下: (1)Task.Wait() 方法:
该方法会阻塞当前线程,直到任务完成。如果任务抛出异常,Wait() 方法也会抛出相同的异常。Wait() 方法返回为void,异常则任务失败,反之成功。
使用实例调用:task.Wait();
(2)Task.WaitAny() 方法:
该方法会阻塞当前线程,直到其中任意一个任务完成。它接受一个 Task 数组或可枚举对象作为参数,并返回已完成任务的索引。如果其中任意一个任务抛出异常,WaitAny() 方法也会抛出相同的异常。返回值为int,表示任务数组的索引,由此可知最先完成的任务。
使用静态方法调用:Task.WaitAny(...)
Task[] tasks = new Task[3];tasks[0] = Task.Run(() =>{Thread.Sleep(1000);Console.WriteLine("任务0完成!");});tasks[1] = Task.Run(() =>{Thread.Sleep(3000);Console.WriteLine("任务1完成!");});tasks[2] = Task.Run(() =>{Thread.Sleep(2000);Console.WriteLine("任务2完成!");});int intStart = Task.WaitAny(tasks);//返回最先完成任务的索引Console.WriteLine($"任务{intStart}先完成!");
结果:
任务0完成!
任务0先完成!
任务2完成!
任务1完成!
(3)Task.WaitAll() 方法:
该方法会阻塞当前线程,直到所有任务都完成。它接受一个 Task 数组或可枚举对象作为参数。如果其中任意一个任务抛出异常,WaitAll() 方法也会抛出相同的异常。它返回是void,所以无法确定谁先谁后完成。
使用静态方法调用:Task.WaitAll(...)
警告:
WhenAll()与WhenAny()都是静态方法,参数就是任务数组。如果没有带参数,将不会等待,直接向下执行!!!
(4)WaitOne() 方法:
是 ManualResetEvent 类中的方法,用于等待信号的触发。
ManualResetEvent 是一个同步等待句柄,它可以通过 Set() 方法设置信号,通过 Reset() 方法重置信号,并且可以通过 WaitOne() 方法等待信号的触发。在异步编程中,可以使用 ManualResetEvent 来实现一些同步操作。
2、WhenAll()介绍
Task.WhenAll() 方法是 .NET 中的一个异步方法,它接受一个 Task 数组或可枚举对象作为参数,并返回一个新的任务,该任务将在所有的输入任务都完成时变为已完成状态。
当你想要等待多个任务同时完成时,可以使用 Task.WhenAll() 方法。它会等待所有的任务都完成,然后返回一个新的任务,该任务将在所有输入任务都完成时变为已完成状态。
(1)Task.WhenAll() 方法接受一个 Task 数组或可枚举对象作为参数。你可以将所有要等待的任务放入一个数组中,并将该数组作为参数传递给 WhenAll() 方法。
(2) 返回的任务将在所有输入任务都完成时变为已完成状态。这意味着,只有当所有的任务都完成时,返回的任务才会完成。 (3)返回的任务的结果类型是 Task 数组,其中每个元素对应于输入任务数组中的一个任务。你可以通过访问返回任务的 Result 属性来获取每个任务的结果。
Task<int> task1 = Task.Run(() =>{ return 32; });Task<int> task2 = Task.Run(() =>{Thread.Sleep(1000);return 43;});Console.WriteLine("前:" + DateTime.Now);Task<int[]> t = Task.WhenAll(task1, task2);//aConsole.WriteLine("后:" + DateTime.Now);int[] ns = await t;//bforeach (int n in ns){Console.WriteLine(n);}
上面b处,是用await t取得结果后,再从内层取得返回值,是异步不会阻塞当前线程。如果用t.Result来代替await t,将是一个同步且阻塞当前线程。一般使用await t,这样可以充分利用异步编程的特性,提高程序的性能和响应能力。
(4) 如果输入任务数组中的任何一个任务失败(即抛出了异常),返回的任务也将失败,并且会抛出一个聚合异常,其中包含了所有任务的异常信息。 (5)你可以使用 await 或 ContinueWith() 等方法来等待返回的任务的完成。当返回的任务完成时,表示所有的输入任务都已经完成。
Task<int> task1 = Task.Run(() =>{return 32;});Task<string> task2 = Task.Run(() =>{return "task2";});Task.WhenAll(task1, task2).ContinueWith((t) =>{Console.WriteLine(task1.Result);Console.WriteLine(task2.Result);});
也可以使用Await来异步等待,不然的话Task.WhenAll(task1, task2)会一闪而过
Task<int> task1 = Task.Run(() =>{return 32;});Task<string> task2 = Task.Run(() =>{Thread.Sleep(1000);return "task2";});Console.WriteLine(DateTime.Now);await Task.WhenAll(task1, task2);Console.WriteLine(DateTime.Now);Console.WriteLine(task1.Result);Console.WriteLine(task2.Result);
上面用Await必须在方法前添加Async。也可改用GetAwait():
Console.WriteLine(DateTime.Now);Task.WhenAll(task1, task2).GetAwaiter().GetResult();Console.WriteLine(DateTime.Now);Console.WriteLine(task1.Result);Console.WriteLine(task2.Result);
问:上面Task.WhenAll(task1, task2).GetAwaiter().GetResult();能否改为Task.WhenAll(task1, task2).GetAwaiter();?
答:不能!!
因为GetAwaiter()相当于在该句前面添加一个Await,它只是有异步等待功能,这个功能一添加就返回了,不会等待它的执行结果。也就是这一句仍然是一闪而过,不会等待。但加了GetReslut它等待出了结果才能过,相当于等待所有任务全部完成。
当调用 GetAwaiter() 方法时,它会立即返回一个 TaskAwaiter 对象,表示异步操作已经开始执行。这个方法并不会等待异步操作的实际完成,而是返回一个可用于等待操作完成的对象。
然后,我们可以使用 GetResult() 方法来获取异步操作的结果。这个方法会阻塞当前线程,直到异步操作完成,并返回异步操作的结果。
Console.WriteLine("前:" + DateTime.Now);TaskAwaiter ta = Task.WhenAll(task1, task2).GetAwaiter();Console.WriteLine("后:" + DateTime.Now);Console.WriteLine(ta.IsCompleted);Console.WriteLine(task1.Result);
结果:
前:2023/9/9 16:08:50
后:2023/9/9 16:08:50
False
32
所以,GetAwaiter() 方法表示异步操作已经开始执行,而 GetResult() 方法表示异步操作已经完成,并返回结果。在异步方法中,我们可以直接使用 await 关键字来等待异步操作的完成,并获取结果,而不需要显式调用这两个方法。但在同步方法中,我们需要使用 GetAwaiter().GetResult() 来等待异步操作的完成,并获取结果。
问:为什么说Task.WhenAll(task1, task2).GetAwaiter().GetResult()要小心死锁?
答:这是因为 GetResult() 方法是同步方法,会阻塞当前线程,直到异步操作完成,但同时也会阻塞异步操作所使用的线程。
在某些情况下,如果我们在同一个上下文中使用 GetAwaiter().GetResult() 来等待多个异步操作的完成,而这些异步操作又依赖于同一个上下文资源(例如共享锁),那么可能会发生死锁。
在Task.WhenAll(task1, task2).GetAwaiter().GetResult(); 中,Task.WhenAll() 方法会等待所有的任务都完成,然后返回一个新的任务,表示所有任务的完成。然后我们调用 GetAwaiter().GetResult() 方法来等待这个新的任务的完成,并获取结果。
如果 task1 和 task2 都依赖于同一个上下文资源,并且在等待它们的完成时使用了 GetAwaiter().GetResult() 方法,那么可能会发生死锁。这是因为 GetResult() 方法会阻塞当前线程,同时也会阻塞异步操作所使用的线程,导致这两个任务无法完成。
为了避免死锁,我们应该在异步上下文中使用异步操作,而不是使用 GetAwaiter().GetResult() 方法来等待异步操作的完成。可以使用 await Task.WhenAll(task1, task2); 来等待多个任务的完成。这样可以避免死锁,并且能够更好地利用异步操作的性能优势。
3、Await与GetAwait的区别
两者都用于等待异步操作完成,区别如下:
(1)语法和使用:
await 是一个关键字,可直接用于异步方法中,通过 await 等待一个返回 Task 或 Task<T> 类型的异步操作完成。使用 await 关键字时,编译器会自动为我们生成异步状态机,简化了异步编程的代码编写。而 GetAwaiter().GetResult() 是一个方法调用,手动等待任务完成,并获取任务的结果。。
(2)异常处理:
await 关键字在等待异步操作时会正确处理异常。如果异步操作抛出异常,await 会将该异常包装在 Task 或 Task<T> 中,并通过异常处理机制进行传播。相比之下,GetAwaiter().GetResult() 方法在等待异步操作时,如果异步操作抛出异常,异常将直接被抛出,而不会被包装在 Task 中。
这意味着如果你没有在实际发生异步操作的地方使用 try-catch 块来捕获异常,异常将会中断程序的执行,而不会被返回到调用处的 Task 对象中进行处理。因此,在使用 GetAwaiter().GetResult() 方法时,确保在发生异步操作的地方使用 try-catch 块来捕获异常,以便适当地处理异常情况。
(3)调用线程:
await 关键字在等待异步操作完成时,会暂时释放当前线程,以允许线程去执行其他任务,避免了阻塞。而 GetAwaiter().GetResult() 方法会阻塞当前线程,直到异步操作完成,如果是在主线程中使用此方法,可能会导致界面卡顿或死锁等问题。
(4)死锁风险:
使用 GetAwaiter().GetResult() 方法时,如果该方法的调用和异步操作所在的上下文处于同一线程上下文中(如 UI 线程中),并且异步操作中包含需要在同一上下文中执行的代码,就有可能发生死锁。而 await 关键字会自动处理上下文切换,避免了潜在的死锁风险。 总结:
await 关键字是 C# 异步编程的语法糖,提供了更简洁、安全和易用的方式来等待异步操作完成。GetAwaiter().GetResult() 方法是一种较底层的手动等待异步操作的方式,需要谨慎使用,避免可能的死锁和异常处理问题。
另外使用await具有异常的传导性,会将异步中的异常传递到主调线程。
这是因为 await 表达式会将异步操作的结果包装在一个 Task 对象中,并返回给主调线程。如果异步操作被取消或异常,那么 Task 对象的状态将变为已取消或异常,并且在主调线程上的 await 表达式中会抛出TaskCanceledException异常。
CancellationTokenSource cts = new CancellationTokenSource();Task task = Task.Run(() =>{while (!cts.IsCancellationRequested){Thread.Sleep(2000);cts.Token.ThrowIfCancellationRequested();//异步中抛出取消异步//throw new Exception("人为抛出");}}, cts.Token);try{cts.Cancel();await task;//此处捕获异常}catch (Exception ex){Console.WriteLine(ex.ToString());}
3、问:上面最后一个例子中第二参数有什么用处?
答:大多数情况下,第二参数用或不用都没有什么区别。
目前个人发的区别有两个:
(1)任务前取消,则异步线路不会启动。
CancellationTokenSource cts = new CancellationTokenSource();List<Task> tasks = new List<Task>();try{for (int i = 0; i < 10; i++){int j = i;tasks.Add(Task.Run(async () =>{Console.WriteLine($"执行任务 {j}开始");if (cts.Token.IsCancellationRequested)// 检查是否应该取消任务{//cts.Token.ThrowIfCancellationRequested();// 取消任务throw new Exception("异常中断");}await Task.Delay(1000);Console.WriteLine($"执行任务 {j}结束");}, cts.Token));await Task.Delay(5);if (j == 5){cts.Cancel();//a}}await Task.WhenAll(tasks.ToArray());//b}catch (TaskCanceledException){Console.WriteLine("任务被取消");}catch (Exception ex){Console.WriteLine("其他异常:" + ex.Message);}
上面在a处,序号为5时就取消任务,所以后面的6,7,8,9任务不会启动。(如果没有这个参数,那么后面的6-9任务会启动,但不会正常结束。简单地)
为了捕获异常,必须要用b处的await,不然异步虽然在Task中,但没有await时try无法捕捉。
(2)有第二参数,异步中异常会传递到主调线程中捕获。
没有第二个参数,异步中的异常直接抛出,中断程序,而不会隐忍不发传递给主调线程。
上面2中有第二参数,所以传递回主调线程捕获。下面没有第二参数
Task task = Task.Run(() =>{Thread.Sleep(2000);throw new Exception("人为抛出");//a});try{await task;//b}catch (Exception ex){ Console.WriteLine(ex.ToString()); }
上面在b处无法捕获异步,在a处直接抛出异常而中断程序。
4、被忽视的async,在异步异常中调试作用。
CancellationTokenSource cts = new CancellationTokenSource();List<Task> tasks = new List<Task>();try{for (int i = 0; i < 10; i++){int j = i;tasks.Add(Task.Run(async () =>//a{Console.WriteLine($"执行任务 {j}开始");if (cts.Token.IsCancellationRequested){//cts.Token.ThrowIfCancellationRequested();// 取消任务throw new Exception("异常中断");}if (j == 3){throw new Exception("异常中断");//b}await Task.Delay(1000);Console.WriteLine($"执行任务 {j}结束");}, cts.Token));//fawait Task.Delay(5);if (j == 5){cts.Cancel();}}await Task.WhenAll(tasks.ToArray());//c//Task.WaitAll(tasks.ToArray());//d}catch (AggregateException ee){Console.WriteLine("agg中断" + ee.Message);}catch (TaskCanceledException){Console.WriteLine("任务被取消");}catch (Exception ex){Console.WriteLine("其他异常:" + ex.Message);}
上面注释了c和d后,b处的异常也不会抛出,主调程序也不会捕获,为什么?
原因:
(1)a处的async是罪魁祸首。
当你取消async关键字时,异常会立即抛出并中断程序的执行。而当你使用async关键字时,异常会被封装在Task对象中,程序可以继续执行后续的代码,需要使用await关键字或Task.Wait()方法来等待任务的完成并捕获异常。
当任务遇到异常时,如果没有使用async关键字,异常将会立即抛出并中断程序的执行。这是因为没有使用async关键字时,任务是在同步上下文中执行的,异常会直接传播到调用方,导致程序中断。
而当你使用async关键字时,任务是在异步上下文中执行的。在异步上下文中,异常不会立即传播到调用方,而是被封装在Task对象中。这样,程序可以继续执行后续的代码,而不会中断。你可以通过await关键字或Task.Wait()方法来等待任务的完成,并捕获异常进行处理。
(2)主线程(调用方)没有使用含有异常的Task,将不会捕获
如果主线程(调用方)没有使用Task对象来等待异步任务的完成,那么它也无法捕获到异步任务中抛出的异常。这是因为异常是封装在Task对象中的,如果没有使用Task对象,异常就无法传播到主线程。
为了确保主线程能够捕获到异步任务中的异常,你需要使用Task对象来等待任务的完成,并在主线程中进行异常处理。这可以通过使用Task.WaitAll或await Task.WhenAll等待任务的完成来实现。这样,如果异步任务中发生异常,它会被传播到主线程,并可以在主线程中进行异常处理。
5、WhenAny()介绍
Task.WhenAny() 方法是 .NET 中的一个异步方法,它接受一个 Task 数组或可枚举对象作为参数,并返回一个新的任务,该任务将在其中任意一个输入任务完成时变为已完成状态。
当你想要等待多个任务中的任意一个完成时,可以使用 Task.WhenAny() 方法。它会等待其中任意一个任务完成,然后返回一个新的任务,该任务将在其中任意一个输入任务完成时变为已完成状态。
(1)Task.WhenAny() 方法接受一个 Task 数组或可枚举对象作为参数。你可以将所有要等待的任务放入一个数组中,并将该数组作为参数传递给 WhenAny() 方法。
(2)返回的任务将在其中任意一个输入任务完成时变为已完成状态。这意味着,只要有一个任务完成,返回的任务就会完成。 (3)返回的任务的结果类型是 Task<Task>,其中内部的 Task 对象表示已完成的任务。你可以通过访问返回任务的 Result 属性来获取已完成任务的结果。
Task<int> task1 = Task.Run(() =>{ return 32; });Task<string> task2 = Task.Run(() =>{Thread.Sleep(1000);return "task2";});Task<Task> firstTask = Task.WhenAny(task1, task2);//aawait firstTask;//bTask completedTask = await firstTask;//c//Task<object> completedTask = (Task<object>)await firstTask;//d//object obj = await completedTask;//Console.WriteLine(obj);if (completedTask == task1){int result = await (Task<int>)completedTask;//eConsole.WriteLine(result);}else{string result = await (Task<string>)completedTask;Console.WriteLine(result);}//object result = await (Task<object>)completedTask;//f//Console.WriteLine(result.ToString());
注意:
a处:它只是标注了一个会“首先完成”的任务,由于是异步,因此,实际它还没有执行或完成,只是作为以后这个首先完成的一个“引用”。
b处:真正的执行,同步等待,但不影响主线程,直到任务完成。
c处:因为返回类型是Task<Task>,需要用Await进行提取内层Task,相当于UnWrap(),与e处类似,也就是说,任务已经执行完成了,现在只是提取内层结果,而不是再执行一次。因为c处的存在,b处实际上是可以取消的,这时候的c处具有两个功能了,执行与提取内层。
d处:原想进行强制转换,这样后面就可以直接显示结果,但是写法正确,但执行异常,估计内部包裹太多,类型的转换出错的地点难以预料,舍弃。
e处:因为确定了是对应的具体类型,所以这里转换非常顺序,然后再次用await提取Task<int>内层结果,即int。所以成功。实际上可以直接用task1或task2来显示,但这样为了看看它的转换。
f处:同样写法正确,但执行异常,原因不明啊。。
(4)如果输入任务数组中的任何一个任务失败(即抛出了异常),返回的任务也将失败,并且会抛出一个聚合异常,其中包含了所有任务的异常信息。
(5)你可以使用 await 或 ContinueWith() 等方法来等待返回的任务的完成。当返回的任务完成时,表示其中任意一个输入任务已经完成。修改上面部分:
Console.WriteLine("前:" + DateTime.Now);Task.WhenAny(task1, task2).ContinueWith(completedTask =>{if (completedTask.Result == task1){int result = ((Task<int>)completedTask.Result).Result;Console.WriteLine(result);}else if (completedTask.Result == task2){string result = ((Task<string>)completedTask.Result).Result;Console.WriteLine(result);}});Console.WriteLine("后:" + DateTime.Now);
结果:
前:2023/9/9 17:55:45
后:2023/9/9 17:55:45
32
说明continuewith只是完成后的下步任务的执行,但并不能改变它是异步执行顺序。
我们使用 Task.WhenAny() 方法创建了一个任务 firstTask,该任务将在其中任意一个任务完成时变为已完成状态。
注意,Task.WhenAny() 方法是一个异步方法,需要在异步上下文中使用。在异步方法中使用 await 关键字等待 Task.WhenAny() 方法的完成,或者使用 ContinueWith() 方法注册一个回调函数来处理任务的完成。
6、总结:带Wait为同步,带When为异步
Task 类中的方法可以分为两类:同步方法和异步方法。
带有 "Wait" 字样的方法是同步方法,会阻塞当前线程,直到任务完成。而带有 "When" 字样的方法是异步方法,返回一个 Task 对象,用于等待多个任务的完成。
(1)同步方法:
这些方法会阻塞当前线程,直到任务完成。它们通常以 "Wait" 结尾,如 Task.Wait()、Task.WaitAny() 和 Task.WaitAll()。这些方法会一直等待,直到任务完成或超时。这些方法返回的是 void 或 int 值,用于指示任务是否已经完成。
(2)异步方法:
这些方法不会阻塞当前线程,而是返回一个 Task 对象,表示异步操作的进行。它们通常以 "When" 开头,如 Task.WhenAny() 和 Task.WhenAll()。这些方法返回的是一个 Task 对象,可以使用 await 关键字等待任务的完成。
注意,虽然 Task.WhenAny() 和 Task.WhenAll() 是异步方法,但它们本身不会执行任何实际的异步操作。它们只是用于等待多个任务中的任意一个或全部任务完成,并返回一个表示完成的 Task 对象。