C#多线程编程:使用任务并行库

原文链接:https://www.cnblogs.com/wyt007/p/9486752.html

我们在之前的章节中学习了什么是线程,如何使用线程,以及为什么需要线程池。使用线程池可以使我们在减少并行度花销时节省操作系统资源。我们可以认为线程池是一个抽象层,其向程序员隐藏了使用线程的细节,使我们专心处理程序逻辑,而不是各种线程,问题。
然而使用线程池也相当复杂。从线程池的工作线程中获取结果并不容易。我们需要实现,自定义方式来获取结果,而且万一有异常发生,还需将异常正确地传播到初始线程中。除此,以外,创建一组相关的异步操作,以及实现当前操作执行完成后下一操作才会执行的逻辑也不容易。在尝试解决这些问题的过程中,创建了异步编程模型及基于事件的异步模式。在第3章中提到过基于事件的异步模式。这些模式使得获取结果更容易,传播异常也更轻松,但是组,合多个异步操作仍需大量工作,需要编写大量的代码。
为了解决所有的问题, .Net Framework4.0引入了一个新的关于异步操作的API,它叫做.任务并行库( Task Parallel Library,简称TPL), .Net Framework 4.5版对该API进行了轻微的改进,使用更简单。在本书的项目中将使用最新版的TPL,即.Net Framework 4.5版中的 API, TPL可被认为是线程池之上的又一个抽象层,其对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度的APL, TPL的核心概念是任务。一个任务代表了一个异步操作,该操作可以通过多种方式运行,可以使用或不使用独立线程运行。在本章中将探究任务的所有使用细节。
默认情况下,程序员无须知道任务实际上是如何执行的。TPL通过向用户隐藏任务的实现细节从而创建一个抽象层。遗憾的是,有些情况下这会导致诡秘的错误,比如试图获取任务的结果时程序被挂起。本章有助于理解TPL底层的原理,以及如何避免不恰当的使用方式。
一个任务可以通过多种方式和其他任务组合起来。例如,可以同时启动多个任务,等待所有任务完成,然后运行一个任务对之前所有任务的结果进行一些计算。TPL与之前的模式相比,其中一个关键优势是其具有用于组合任务的便利的API,
处理任务中的异常结果有多种方式。由于一个任务可能会由多个其他任务组成,这些任,务也可能依次拥有各自的子任务,所以有一个AggregateException的概念。这种异常可以捕获底层任务内部的所有异常,并允许单独处理这些异常。
而且,最后但并不是最不重要的, C# 5.0已经内置了对TPL的支持,允许我们使用新的 await和async关键字以平滑的、舒服的方式操作任务。
在本章中我们将学习使用TPL来执行异步操作。我们将学习什么是任务,如何用不同的,方式创建任务,以及如何将任务组合在一起。我们会讨论如何将遗留的APM和EAP模式转换为使用任务,还有如何正确地处理异常,如何取消任务,以及如何使多个任务同时执行。另外,还将讲述如何在Windows GUI应用程序中正确地使用任务。

创建任务

class Program
{
    static void Main(string[] args)
    {
        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.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning);
        Thread.Sleep(TimeSpan.FromSeconds(1));

        Console.ReadKey();
    }

    static void TaskMethod(string name)
    {
        Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
            name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
    }
}

当程序运行时,我们使用Task的构造函数创建了两个任务。我们传入一个lambda表达式作为Action委托。这可以使我们给TaskMethod提供一个string参数。然后使用Start方法运行这些任务。
请注意只有调用了这些任务的Start方法,才会执行任务。很容易忘记真正启动任务。
然后使用Task.Run和Task.Factory.StartNew方法来运行了另外两个任务。与使用Task构造函数的不同之处在于这两个被创建的任务会立即开始工作,所以无需显式地调用这些任务的Start方法。从Task 1到Task 4的所有任务都被放置在线程池的工作线程中并以未指定的顺序运行。如果多次运行该程序,就会发现任务的执行顺序是不确定的。
Task.Run方法只是Task.Factory.StartNew的一个快捷方式,但是后者有附加的选项。通!常如果无特殊需求,则可使用前一个方法,如Task 5所示。我们标记该任务为长时间运行,结果该任务将不会使用线程池,而在单独的线程中运行。然而,根据运行该任务的当前的任务调度程序( task scheduler)运行方式有可能不同。

使用任务执行基本的操作

本节将描述如何从任务中获取结果值。我们将通过几个场景来了解在线程池中和主线程中运行任务的不同之处。

class Program
{
    static void Main(string[] args)
    {
        TaskMethod("Main Thread Task");
        Task<int> task = CreateTask("Task 1");
        task.Start();
        int result = task.Result;
        Console.WriteLine("Result is: {0}", result);

        task = CreateTask("Task 2");
        task.RunSynchronously();
        result = task.Result;
        Console.WriteLine("Result is: {0}", result);

        task = CreateTask("Task 3");
        Console.WriteLine(task.Status);
        task.Start();

        while (!task.IsCompleted)
        {
            Console.WriteLine(task.Status);
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
        } 
        
        Console.WriteLine(task.Status);
        result = task.Result;
        Console.WriteLine("Result is: {0}", result);

        Console.ReadKey();
    }

    static Task<int> CreateTask(string name)
    {
        return new Task<int>(() => TaskMethod(name));
    }

    static int TaskMethod(string name)
    {
        Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
            name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
        Thread.Sleep(TimeSpan.FromSeconds(2));
        return 42;
    }
}

首先直接运行TaskMethod方法,这里并没有把它封装到一个任务中。结果根据它提供给我们的主线程的信息可以得知该方法是被同步执行的。很显然它不是线程池中的线程。
然后我们运行了Task 1,使用Start方法启动该任务并等待结果。该任务会被放置在线程池中,并且主线程会等待,直到任务返回前一直处于阻塞状态。
Task 2和Task 1类似,除了Task 2是通过RunSynchronously()方法运行的。该任务会运行在主线程中,该任务的输出与第一个例子中直接同步调用TaskMethod的输出完全一样。这是个非常好的优化,可以避免使用线程池来执行非常短暂的操作。
我们用以运行Task 1相同的方式来运行Task 3,但这次没有阻塞主线程,只是在该任务完成前循环打印出任务状态。结果展示了多种任务状态,分别是Creatd, Running和 RanToCompletion。

组合任务

本节将展示如何设置相互依赖的任务。我们将学习如何创建一个任务,使其在父任务完成后才会被运行。另外,将探寻为非常短暂的任务节省线程开销的可能性。

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));

        firstTask.ContinueWith(
            t => Console.WriteLine("The first answer is {0}. Thread id {1}, is thread pool thread: {2}",
                t.Result, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread),
            TaskContinuationOptions.OnlyOnRanToCompletion);

        firstTask.Start();
        secondTask.Start();

        Thread.Sleep(TimeSpan.FromSeconds(4));

        Task continuation = secondTask.ContinueWith(
            t => Console.WriteLine("The second answer is {0}. Thread id {1}, is thread pool thread: {2}",
                t.Result, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread),
            TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously);

        continuation.GetAwaiter().OnCompleted(
            () => Console.WriteLine("Continuation Task Completed! Thread id {0}, is thread pool thread: {1}",
                Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread));

        Thread.Sleep(TimeSpan.FromSeconds(2));
        Console.WriteLine();

        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)
        {
            Console.WriteLine(firstTask.Status);
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
        }
        Console.WriteLine(firstTask.Status);

        Thread.Sleep(TimeSpan.FromSeconds(10));
    }

    static int TaskMethod(string name, int seconds)
    {
        Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
            name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
        Thread.Sleep(TimeSpan.FromSeconds(seconds));
        return 42 * seconds;
    }
}

当主程序启动时,我们创建了两个任务,并为第一个任务设置了一个后续操作( continuation,一个代码块,会在当前任务完成后运行),然后启动这两个任务并等待4秒,这个时间足够两个任务完成。然后给第二个任务运行另一个后续操作,并通过指定TaskContinuationOptions."ExecuteSynchronously选项来尝试同步执行该后续操作。如果后续操作耗时非常短暂,使用以上方式是非常有用的,因为放置在主线程中运行比放置在线程池中运行要快。可以实现这一点是因为第二个任务恰好在那刻完成。如果注释掉4秒的Thread.Sleep方法,将会看到该代码被放置到线程池中,这是因为还未从之前的任务中得到结果。
最后我们为之前的后续操作也定义了一个后续操作,但这里使用了一个稍微不同的方式,即使用了新的GetAwaiter和OnCompleted方法。这些方法是C# 5.0语言中异步机制中的方法。
本节示例的最后部分与父子线程有关。我们创建了一个新任务,当运行该任务时,通过提供一个TaskCreationOptions.AttachedToParent选项来运行一个所谓的子任务。
子任务必须在父任务运行时创建,并正确的附加给父任务!
这意味着只有所有子任务结束工作,父任务才会完成。通过提供一个TaskContinuation Options选项也可以给在子任务上运行后续操作。该后续操作也会影响父任务,并且直到最后一个子任务结束它才会运行完成。

将APM模式转换成任务

本节将说明如何将过时的APM API转换为任务。多个示例覆盖了转换过程中可能发生的不同情况。

class Program
{
    private static void Main(string[] args)
    {
        int threadId;
        AsynchronousTask d = Test;
        IncompatibleAsynchronousTask e = Test;

        Console.WriteLine("Option 1");
        Task<string> task = Task<string>.Factory.FromAsync(
            d.BeginInvoke("AsyncTaskThread", Callback, "a delegate asynchronous call"), d.EndInvoke);

        task.ContinueWith(t => Console.WriteLine("Callback is finished, now running a continuation! Result: {0}",
            t.Result));

        while (!task.IsCompleted)
        {
            Console.WriteLine(task.Status);
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
        }
        Console.WriteLine(task.Status);
        Thread.Sleep(TimeSpan.FromSeconds(1));

        Console.WriteLine("----------------------------------------------");
        Console.WriteLine();
        Console.WriteLine("Option 2");

        task = Task<string>.Factory.FromAsync(
            d.BeginInvoke, d.EndInvoke, "AsyncTaskThread", "a delegate asynchronous call");
        task.ContinueWith(t => Console.WriteLine("Task is completed, now running a continuation! Result: {0}",
            t.Result));
        while (!task.IsCompleted)
        {
            Console.WriteLine(task.Status);
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
        }
        Console.WriteLine(task.Status);
        Thread.Sleep(TimeSpan.FromSeconds(1));

        Console.WriteLine("----------------------------------------------");
        Console.WriteLine();
        Console.WriteLine("Option 3");

        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 => 
            Console.WriteLine("Task is completed, now running a continuation! Result: {0}, ThreadId: {1}",
                t.Result, threadId));

        while (!task.IsCompleted)
        {
            Console.WriteLine(task.Status);
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
        }
        Console.WriteLine(task.Status);

        Thread.Sleep(TimeSpan.FromSeconds(1));

        Console.ReadKey();
    }

    private delegate string AsynchronousTask(string threadName);
    private delegate string IncompatibleAsynchronousTask(out int threadId);

    private static void Callback(IAsyncResult ar)
    {
        Console.WriteLine("Starting a callback...");
        Console.WriteLine("State passed to a callbak: {0}", ar.AsyncState);
        Console.WriteLine("Is thread pool thread: {0}", Thread.CurrentThread.IsThreadPoolThread);
        Console.WriteLine("Thread pool worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
    }

    private static string Test(string threadName)
    {
        Console.WriteLine("Starting...");
        Console.WriteLine("Is thread pool thread: {0}", Thread.CurrentThread.IsThreadPoolThread);
        Thread.Sleep(TimeSpan.FromSeconds(2));
        Thread.CurrentThread.Name = threadName;
        return string.Format("Thread name: {0}", Thread.CurrentThread.Name);
    }

    private static string Test(out int threadId)
    {
        Console.WriteLine("Starting...");
        Console.WriteLine("Is thread pool thread: {0}", Thread.CurrentThread.IsThreadPoolThread);
        Thread.Sleep(TimeSpan.FromSeconds(2));
        threadId = Thread.CurrentThread.ManagedThreadId;
        return string.Format("Thread pool worker thread id was: {0}", threadId);
    }
}

这里我们定义了两种委托。其中一个使用了out参数,因此在将APM模式转换为任务时,与标准的TPLAPI是不兼容的。这样的转换有三个示例。
将APM转换为TPL的关键点是Task<T>.Factory.FromAsync方法, T是异步操作结果的类型。该方法有数个重载。在第一个例子中传人了IAsyncResult和Func<lAsyncResult, string>,这是一个将IAsyncResult的实现作为参数并返回一个字符串的方法。由于第一个委托类型提供的EndMethod与该签名是兼容的,所以将该委托的异步调用转换为任务没有任何问题。
第二个例子做的事与第一个非常相似,但是使用了不同的FromAsync方法重载,该重载 ,并不允许指定一个将会在异步委托调用完成后被调用的回调函数。但我们可以使用后续操作,替代它。但如果回调函数很重要,可以使用第一个例子所示的方法。
最后一个例子展示了一个小技巧。这次IncompatibleAsynchronousTask委托的EndMethod使用了out参数,与FromAsync方法重载并不兼容。然而,可以很容易地将 EndMethod调用封装到一个lambda表达式中,从而适合任务工厂方法。
可以在等待异步操作结果过程中打印出任务状态,从而了解底层任务的运行情况。可以看到第一个任务的状态为WaitingForActivation,这意味着TPL基础设施实际上还未启动该任务。

将EAP模式转换成任务

本节将描述如何将基于事件的异步操作转换为任务。在本节中,你将发现有一个可靠的模式可适用于.Net Framework类库中的所有基于事件的异步API.

class Program
{
    static void Main(string[] args)
    {
        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;

        Console.WriteLine("Result is: {0}", result);
    }

    static int TaskMethod(string name, int seconds)
    {
        Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
            name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
        Thread.Sleep(TimeSpan.FromSeconds(seconds));
        return 42 * seconds;
    }
}

这是一个将EAP模式转换为任务的既简单又优美的示例。关键点在于使用TaskCompletionSource<T>类型, T是异步操作结果类型。
不要忘记将tcs.SetResult调用封装在try-catch代码块中,从而保证错误信息始终会设置给任务完成源对象。也可以使用TrySetResult方法来替代SetResult方法,以保证结果能被成功设置。

实现取消选项

本节是关于如何给基于任务的异步操作实现取消流程。我们将学习如何正确的使用取消标志,以及在任务真正运行前如何得知其是否被取消。

class Program
{
    private static void Main(string[] args)
    {
        var cts = new CancellationTokenSource();
        var longTask = new Task<int>(() => TaskMethod("Task 1", 10, cts.Token), cts.Token);
        Console.WriteLine(longTask.Status);
        cts.Cancel();
        Console.WriteLine(longTask.Status);
        Console.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++ )
        {
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
            Console.WriteLine(longTask.Status);
        }
        cts.Cancel();
        for (int i = 0; i < 5; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
            Console.WriteLine(longTask.Status);
        }

        Console.WriteLine("A task has been completed with result {0}.", longTask.Result);

        Console.ReadKey();
    }

    private static int TaskMethod(string name, int seconds, CancellationToken token)
    {
        Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
            name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
        for (int i = 0; i < seconds; i ++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
            if (token.IsCancellationRequested) return -1;
        }
        return 42*seconds;
    }
}

第3章中我们已经讨论了取消标志概念,你已经相当熟悉了。而本节又是一个关于为TPL任务实现取消选项的简单例子。
首先仔细看看longTask的创建代码。我们将给底层任务传递一次取消标志,然后给任务构造函数再传递一次。为什么需要提供取消标志两次呢?
答案是如果在任务实际启动前取消它,该任务的TPL基础设施有责任处理该取消操作,因为这些代码根本不会执行。通过得到的第一个任务的状态可以知道它被取消了。如果尝试对该任务调用Start方法,将会得到InvalidOperationException异常。
然后需要自己写代码来处理取消过程。这意味着我们对取消过程全权负责,并且在取消,任务后,任务的状态仍然是RanToCompletion,因为从TPL的视角来看,该任务正常完成了它的工作。辨别这两种情况是非常重要的,并且需要理解每种情况下职责的不同。

处理任务中的异常

本节将描述异步任务中处理异常这一重要的主题。我们将讨论任务中抛出异常的不同情况及如何获取这些异常信息。

class Program
{
    static void Main(string[] args)
    {
        Task<int> task;
        try
        {
            task = Task.Run(() => TaskMethod("Task 1", 2));
            int result = task.Result;
            Console.WriteLine("Result: {0}", result);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception caught: {0}", ex);
        }
        Console.WriteLine("----------------------------------------------");
        Console.WriteLine();

        try
        {
            task = Task.Run(() => TaskMethod("Task 2", 2));
            int result = task.GetAwaiter().GetResult();
            Console.WriteLine("Result: {0}", result);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception caught: {0}", ex);
        }
        Console.WriteLine("----------------------------------------------");
        Console.WriteLine();

        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 => 
                Console.WriteLine("Exception caught: {0}", t.Exception), 
                TaskContinuationOptions.OnlyOnFaulted
            );
        t1.Start();
        t2.Start();

        Thread.Sleep(TimeSpan.FromSeconds(5));

        Console.ReadKey();
    }

    static int TaskMethod(string name, int seconds)
    {
        Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
            name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
        Thread.Sleep(TimeSpan.FromSeconds(seconds));
        throw new Exception("Boom!");
        return 42 * seconds;
    }
}

当程序启动时,创建了一个任务并尝试同步获取任务结果。Result属性的Get部分会使,当前线程等待直到该任务完成,并将异常传播给当前线程。在这种情况下,通过catch代码块可以很容易地捕获异常,但是该异常是一个被封装的异常,叫做AggregateException。在本例中,它里面包含一个异常,因为只有一个任务抛出了异常。可以访问InnerException属性来得到底层异常。
第二个例子与第一个非常相似,不同之处是使用GetAwaiter和GetResult方法来访问任务结果。这种情况下,无需封装异常,因为TPL基础设施会提取该异常。如果只有一个底层,任务,那么一次只能获取一个原始异常,这种设计非常合适。
最后一个例子展示了两个任务抛出异常的情形。现在使用后续操作来处理异常。只有之前,的任务完成前有异常时,该后续操作才会被执行。通过给后续操作传递TaskContinuationOptions.OnlyOnFaulted选项可以实现该行为。结果打印出了AggregateException,其内部封装了两个任务抛出的异常。

并行运行任务

本节展示了如何同时运行多个异步任务。我们将学习当所有任务都完成或任意一个任务,完成了工作时,如何高效地得到通知。

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);

        whenAllTask.ContinueWith(t =>
            Console.WriteLine("The first answer is {0}, the second is {1}", t.Result[0], t.Result[1]),
            TaskContinuationOptions.OnlyOnRanToCompletion
            );

        firstTask.Start();
        secondTask.Start();

        Thread.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(string.Format("Task {0}", counter), counter));
            tasks.Add(task);
            task.Start();
        }

        while (tasks.Count > 0)
        {
            var completedTask = Task.WhenAny(tasks).Result;
            tasks.Remove(completedTask);
            Console.WriteLine("A task has been completed with result {0}.", completedTask.Result);
        }

        Thread.Sleep(TimeSpan.FromSeconds(1));

        Console.ReadKey();
    }

    static int TaskMethod(string name, int seconds)
    {
        Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
            name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
        Thread.Sleep(TimeSpan.FromSeconds(seconds));
        return 42 * seconds;
    }
}

当程序启动时,创建了两个任务。然后借助于Task.WhenAll方法,创建了第三个任务,该任务将会在所有任务完成后运行。该任务的结果提供了一个结果数组,第一个元素是第.个任务的结果,第二个元素是第二个任务的结果,以此类推。
然后我们创建了另外一系列任务,并使用Task.WhenAny方法等待这些任务中的任何一个完成。当有一个完成任务后,从列表中移除该任务并继续等待其他任务完成,直到列表为空。获取任务的完成进展情况或在运行任务时使用超时,都可以使用Task.WhenAny方法。例如,我们等待一组任务运行,并且使用其中一个任务用来记录是否超时。如果该任务先完,成,则只需取消掉其他还未完成的任务。

使用TaskScheduler配置任务的执行

1、新建一个C# WPF应用程序项目

2、在MainWindow.xaml文件中,将下面的标记代码加入到一个网格元素中(即<Grid和<Grid>标签间):

<TextBlock Name="ContentTextBlock" HorizontalAlignment="Left" Margin="44,134,0,0" VerticalAlignment="Top" Width="425" Height="40"/>
<Button Content="Sync" HorizontalAlignment="Left" Margin="45,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonSync_Click"/>
<Button Content="Async" HorizontalAlignment="Left" Margin="165,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonAsync_Click"/>
<Button Content="Async OK" HorizontalAlignment="Left" Margin="285,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonAsyncOK_Click"/>

3、在MainWindow.xaml.cs文件中使用以下using指令;

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;

4、在MainWindow构造函数下面加入以下代码片段:

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;
    }
}

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());
}

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 = string.Format("Task is running on a thread id {0}. Is thread pool thread: {1}",
                Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
        ContentTextBlock.Text = str;
        return str;
    }, scheduler);
}

本例中引人了很多新鲜的东西。首先,创建了一个WPF应用程序,而不是一个命令行程序。这是很有必要的,因为我们需要一个拥有消息循环的用户界面线程来演示异步运行任,务的不同情形。
TaskScheduler是一个非常重要的抽象。该组件实际上负责如何执行任务。默认的任务调度程序将任务放置到线程池的工作线程中。这是非常常见的场景,所以TPL将其作为默认选项并不用奇怪。我们已经知道了如何同步运行任务,以及如何将任务附加到父任务上从而一起运行。现在让我们看看使用任务的其他方式。
当程序启动时,创建了一个包含三个按钮的窗口。第一个按钮调用了一个同步任务的执行。该代码被放置在ButtonSync Click方法中。当任务运行时,我们甚至无法移动应用程序,窗口。当用户界面线程忙于运行任务时,整个用户界面被完全冻结,在任务完成前无法响应任何消息循环。对于GUI窗口程序来说这是一个相当不好的实践,我们需要找到一个方式来,解决该问题 ,
第二个问题是我们尝试从其他线程访问UI控制器。图形用户界面控制器从没有被设计,为可被多线程使用,并且为了避免可能的错误,不允许从创建UI的线程之外的线程中访问U1组件。当我们尝试这样做时,得到了一个异常,该异常信息5秒后打印到了主窗口中。
为了解决第一个问题,我们尝试异步运行任务。第二个按钮就是这样做的。该代码被,.放置在ButtonAsync Click方法中。当使用调试模式运行该任务时,将会看到该任务被放置,在线程池中,最后将得到同样的异常。然而,当任务运行时用户界面一直保持响应。这是好事,但是我们仍需要除掉异常。
其实我们已经解决了该问题。给TaskScheduler.FromCurrentSynchronizationContext选项提供一个后续操作用于输出错误信息。如果不这样做,我们将无法看到错误信息,因为可能会得到在任务中产生的相同异常。该选项驱使TPL基础设施给U1线程的后续操作中放入代码,并借助UI线程消息循环来异步运行该代码。这解决了从其他线程访问UI控制器并仍保持U1处于响应状态的问题。
为了检查是否真的是这样,可以按下最后一个按钮来运行ButtonAsyncOK-Click方法中的代码。与其余例子不同之处在于我们将UI线程任务调度程序提供给了该任务。你将看到 ,任务以异步的方式运行在UI线程中。U1依然保持响应。甚至尽管等待光标处于激活状态,你仍可以按下另一个按钮,
然而使用U1线程运行任务有一些技巧。如果回到同步任务代码,取消对使用UI线程任务调度程序获取结果的代码行的注释,我们将永远得不到任何结果。这是一个经典的死锁情,况:我们在UI线程队列中调度了一个操作, U1线程等待该操作完成,但当等待时,它又无法运行该操作,这将永不会结束(甚至永不会开始),如果在任务中调用Wait方法也会发生死锁。为了避免死锁,绝对不要通过任务调度程序在U1线程中使用同步操作,请使用C# 5.0中的ContinueWith或async/await方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值