简介:
线程池的优势与不足:使用线程池可以使我们在减少并行度花销时节省操作系统资源。我们可以认为线程池是一个 抽象层,其向程序员隐藏了使用线程的细节,使我们专心处理程序逻辑,而不是各种线程问题。但使用线程池也相当复杂。从线程池的工作者线程中获取结果并不容易。我们需要实现自定义方式来获取结果,而且万一有异常发生,还需将异常正确地传播到初始线程中。除此以外,创建一组相关的异步操作,以及实现当前操作执行完成后下一操作才会执行的逻辑也不容易。
任务并行库Task Parallel Library,简称 TPL)产生的原因:在尝试解决这些问题的过程中,创建了异步编程模型及基于事件的异步模式。在第3章中提到过基于事件的异步模式。这些模式使得获取结果更容易,传播异常也更轻松,但是组 合多个异步操作仍需大量工作,需要编写大量的代码。为了解决所有的问题,.Net Framework4.0引入了一个新的关于异步操作的API。它叫做任务并行库.
Framework 4.5 版对该 API 进行了 轻微 的改进,使用更简单。在本书的项目中将使用最新版的TPL,即.Net Framework 4.5版中的 ,TPL可被认为是线程池之上的又一个抽象层,其对程序员隐藏了与线程池交互的底层 代码,并提供了更方便的细粒度的API。
TPL的核心概念是任务。一个任务代表了一个异步操作,该操作可以通过多种方式运 行,可以使用或不使用独立线程运行。在本章中将探究任务的所有使用细节。
1. 创建任务(多种方式)
using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using static System.Console; using static System.Threading.Thread; namespace Recipe1 { class Program { static void Main(string[] args) { //从Task 1到Task 4的所有任务都被放置在线程池的工作者线程中并以未指 定的顺序运行。 //如果多次运行该程序,就会发现任务的执行顺序是不确定的. var t1 = new Task(() => TaskMethod("Task 1")); var t2 = new Task(() => TaskMethod("Task 2")); t2.Start(); t1.Start(); Task.Run(() => TaskMethod("Task 3")); Task.Factory.StartNew(() => TaskMethod("Task 4")); //Task.Run方法只是Task.Factory.StartNew的一个快捷方式,但是后者有附加的选项。通常 如果无特殊需求,则可使用前一个方法, //如Task 5所示。我们标记该任务为长时间运行,结果 该任务将不会使用线程池,而在单独的线程中运行。 Task.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning); Sleep(TimeSpan.FromSeconds(1)); Read(); } static void TaskMethod(string name) { WriteLine($"Task {name} is running on a thread id " + $"{CurrentThread.ManagedThreadId}. Is thread pool thread: " + $"{CurrentThread.IsThreadPoolThread}"); } } }多种方式执行并行库
结果:任务是否线程池任务
2. 从任务中获取结果值
using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using static System.Console; using static System.Threading.Thread; namespace Recipe1 { class Program { static void Main(string[] args) { //首先直接运行TaskMethod方法,这里并没有把它封装到一个任务中。 //结果根据它提供 给我们的主线程的信息可以得知该方法是被同步执行的。很显然它不是线程池中的线程 TaskMethod("Main Thread Task"); // Task 1,使用Start方法启动该任务并等待结果.该任务会被放置在线程 池中, // 并且主线程会等待,直到任务返回前一直处于阻塞状态。 Task<int> task = CreateTask("Task 1"); task.Start(); int result = task.Result; WriteLine($"Result is: {result}"); //Task 2是通过RunSynchronously()方法运行的。该任务会 运行在主线程中, //该任务的输出与第一个例子中直接同步调用TaskMethod的输出完全一样。 //这是个非常好的优化,可以避免使用絞程池来执行非常短暂的操作。 task = CreateTask("Task 2"); task.RunSynchronously(); result = task.Result; WriteLine($"Result is: {result}"); //用以运行Task 1相同的方式来运行Task 3O但这次没有阻塞主线程,只是在该 任务完成前循环打印出任务状态。 //结果展示了多种任务状态,分别是Created. Running和 RanToCompletion task = CreateTask("Task 3"); WriteLine(task.Status); task.Start(); while (!task.IsCompleted) { WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(0.5)); } WriteLine(task.Status); result = task.Result; WriteLine($"Result is: {result}"); Read(); } static Task<int> CreateTask(string name) { return new Task<int>(() => TaskMethod(name)); } static int TaskMethod(string name) { WriteLine($"Task {name} is running on a thread id " + $"{CurrentThread.ManagedThreadId}. Is thread pool thread: " + $"{CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(2)); return 42; } } }获取任务结果
3. 组合任务(展示如何设置相互依赖的任务,后续任务,后续的后续,或者父任务要等着主任务完成)
using System; using System.Threading.Tasks; using static System.Console; using static System.Threading.Thread; namespace Recipe1 { class Program { static void Main(string[] args) { var firstTask = new Task<int>(() => TaskMethod("First Task", 3)); var secondTask = new Task<int>(() => TaskMethod("Second Task", 2)); //为第一个任务设置了一个后续操作(continuation, 一个代码块,会在当前任务完成后运行)。 //然后启动这两个任务并等待4秒,这个时间足够两个任务完成(都是线程任务) firstTask.ContinueWith( t => WriteLine( $"The first answer is {t.Result}. Thread id " + $"{CurrentThread.ManagedThreadId}, is thread pool thread: " + $"{CurrentThread.IsThreadPoolThread}"), TaskContinuationOptions.OnlyOnRanToCompletion); firstTask.Start(); //如果没有这句,可能第二个线程也在并行运行了。 secondTask.Start(); Sleep(TimeSpan.FromSeconds(4)); //给第二个任务运行另一个后续操作,并通过指定TaskContinuationOptions. ExecuteSynchronously //选项来尝试同步执行该后续操作。如果后续操作耗时非常短暂,使用 以上方式是非常有用的, //因为放置在主线程中运行比放置在线程池中运行要快。可以实现这 一点是因为第二个任务恰好在那刻完成。 //如果注释掉4秒的Thread.Sleep方法,将会看到该 代码被放置到线程池中,这是因为还未从之前的任务中得到结果。 Task continuation = secondTask.ContinueWith( t => WriteLine( $"The second answer is {t.Result}. Thread id " + $"{CurrentThread.ManagedThreadId}, is thread pool thread: " + $"{CurrentThread.IsThreadPoolThread}"), TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously); //最后我们为之前的后续操作也定义了一个后续操作,但这里使用了一个稍微不同的方 式,即使用了新的 //GetAwaiter和OnCompleted方法。这些方法是C# 5.0语言中异步机制中 的方法 continuation.GetAwaiter().OnCompleted( () => WriteLine( $"Continuation Task Completed! Thread id " + $"{CurrentThread.ManagedThreadId}, is thread pool thread: " + $"{CurrentThread.IsThreadPoolThread}")); Sleep(TimeSpan.FromSeconds(2)); WriteLine(); //最后部分与父子线程有关。我们创建了一个新任务,当运行该任务时,通过 提供一个TaskCreationOptions.AttachedToParent //选项来运行一个所谓的子任务。 子任务必须在父任务运行时创建,并正确的附加给父任务! //这意味着只有所有子任务结束工作,父任务才会完成,通过提供一个TaskContinuation - Options选项也可以给在 //子任务上运行后续操作。该后续操作也会影响父任务,并且直到最 后一个子任务结束它才会运行完成。 firstTask = new Task<int>(() => { var innerTask = Task.Factory.StartNew(() => TaskMethod("Second Task", 5), TaskCreationOptions.AttachedToParent); innerTask.ContinueWith(t => TaskMethod("Third Task", 2), TaskContinuationOptions.AttachedToParent); return TaskMethod("First Task", 2); }); firstTask.Start(); while (!firstTask.IsCompleted) { WriteLine(firstTask.Status); Sleep(TimeSpan.FromSeconds(0.5)); } WriteLine(firstTask.Status); Sleep(TimeSpan.FromSeconds(10)); Read(); } static int TaskMethod(string name, int seconds) { WriteLine( $"Task {name} is running on a thread id " + $"{CurrentThread.ManagedThreadId}. Is thread pool thread: " + $"{CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(seconds)); return 42 * seconds; } } }多个任务组合在一起
4. 将过时的APM(异步委托) API转换为任务
using System; using System.Threading.Tasks; using static System.Console; using static System.Threading.Thread; namespace Recipe1 { class Program { //委托 delegate string AsynchronousTask(string threadName); delegate string IncompatibleAsynchronousTask(out int threadId); static void Main(string[] args) { int threadId; AsynchronousTask d = Test; IncompatibleAsynchronousTask e = Test; //APM模式(异步委托)转换为任务 时,与标准的TPLAPI是不兼容的。这样的转换有三个示例 //将APM转换为TPL的关键点是Task<T>.Factory.FromAsync方法,T是异步操作结果的类 //型。该方法有数个重载。 WriteLine("Option 1"); //第一个例子中传入了 lAsyncResult和FuncvIAsyncResult, string>, //这是一个将lAsyncResult的实现作为参数并返回一个字符串的方法 Task<string> task = Task<string>.Factory.FromAsync( d.BeginInvoke("AsyncTaskThread", Callback, "a delegate asynchronous call"), d.EndInvoke); task.ContinueWith(t => WriteLine( $"Callback is finished, now running a continuation! Result: {t.Result}")); //可以在等待异步操作结果过程中打印出任务状态,从而了解底层任务的运行情况。 //可以 看到第一个任务的状态为WaitingForActivation,这意味着TPL基础设施实际上还未启动该 任务 while (!task.IsCompleted) { WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(0.5)); } WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(1)); WriteLine("----------------------------------------------"); WriteLine(); WriteLine("Option 2"); //使用了不同的FromAsync方法重载,该重载 并不允许指定一个将会在异步委托调用完成后被调用的回调函数但我们可以使用后续操作 替代它。 //但如果回调函数很重要,可以使用第一个例子所示的方法 task = Task<string>.Factory.FromAsync( d.BeginInvoke, d.EndInvoke, "AsyncTaskThread", "a delegate asynchronous call"); task.ContinueWith(t => WriteLine( $"Task is completed, now running a continuation! Result: {t.Result}")); while (!task.IsCompleted) { WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(0.5)); } WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(1)); WriteLine("----------------------------------------------"); WriteLine(); WriteLine("Option 3"); //最后一个例子展示T —个小技巧°这次IncompatibleAsynchronousTask委托的 EndMethod使用了out参数,与FromAsync方法重载并不兼容。 //然而,可以很容易地将 EndMethod调用封装到一个lambda表达式中,从而适合任务工厂方法 IAsyncResult ar = e.BeginInvoke(out threadId, Callback, "a delegate asynchronous call"); task = Task<string>.Factory.FromAsync(ar, _ => e.EndInvoke(out threadId, ar)); task.ContinueWith(t => WriteLine( $"Task is completed, now running a continuation! " + $"Result: {t.Result}, ThreadId: {threadId}")); while (!task.IsCompleted) { WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(0.5)); } WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(1)); Read(); } static void Callback(IAsyncResult ar) { WriteLine("Starting a callback..."); WriteLine($"State passed to a callbak: {ar.AsyncState}"); WriteLine($"Is thread pool thread: {CurrentThread.IsThreadPoolThread}"); WriteLine($"Thread pool worker thread id: {CurrentThread.ManagedThreadId}"); } static string Test(string threadName) { WriteLine("Starting Test with threadName..."); WriteLine($"Is thread pool thread: {CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(2)); CurrentThread.Name = threadName; return $"Thread name: {CurrentThread.Name}"; } static string Test(out int threadId) { WriteLine("Starting Test with threadId..."); WriteLine($"Is thread pool thread: {CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(2)); threadId = CurrentThread.ManagedThreadId; return $"Thread pool worker thread id was: {threadId}"; } } }异步委托转为任务
5. 将EAP(基于事件的异步操作)模式转换为任务
class Program { static void Main(string[] args) { //这是一个将EAP模式转换为任务的既简单又优美的示例。关键点在于使用Task- CompletionSource<T>类型, //T是异步操作结果类型。不要忘记将tcs.SetResult调用封装在try - catch代码块中,从而保证错误信息始终会设置 //给任务完成源对象。也可以使用TrySetResult方法来替代SetResult方法,以保证结果能被成 功设置。 var tcs = new TaskCompletionSource<int>(); var worker = new BackgroundWorker(); worker.DoWork += (sender, eventArgs) => { eventArgs.Result = TaskMethod("Background worker", 5); }; worker.RunWorkerCompleted += (sender, eventArgs) => { if (eventArgs.Error != null) { tcs.SetException(eventArgs.Error); } else if (eventArgs.Cancelled) { tcs.SetCanceled(); } else { tcs.SetResult((int)eventArgs.Result); } }; worker.RunWorkerAsync(); int result = tcs.Task.Result; WriteLine($"Result is: {result}"); WriteLine($"haha "); Read(); } static int TaskMethod(string name, int seconds) { WriteLine( $"Task {name} is running on a thread id " + $"{CurrentThread.ManagedThreadId}. Is thread pool thread: " + $"{CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(seconds)); return 42 * seconds; } }将EAP(后台事件)模式转换为任务
6. 给基于任务的异步操作实现取消流程
class Program { static void Main(string[] args) { /* 首先仔细看看kmgTask的创建代码。我们将给底层任务传递一次取消标志,然后给任务 构造函数再传递一次。 为什么需要提供取消标志两次呢?答案是如果在任务实际启动前取消它,该任务的TPL基础设施有责任处理该取消操作, 因为这些代码根本不会执行。通过得到的第一个任务的状态可以知道它被取消了。如果尝试 对该任务调用Start方法, 将会得到InvalidOperationException异常。 */ var cts = new CancellationTokenSource(); var longTask = new Task<int>(() => TaskMethod("Task 1", 10, cts.Token), cts.Token); WriteLine(longTask.Status); //然后需要自己写代码来处理取消过程。这意味着我们对取消过程全权负责,并且在取消 任务后,任务的状态仍然是RanToCompletion,因为从TPL的视角来看, //该任务正常完成了 它的工作。辨别这两种情况是非常重要的,并且需要理解每种情况下职责的不同。 cts.Cancel(); WriteLine(longTask.Status); WriteLine("First task has been cancelled before execution"); cts = new CancellationTokenSource(); longTask = new Task<int>(() => TaskMethod("Task 2", 10, cts.Token), cts.Token); longTask.Start(); for (int i = 0; i < 5; i++) { Sleep(TimeSpan.FromSeconds(0.5)); WriteLine(longTask.Status); } cts.Cancel(); for (int i = 0; i < 5; i++) { Sleep(TimeSpan.FromSeconds(0.5)); WriteLine(longTask.Status); } WriteLine($"A task has been completed with result {longTask.Result}."); Read(); } static int TaskMethod(string name, int seconds, CancellationToken token) { WriteLine( $"Task {name} is running on a thread id " + $"{CurrentThread.ManagedThreadId}. Is thread pool thread: " + $"{CurrentThread.IsThreadPoolThread}"); for (int i = 0; i < seconds; i++) { Sleep(TimeSpan.FromSeconds(1)); if (token.IsCancellationRequested) return -1; } return 42 * seconds; } }任务中途取消
7. 异步任务中处理异常
class Program { static void Main(string[] args) { Task<int> task; try { task = Task.Run(() => TaskMethod("Task 1", 2)); //步获取任务结果。Result属性的Get部分会使 当前线程等待直到该任务完成,并将异常传播给当前线程。 //在这种情况下,通过catch代码 块可以很容易地捕获异常,但是该异常是一个被封装的异常, //叫做AggregateExceptiono在 本例中,它里面包含一个异常, //因为只有一个任务抛出了异常。可以访问InnerException属 性来得到底层异常 int result = task.Result; WriteLine($"Result: {result}"); } catch (Exception ex) { WriteLine($"Exception caught: {ex}"); } WriteLine("----------------------------------------------"); WriteLine(); try { //不同之处是使用GetAwaiter和GetResult方法来访问任 务结果。这种情况下,无需封装异常, //因为TPL基础设施会提取该异常。如果只有一个底层 任务,那么一次只能获取一个原始异常,这种设计非常合适 task = Task.Run(() => TaskMethod("Task 2", 2)); int result = task.GetAwaiter().GetResult(); WriteLine($"Result: {result}"); } catch (Exception ex) { WriteLine($"Exception caught: {ex}"); } WriteLine("----------------------------------------------"); WriteLine(); //最后一个例子展示了两个任务抛岀异常的情形。现在使用后续操作来处理异常。只有之前 的任务完成前有异常时, //该后续操作才会被执行。通过给后续操作传递TaskContinuationOptions. OnlyOnFaulted选项可以实现该行为。 //结果打印出了 AggregateException,其内部封装了两个任 务抛岀的异常。 var t1 = new Task<int>(() => TaskMethod("Task 3", 3)); var t2 = new Task<int>(() => TaskMethod("Task 4", 2)); var complexTask = Task.WhenAll(t1, t2); var exceptionHandler = complexTask.ContinueWith(t => WriteLine($"Exception caught: {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted ); t1.Start(); t2.Start(); //为了摆脱这些对异常的封装,我们可以使用根聚合异常的Flatten方法。它将返回一个集 合。 //该集合包含以层级结构中每个子聚合异常中的内部异常 Sleep(TimeSpan.FromSeconds(5)); Read(); } static int TaskMethod(string name, int seconds) { WriteLine( $"Task {name} is running on a thread id " + $"{CurrentThread.ManagedThreadId}. Is thread pool thread: " + $"{CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(seconds)); throw new Exception("Boom!"); return 42 * seconds; } }任务中处理异常
8. 并行运行任务(同时运行多个异步任务)
class Program { static void Main(string[] args) { var firstTask = new Task<int>(() => TaskMethod("First Task", 3)); var secondTask = new Task<int>(() => TaskMethod("Second Task", 2)); var whenAllTask = Task.WhenAll(firstTask, secondTask); //当程序启动时,创建了两个任务。然后借助于Task.WhenAll方法,创建了第三个任务, //该任务将会在所有任务完成后运行。该任务的结果提供了一个结果数组,第一个元素是第一 //个任务的结果,第二个元素是第二个任务的结果,以此类推 whenAllTask.ContinueWith(t => WriteLine($"The first answer is {t.Result[0]}, the second is {t.Result[1]}"), TaskContinuationOptions.OnlyOnRanToCompletion); firstTask.Start(); secondTask.Start(); Sleep(TimeSpan.FromSeconds(4)); var tasks = new List<Task<int>>(); for (int i = 1; i < 4; i++) { int counter = i; var task = new Task<int>(() => TaskMethod($"Task {counter}", counter)); tasks.Add(task); task.Start(); } //创建了另外一系列任务,并使用Task.WhenAny方法等待这些任务中的任何一 个完成。当有一个完成任务后, //从列表中移除该任务并继续等待其他任务完成,直到列表为 空。获取任务的完成进展情况或在运行任务时使用超时, //都可以使用Task.WhenAny方法。 例如,我们等待一组任务运行,并且使用其中一个任务用来记录是否超时。 //如果该任务先完 成,则只需取消掉其他还未完成的任务 while (tasks.Count > 0) { var completedTask = Task.WhenAny(tasks).Result; tasks.Remove(completedTask); WriteLine($"A task has been completed with result {completedTask.Result}."); } Sleep(TimeSpan.FromSeconds(1)); Read(); } static int TaskMethod(string name, int seconds) { WriteLine( $"Task {name} is running on a thread id " + $"{CurrentThread.ManagedThreadId}. Is thread pool thread: " + $"{CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(seconds)); return 42 * seconds; } }多个任务同时执行并获取结果
9. 使用TaskScheduler配置任务的执行(即通过异步代码与ui正确地交互)
我们将学 习什么是任务调度程序,为什么它如此重要,它如何损害应用程序,以及如何在使用它时避 免错误
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } //第一个按钮调用了一个同步任务的执 行。该代码被放置在ButtonSync_Click方法中。当任务运行时, //我们甚至无法移动应用程序 窗口。当用户界面线程忙于运行任务时,整个用户界面被完全冻结,在任务完成前无法响应 任何消息循环。 //对于GUI窗口程序来说这是一个相当不好的实践,我们需要找到一个方式来 解决该问题 void ButtonSync_Click(object sender, RoutedEventArgs e) { ContentTextBlock.Text = string.Empty; try { //string result = TaskMethod(TaskScheduler.FromCurrentSynchronizationContext()).Result; string result = TaskMethod().Result; ContentTextBlock.Text = result; } catch (Exception ex) { ContentTextBlock.Text = ex.InnerException.Message; } } //我们尝试从其他线程访问UI控制器。图形用户界面控制器从没有被设计 为可被多线程使用,并且为了避免可能的错误, //不允许从创建UI的线程之外的线程中访问 U1组件。当我们尝试这样做时, //得到了一个异常,该异常信息5秒后打印到了主窗口中 //为了解决第一个问题,我们尝试异步运行任务。第二个按钮就是这样做的。该代码被 放置在ButtonAsync_Click方法中° //当使用调试模式运行该任务时,将会看到该任务被放置 在线程池中,最后将得到同样的异常。 //然而,当任务运行时用户界面一直保持响应:这是好事,但是我们仍需要除掉异常。 void ButtonAsync_Click(object sender, RoutedEventArgs e) { ContentTextBlock.Text = string.Empty; Mouse.OverrideCursor = Cursors.Wait; Task<string> task = TaskMethod(); task.ContinueWith(t => { ContentTextBlock.Text = t.Exception.InnerException.Message; Mouse.OverrideCursor = null; }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext()); } /*给 TaskScheduler.FromCurrentSynchronizationContext 选项 提供一个后续操作用于输出错误信息。如果不这样做, * 我们将无法看到错误信息,因为可能 会得到在任务中产生的相同异常。该选项驱使TPL基础设施给UI线程的后续操作中放入代 码, * 并借助UI线程消息循环来异步运行该代码。这解决了从其他线程访问UI控制器并仍保 持UI处于响应状态的问题。 * 为了检查是否真的是这样,可以按下最后一个按钮来运行ButtonAsyncOK Click方法中 的代码: * 与其余例子不同之处在于我们将UI线程任务调度程序提供给了该任务。你将看到 任务以异步的方式运行在UI线程中。 * UI依然保持响应。甚至尽管等待光标处于激活状态, 你仍可以按下另一个按钮。 * 然而使用UI线程运行任务有一些技巧。如果回到同步任务代码,取消对使用UI线程任 务调度程序获取结果的代码行的注释, * 我们将永远得不到任何结果。这是一个经典的死锁情 况:我们在UI线程队列中调度了一个操作,UI线程等待该操作完成, * 但当等待时,它又无 法运行该操作,这将永不会结束(甚至永不会开始爲如果在任务中调用Wait方法也会发生 死锁。 * 为了避免死锁,绝对不要通过任务调度程序在UI线程中使用同步操作,请使用C#5.0 中的 ContinueWith 或 async/await 方法 */ void ButtonAsyncOK_Click(object sender, RoutedEventArgs e) { ContentTextBlock.Text = string.Empty; Mouse.OverrideCursor = Cursors.Wait; Task<string> task = TaskMethod(TaskScheduler.FromCurrentSynchronizationContext()); task.ContinueWith(t => Mouse.OverrideCursor = null, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); } Task<string> TaskMethod() { return TaskMethod(TaskScheduler.Default); } Task<string> TaskMethod(TaskScheduler scheduler) { Task delay = Task.Delay(TimeSpan.FromSeconds(5)); return delay.ContinueWith(t => { string str = "Task is running on a thread id " + $"{CurrentThread.ManagedThreadId}. Is thread pool thread: " + $"{CurrentThread.IsThreadPoolThread}"; ContentTextBlock.Text = str; return str; }, scheduler); } }TaskScheduler配置任务(Task与界面交互)
标签:Task,thread,C#,TPL,任务,task,线程,WriteLine From: https://www.cnblogs.com/apple-hu/p/18470367