zoukankan      html  css  js  c++  java
  • .NET:CLR via C# Compute-Bound Asynchronous Operations

    线程槽

    使用线程池了以后就不要使用线程槽了,当线程池执行完调度任务后,线程槽的数据还在。

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading;
     6 using System.Threading.Tasks;
     7 using System.Runtime.Remoting;
     8 
     9 namespace ExecutionContextStudy
    10 {
    11     class Program
    12     {
    13         static void Main(string[] args)
    14         {
    15             for (var i = 0; i < 10; i++)
    16             {
    17                 Thread.Sleep(10);
    18 
    19                 Task.Run(() =>
    20                 {
    21                     var slot = Thread.GetNamedDataSlot("test");
    22                     if (slot == null)
    23                     {
    24                         Thread.AllocateNamedDataSlot("test");
    25                     }
    26 
    27                     if (Thread.GetData(slot) == null)
    28                     {
    29                         Thread.SetData(slot, DateTime.Now.Millisecond);
    30                     }
    31 
    32                     Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":" + Thread.GetData(slot));
    33                 });
    34             }
    35 
    36             Console.ReadLine();
    37         }
    38     }
    39 }

    输出结果

    CallContext

    CallContext 可以避免线程槽的问题。

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading;
     6 using System.Threading.Tasks;
     7 using System.Runtime.Remoting.Messaging;
     8 
     9 namespace ExecutionContextStudy
    10 {
    11     class CallContextTest
    12     {
    13         public static void Test()
    14         {
    15             Console.WriteLine("测试:CallContext.SetData");
    16             for (var i = 0; i < 10; i++)
    17             {
    18                 Thread.Sleep(10);
    19 
    20                 Task.Run(() =>
    21                 {
    22                     if (CallContext.GetData("test") == null)
    23                     {
    24                         CallContext.SetData("test", DateTime.Now.Millisecond);
    25                     }
    26 
    27                     Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":" + CallContext.GetData("test"));
    28                 });
    29             }
    30 
    31             Console.WriteLine("测试:CallContext.LogicalSetData");
    32             for (var i = 0; i < 10; i++)
    33             {
    34                 Thread.Sleep(10);
    35 
    36                 Task.Run(() =>
    37                 {
    38                     if (CallContext.LogicalGetData("test") == null)
    39                     {
    40                         CallContext.LogicalSetData("test", DateTime.Now.Millisecond);
    41                     }
    42 
    43                     Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":" + CallContext.LogicalGetData("test"));
    44                 });
    45             }
    46 
    47             Console.ReadLine();
    48         }
    49     }
    50 }

    输出结果

    Execution Contexts

    Every thread has an execution context data structure associated with it. The execution context includes things such as security settings (compressed stack, Thread’s Principal property, and Windows identity), host settings (see System.Threading.HostExecutionContextManager), and logical call context data (see System.Runtime.Remoting.Messaging.CallContext’s LogicalSetData and LogicalGetData methods). When a thread executes code, some operations are affected by the values of the thread’s execution context settings. This is certainly true for the security settings. Ideally, whenever a thread uses another (helper) thread to perform tasks, the issuing thread’s execution context should flow (be copied) to the helper thread. This ensures that any operations performed by helper thread(s) are executing with the same security settings and host settings. It also CHAPTER 27 Compute-Bound Asynchronous Operations 695 ensures that any data stored in the initiating thread’s logical call context is available to the helper thread.

    By default, the CLR automatically causes the initiating thread’s execution context to flow to any helper threads. This transfers context information to the helper thread, but it comes at a performance cost, because there is a lot of information in an execution context, and accumulating all of this information and then copying it for the helper thread takes a fair amount of time. If the helper thread then employs additional helper threads, then more execution context data structures have to be created and initialized as well.

    In the System.Threading namespace, there is an ExecutionContext class that allows you to control how a thread’s execution context flows from one thread to another.

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading;
     6 using System.Threading.Tasks;
     7 using System.Runtime.Remoting.Messaging;
     8 
     9 namespace ExecutionContextStudy
    10 {
    11     class ExecutionContextTest
    12     {
    13         public static void Test()
    14         {
    15             Console.WriteLine("测试:CallContext.SetData");
    16             Task.Run(() =>
    17             {
    18                 CallContext.SetData("test", "段光伟");
    19                 Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":" + CallContext.GetData("test"));
    20 
    21                 Task.Run(() =>
    22                 {
    23                     Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":" + CallContext.GetData("test"));
    24                 });
    25             });
    26 
    27             Thread.Sleep(100);
    28 
    29             Console.WriteLine("测试:CallContext.LogicalSetData");
    30             Task.Run(() =>
    31             {
    32                 CallContext.LogicalSetData("test", "段光伟");
    33                 Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":" + CallContext.LogicalGetData("test"));
    34 
    35                 Task.Run(() =>
    36                 {
    37                     Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":" + CallContext.LogicalGetData("test"));
    38                 });
    39 
    40                 ExecutionContext.SuppressFlow();
    41                 Task.Run(() =>
    42                 {
    43                     Console.WriteLine("SuppressFlow 之后:" + CallContext.LogicalGetData("test"));
    44                 });
    45 
    46                 ExecutionContext.RestoreFlow();
    47                 Task.Run(() =>
    48                 {
    49                     Console.WriteLine("RestoreFlow 之后:" + CallContext.LogicalGetData("test"));
    50                 });
    51             });
    52 
    53             Console.ReadLine();
    54         }
    55     }
    56 }

    输出结果

    Cooperative Cancellation and Timeout

    To cancel an operation, you must first create a System.Threading.CancellationTokenSource object. This object contains all the states having to do with managing cancellation. After constructing a CancellationTokenSource (a reference type), one or more CancellationToken (a value type) instances can be obtained by querying its Token property and passed around to your operations that CHAPTER 27 Compute-Bound Asynchronous Operations 697 allow themselves to be canceled. Here are the most useful members of the CancellationToken value type.

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 using System.Threading;
     7 
     8 namespace ExecutionContextStudy
     9 {
    10     class CancellationTest
    11     {
    12         public static void Test()
    13         {
    14             CancellationTokenSource cts = new CancellationTokenSource();
    15 
    16             ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 10));
    17             Console.WriteLine("Press <Enter> to cancel the operation.");
    18             Console.ReadLine();
    19             cts.Cancel();
    20             Console.ReadLine();
    21         }
    22 
    23         private static void Count(CancellationToken token, Int32 countTo)
    24         {
    25             for (Int32 count = 0; count < countTo; count++)
    26             {
    27                 if (token.IsCancellationRequested)
    28                 {
    29                     Console.WriteLine("Count is cancelled");
    30                     break;
    31                 }
    32                 Console.WriteLine(count);
    33                 Thread.Sleep(200);
    34             }
    35             Console.WriteLine("Count is done");
    36         }
    37     }
    38 }

    输出结果

    If you’d like, you can call CancellationTokenSource’s Register method to register one or more methods to be invoked when a CancellationTokenSource is canceled. To this method, you pass an Action<Object> delegate, a state value that will be passed to the callback via the delegate, and a Boolean indicating whether or not to invoke the delegate by using the calling thread’s SynchronizationContext. If you pass false for the useSynchronizationContext parameter, then the thread that calls Cancel will invoke all the registered methods sequentially. If you pass true for the useSynchronizationContext parameter, then the callbacks are sent (as opposed to posted) to the captured SynchronizationContext object that governs which thread invokes the callback.

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 using System.Threading;
     7 
     8 namespace ExecutionContextStudy
     9 {
    10     class CancellationTest
    11     {
    12         public static void Test()
    13         {
    14             CancellationTokenSource cts = new CancellationTokenSource();
    15 
    16             cts.Token.Register(() =>
    17             {
    18                 Console.WriteLine("Cancel callback 1,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    19             });
    20             cts.Token.Register(() =>
    21             {
    22                 Console.WriteLine("Cancel callback 2,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    23             }, true);
    24 
    25 
    26             ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 10));
    27             Console.WriteLine("Press <Enter> to cancel the operation.");
    28             Console.ReadLine();
    29             cts.Cancel();
    30             cts.Token.Register(() =>
    31             {
    32                 Console.WriteLine("Cancel callback 3,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    33             });
    34             cts.Token.Register(() =>
    35             {
    36                 Console.WriteLine("Cancel callback 4,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    37             }, true);
    38             Console.ReadLine();
    39         }
    40 
    41         private static void Count(CancellationToken token, Int32 countTo)
    42         {
    43             for (Int32 count = 0; count < countTo; count++)
    44             {
    45                 if (token.IsCancellationRequested)
    46                 {
    47                     Console.WriteLine("Count is cancelled,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    48                     break;
    49                 }
    50                 Console.WriteLine(count);
    51                 Thread.Sleep(200);
    52             }
    53             Console.WriteLine("Count is done,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    54         }
    55     }
    56 }

    输出结果

    If Register is called multiple times, then multiple callback methods will be invoked. These callback methods could throw an unhandled exception. If you call CancellationTokenSource’s Cancel, passing it true, then the first callback method that throws an unhandled exception stops the other callback methods from executing, and the exception thrown will be thrown from Cancel as well. If you call Cancel passing it false, then all registered callback methods are invoked. Any unhandled exceptions that occur are added to a collection. After all callback methods have executed, if any of them threw an unhandled exception, then Cancel throws an AggregateException with its InnerExceptions property set to the collection of exception objects that were thrown. If no registered callback methods threw an unhandled exception, then Cancel simply returns without throwing any exception at all.

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 using System.Threading;
     7 
     8 namespace ExecutionContextStudy
     9 {
    10     class CancellationTest
    11     {
    12         public static void Test()
    13         {
    14             CancellationTokenSource cts = new CancellationTokenSource();
    15 
    16             cts.Token.Register(() =>
    17             {
    18                 Console.WriteLine("Cancel callback 1,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    19             });
    20             cts.Token.Register(() =>
    21             {
    22                 throw new Exception("抛出异常");
    23                 Console.WriteLine("Cancel callback 2,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    24             }, true);
    25 
    26 
    27             ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 10));
    28             Console.WriteLine("Press <Enter> to cancel the operation.");
    29             Console.ReadLine();
    30 
    31             try
    32             {
    33                 cts.Cancel();
    34             }
    35             catch (Exception ex)
    36             {
    37                 Console.WriteLine(ex.Message);
    38             }
    39 
    40             cts.Token.Register(() =>
    41             {
    42                 Console.WriteLine("Cancel callback 3,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    43             });
    44             cts.Token.Register(() =>
    45             {
    46                 Console.WriteLine("Cancel callback 4,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    47             }, true);
    48             Console.ReadLine();
    49         }
    50 
    51         private static void Count(CancellationToken token, Int32 countTo)
    52         {
    53             for (Int32 count = 0; count < countTo; count++)
    54             {
    55                 if (token.IsCancellationRequested)
    56                 {
    57                     Console.WriteLine("Count is cancelled,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    58                     break;
    59                 }
    60                 Console.WriteLine(count);
    61                 Thread.Sleep(200);
    62             }
    63             Console.WriteLine("Count is done,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    64         }
    65     }
    66 }

    输出结果

    you can create a new CancellationTokenSource object by linking a bunch of other CancellationTokenSource objects. This new CancellationTokenSource object will be canceled when any of the linked CancellationTokenSource objects are canceled. 

    It is often valuable to cancel an operation after a period of time has elapsed. For example, imagine a server application that starts computing some work based on a client request. But the server application needs to respond to the client within two seconds, no matter what. In some scenarios, it is better to respond in a short period of time with an error or with partially computed results as opposed to waiting a long time for a complete result. Fortunately, CancellationTokenSource gives you a way to have it self-cancel itself after a period of time. To take advantage of this, you can either construct a CancellationTokenSource object by using one of the constructors that accepts a delay, or you can call CancellationTokenSource’s CancelAfter method.

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 using System.Threading;
     7 
     8 namespace ExecutionContextStudy
     9 {
    10     class CancellationTest
    11     {
    12         public static void Test()
    13         {
    14             CancellationTokenSource cts = new CancellationTokenSource();
    15 
    16             cts.Token.Register(() =>
    17             {
    18                 Console.WriteLine("Cancel callback 1,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    19             });
    20             cts.Token.Register(() =>
    21             {
    22                 Console.WriteLine("Cancel callback 2,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    23             }, true);
    24 
    25 
    26             ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 10));
    27             Console.WriteLine("Press <Enter> to cancel the operation.");
    28             Console.ReadLine();
    29 
    30             cts.CancelAfter(500);
    31 
    32             cts.Token.Register(() =>
    33             {
    34                 Console.WriteLine("Cancel callback 3,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    35             });
    36             cts.Token.Register(() =>
    37             {
    38                 Console.WriteLine("Cancel callback 4,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    39             }, true);
    40             Console.ReadLine();
    41         }
    42 
    43         private static void Count(CancellationToken token, Int32 countTo)
    44         {
    45             for (Int32 count = 0; count < countTo; count++)
    46             {
    47                 if (token.IsCancellationRequested)
    48                 {
    49                     Console.WriteLine("Count is cancelled,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    50                     break;
    51                 }
    52                 Console.WriteLine(count);
    53                 Thread.Sleep(200);
    54             }
    55             Console.WriteLine("Count is done,In Thread:" + Thread.CurrentThread.ManagedThreadId);
    56         }
    57     }
    58 }

    输出结果

    Task

    If the compute-bound task throws an unhandled exception, the exception will be swallowed, stored in a collection, and the thread pool thread is allowed to return to the thread pool. When the Wait method or the Result property is invoked, these members will throw a System.AggregateException object.

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 using System.Threading;
     7 
     8 namespace TaskProgramming
     9 {
    10     class _003_Exception
    11     {
    12         public static void Test()
    13         {
    14             var task = Task.Factory.StartNew(() =>
    15             {
    16                 Console.WriteLine("start");
    17                 throw new Exception("xxx");
    18             });
    19 
    20             Thread.Sleep(100);
    21 
    22             Console.WriteLine("IsFaulted:" + task.IsFaulted);
    23             Console.WriteLine("IsCompleted:" + task.IsCompleted);
    24             Console.WriteLine("Error:" + task.Exception.InnerExceptions.First().Message);
    25 
    26             try
    27             {
    28                 task.Wait();
    29             }
    30             catch (AggregateException ex)
    31             {
    32                 Console.WriteLine("Error:" + ex.InnerExceptions.First().Message);
    33             }
    34         }
    35     }
    36 }

    输出结果

    The AggregateException type is used to encapsulate a collection of exception objects (which can happen if a parent task spawns multiple child tasks that throw exceptions).

    Canceling a Task

    When creating a Task, you can associate a CancellationToken with it by passing it to Task’s constructor (as shown in the preceding code). If the CancellationToken gets canceled before the Task is scheduled, the Task gets canceled and never executes at all.2 But if the Task has already been scheduled (by calling the Start method), then the Task’s code must explicitly support cancellation if it allows its operation to be canceled while executing. Unfortunately, while a Task object has a CancellationToken associated with it, there is no way to access it, so you must somehow get the same CancellationToken that was used to create the Task object into the Task’s code itself. The easiest way to write this code is to use a lambda expression and “pass” the CancellationToken as a closure variable (as I’ve done in the previous code example).

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 using System.Threading;
     7 
     8 namespace TaskProgramming
     9 {
    10     class _004_Cancellation
    11     {
    12         public static void Test()
    13         {
    14             CancellationTokenSource cts = new CancellationTokenSource();
    15             var ct = cts.Token;
    16 
    17             var task = Task.Factory.StartNew(() =>
    18             {
    19                 for (var i = 0; i < 20; i++)
    20                 {
    21                     Thread.Sleep(500);
    22 
    23                     ct.ThrowIfCancellationRequested();
    24                     Console.WriteLine(i);
    25                 }
    26             }, ct);
    27 
    28             Console.WriteLine("输入 Enter 键取消计数");
    29             Console.ReadLine();
    30             cts.Cancel();
    31 
    32             Thread.Sleep(1000);
    33 
    34             try
    35             {
    36                 task.Wait();
    37             }
    38             catch(AggregateException ex)
    39             {
    40                 Console.WriteLine(ex.InnerException.GetType());
    41                 Console.WriteLine("IsCanceled:" + task.IsCanceled);
    42                 Console.WriteLine("IsCompleted:" + task.IsCompleted);
    43                 Console.WriteLine("IsFaulted:" + task.IsFaulted);
    44             }
    45         }
    46     }
    47 }

    运行结果

    Starting a New Task Automatically When Another Task Completes

    When you call ContinueWith, you can indicate that you want the new task to execute only if the first task is canceled by specifying the TaskContinuationOptions.OnlyOnCanceled flag. Similarly, you have the new task execute only if the first task throws an unhandled exception using the TaskContinuationOptions. OnlyOnFaulted flag. And, of course, you can use the TaskContinuationOptions. OnlyOnRanToCompletion flag to have the new task execute only if the first task runs all the way to completion without being canceled or throwing an unhandled exception. By default, if you do not specify any of these flags, then the new task will run regardless of how the first task completes. When a Task completes, any of its continue-with tasks that do not run are automatically canceled.

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 using System.Threading;
     7 
     8 namespace TaskProgramming
     9 {
    10     class _005_Continuation
    11     {
    12         public static void Test()
    13         {
    14             var task = Task.Factory.StartNew(() =>
    15             {
    16                 Thread.Sleep(1000);
    17 
    18                 return "段光伟";
    19             });
    20 
    21             var t1 = task.ContinueWith(x =>
    22             {
    23                 Console.WriteLine(x.Result);
    24             });
    25 
    26             var t2 = task.ContinueWith(x =>
    27             {
    28                 Console.WriteLine("OnlyOnCanceled:" + x.Result);
    29             }, TaskContinuationOptions.OnlyOnCanceled);
    30 
    31             var t3 = task.ContinueWith(x =>
    32             {
    33                 Console.WriteLine("OnlyOnFaulted:" + x.Result);
    34             }, TaskContinuationOptions.OnlyOnFaulted);
    35 
    36             var t4 = task.ContinueWith(x =>
    37             {
    38                 Console.WriteLine("OnlyOnRanToCompletion:" + x.Result);
    39             }, TaskContinuationOptions.OnlyOnRanToCompletion);
    40 
    41             Console.ReadLine();
    42 
    43             Console.WriteLine("t2.IsCanceled:" + t2.IsCanceled);
    44             Console.WriteLine("t2.IsCompleted:" + t2.IsCompleted);
    45         }
    46     }
    47 }

    输出结果

    A Task May Start Child Tasks

    By default, Task objects created by another task are top-level tasks that have no relationship to the task that creates them. However, the TaskCreationOptions.AttachedToParent flag associates a Task with the Task that creates it so that the creating task is not considered finished until all its children (and grandchildren) have finished running. When creating a Task by calling the ContinueWith method, you can make the continuewith task be a child by specifying the TaskContinuationOptions.AttachedToParent flag.

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 using System.Threading;
     7 
     8 namespace TaskProgramming
     9 {
    10     class _006_Child
    11     {
    12         public static void Test()
    13         {
    14             var parent = new Task(() =>
    15             {
    16                 Console.WriteLine("Parent");
    17 
    18                 new Task(() =>
    19                 {
    20                     Thread.Sleep(100);
    21                     Console.WriteLine("Child #1");
    22                 }, TaskCreationOptions.AttachedToParent).Start();
    23 
    24                 new Task(() =>
    25                 {
    26                     Thread.Sleep(150);
    27                     Console.WriteLine("Child #2");
    28                 }, TaskCreationOptions.AttachedToParent).Start();
    29             });
    30 
    31             parent.ContinueWith((x) =>
    32             {
    33                 Console.WriteLine("Child #3");
    34             });
    35 
    36             parent.Start();
    37             Console.ReadLine();
    38         }
    39     }
    40 }

    输出结果

    Inside a Task

    When you first construct a Task object, its status is Created. Later, when the task is started, its status changes to WaitingToRun. When the Task is actually running on a thread, its status changes CHAPTER 27 Compute-Bound Asynchronous Operations 709 to Running. When the task stops running and is waiting for any child tasks, the status changes to WaitingForChildrenToComplete. When a task is completely finished, it enters one of three final states: RanToCompletion, Canceled, or Faulted. When a Task<TResult> runs to completion, you can query the task’s result via Task<TResult>’s Result property. When a Task or Task<TResult> faults, you can obtain the unhandled exception that the task threw by querying Task’s Exception property; which always returns an AggregateException object whose collection contains the set of unhandled exceptions.

    A Task object is in the WaitingForActivation state if that Task is created by calling one of these functions: ContinueWith, ContinueWhenAll, ContinueWhenAny, or FromAsync. A Task created by constructing a TaskCompletionSource< TResult> object is also created in the WaitingForActivation state. This state means that the Task’s scheduling is controlled by the task infrastructure. For example, you cannot explicitly start a Task object that was created by calling ContinueWith. This Task will start automatically when its antecedent task has finished executing.

    测试代码

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 using System.Threading;
     7 
     8 namespace TaskProgramming
     9 {
    10     class _006_ContinuteWhen
    11     {
    12         public static void Test()
    13         {
    14             var t1 = Task.Run(() =>
    15             {
    16                 Thread.Sleep(100);
    17                 Console.WriteLine("A");
    18             });
    19 
    20             var t2 = Task.Run(() =>
    21             {
    22                 Thread.Sleep(100);
    23                 Console.WriteLine("B");
    24             });
    25 
    26             Task.Factory.ContinueWhenAll(new Task[] { t1, t2 }, (ts) =>
    27             {
    28                 Thread.Sleep(100);
    29                 Console.WriteLine("C");
    30             });
    31 
    32             Console.ReadLine();
    33         }
    34     }
    35 }

    输出结果

  • 相关阅读:
    InstallShield 2010集成.net Framework 4的安装包制作
    linux之稀疏文件
    linux之od命令
    Python多线程
    GCC编译器和GDB调试器常用选项
    Linux GDB Debug
    Linux Core Dump
    linux 通过 ulimit 改善系统性能
    linux的ulimit命令
    字符编码笔记:ASCII,Unicode和UTF-8
  • 原文地址:https://www.cnblogs.com/happyframework/p/3409571.html
Copyright © 2011-2022 走看看