Loading

[转] .Net并行编程之Task


原文链接:
C# .Net并行(多核)编程——CSDN
C# Task详解——博客园
C# Task学习——博客园

Task的基本用法(创建与执行)

Title :

  • Pro .NET 4 Parallel Programming in C# (Adam Freeman)
  • Task的基本用法

Quotes :

Task 代表一个异步操作。[From MSDN: Task class represents an asynchronous operation.]
System.Threading.Tasks.Task 是一个实现并行编程的基本的类。我们可以理解为是一个任务,它在某一个线程上执行,一个线程可以执行多个任务。

Sample 1: Hello Task

static void Main(string[] args)
{
    Task.Factory.StartNew(() => Console.WriteLine("Hello task"));
    Console.WriteLine("Main method complete. Press enter to finish.");
    Console.ReadLine();
}

这里用了Task的Factory属性返回一个TaskFactory对象,然后调用这个对象的StartNew方法创建并执行一个Task对象。(这里符合静态工厂(Static Factory)的设计模式)

Sample 2: 3种创建一个Task的方法

public static void Main(string[] args)
{
    //使用Action代理创建
    Task task1 = new Task(new Action(printMessage));
    
    //使用一个匿名的代理创建
    Task task2 = new Task(delegate()
    {
        printMessage();
    });

    //用一个lambda表达式创建
    Task task3 = new Task(() => printMessage());

    //执行Task
    task1.Start();
    task2.Start();
    task3.Start();

    Console.WriteLine("Main method complete. Press enter to finish.");
    Console.ReadLine();
}
 
static void printMessage()
{
    Console.WriteLine("Hello World!");
}

Main方法主体在主线程中执行,task1,task2,task3在其它线程中运行,结果是输出3个 “Hello World!”。

Extends : Action代理

public delegate void Action();
public delegate void Action<in T>(T obj);

End

Task的基本用法(组合任务.ContinueWith)

public static void Main()
{
    //创建一个任务
    Task<int> task = new Task<int>(() =>
    {
        int sum = 0;
        Console.WriteLine("使用Task执行异步操作.");
        for (int i = 0; i < 100; i++)
        {
            sum += i;
        }
        return sum;
    });
    //启动任务,并安排到当前任务队列线程中执行任务(System.Threading.Tasks.TaskScheduler)
    task.Start();
    Console.WriteLine("主线程执行其他处理");
    //任务完成时执行处理。
    Task cwt = task.ContinueWith(t =>
    {
        Console.WriteLine("任务完成后的执行结果:{0}", t.Result.ToString());
    });
    task.Wait();
    cwt.Wait();
}

Task的基本用法(设置任务方式与获取任务结果)

Title :

  • Pro .NET 4 Parallel Programming in C# (Adam Freeman) Task的基本用法

G 1 :

设置任务方式:public Task(Action<Object> action, Object state)
通过这个构造方法可以为任务执行体提供输入参数。action 代表任务执行过程,state 为该任务提供参数。

Sample 1 :

public static void Main(string[] args)
{
    string[] items = new string[] { "Ggicci", "Apple", "Google", "Microsoft" };
    Task[] tasks = new Task[items.Length];
    for (int i = 0; i < tasks.Length; i++)
    {
        //items[i]为参数提供给前面的lambda表达式所代表的任务执行体
        tasks[i] = new Task(obj => Console.WriteLine((string)obj), items[i]);
        tasks[i].Start();
    }

    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

该段代码创建了4个 Task,每个 Task 的执行过程就是输出一段字符串,而这段字符串就是由 items 提供的,每个 Task 都会接收一个 items 的参数。

G 2 :

获取任务结果
通过使用 Task 的泛型子类 Task<TResult>>,TResult 是返回数据的类型。其构造方法和 Task 同样有 8 个,实现同样的功能,只不过比 Task 多加了一个可返回的数据结果。
看一个对比:public Task (Action<Object> action, Object state) 和 public Task<TResult>(Func<Object, TResult> function, Object state)

Sample 2:

using System;
using System.Threading.Tasks;

namespace Parallel_TaskProgramming
{
    class Worker
    {
        public DateTime EmployedDate { get; set; }
    }

    class Listing3_13
    {
        public static void Main(string[] args)
        {
            //创建一个能返回 int 型数据的 Task
            //Func<int>为下面 lambda 表达式,其中调用了自定义的 WorkLevel 方法返回 int 值
            //提供一个 Worker 的对象为执行体的参数
            Task<int> task = new Task<int>(worker =>
            {
                return WorkLevel((Worker)worker);
            },
            new Worker() { EmployedDate = new DateTime(2008, 3, 18) });

            task.Start();
            //通过 Task 的 Result 属性获取任务执行体的返回值
            Console.WriteLine("Level: {0}", task.Result);

            Console.WriteLine("Press enter to finish.");
            Console.ReadLine();
        }
        
        //一个计算员工的工作等级的自定义方法
        static int WorkLevel(Worker worker)
        {
            int level = 0;
            int days = (DateTime.Today - worker.EmployedDate).Days;
            while (days > 0)
            {
                days -= (int) Math.Pow(2, level);
                level++;
            }
            return level;
        }
    }
}

Result :

Level: 11
Press enter to finish.

End

Task的基本用法(取消Task的执行)

Title :

  • Pro .NET 4 Parallel Programming in C# (Adam Freeman) Task的基本用法
  • Task 的取消

Steps :

Quote :

You must also throw an instance of System.Threading.OperationCanceledException in your task body; this is how you acknowledge the cancellation, and if you forget, the status of your task will not be set correctly. [From “Pro .Net 4 Parallel Programming in C#”]
你必须在你的Task的执行体里面抛出一个System.Threading.OperationCanceledException异常。具体有两种形式 :

while(true) {
    if(token.IsCancellationRequested) {
        throw new OperationCanceledException(token);
    } else {
        //do your work
    }
}
while(true) {
    token.ThrowIfCancellationRequested();
    //do your work
}

Simple Sample :

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Parallel_TaskProgramming
{
    class Program
    {
        public static void Main(string[] args)
        {
            //步骤1:创建System.Threading.CancellationTokenSource的实例
            CancellationTokenSource tokenSource = new CancellationTokenSource();
            //步骤2:调用CancellationTokenSource.Token属性以获取System.Threading.CancellationToken
            CancellationToken token = tokenSource.Token;

            //步骤3:创建一个新的 Task or Task<T>
            Task task = new Task(() =>
            {
                int i = 0;
                while (true)
                {
                    token.ThrowIfCancellationRequested();
                    Console.WriteLine(i++);
                }
            }, token);
            //步骤4:启动任务,然后可以调用ancellationTokenSource.Cancel()取消任务
            task.Start();
            //主线程睡眠 5 ms
            Thread.Sleep(5);
            tokenSource.Cancel();

            Console.WriteLine("Press enter to finish.");
            Console.ReadLine();
        }
    }
}

Result :结果是不确定的,5 ms 能打印多少次数字取决于运行时 CPU 的忙碌程度

0
1
2
3
4
5
6
7
8
9
10
11
12
Press enter to finish.

Register : Monitoring Cancellation with a Delegate
CancellationToken 的 Register 方法可以为 token 登记一个 Action 或 Action<Object> 委托(代理)的实例。当 Task 被取消执行后,这个委托会被执行。
在上面 Simple Sample 的代码中的 task.Start() 前加上如下代码:

token.Register(() =>
{
     Console.WriteLine("Task was canceled and then I'm invoked.");
 });

Result :

0
1
2
3
4
Task was canceled and then I'm invoked.
Press enter to finish.

WaitHandle : Monitoring Cancellation with a Wait Handle

CancellationToken 有个 WaitHandle 属性,这个属性是一个 WaitHandle 类的对象,WaitHandle 类的 WaitOne 方法的作用是:阻塞当前线程,直到当前的 WaitHandle 接收到一个信号。

Sample :

public static void Main(string[] args)
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;
 
    Task task = new Task(() =>
    {
        Console.WriteLine("Task running...");
        token.WaitHandle.WaitOne();
        Console.WriteLine("Hello");
    }, token);
 
    Console.WriteLine("Press enter to cancel the task.");
    task.Start();
    
    //按空格键取消任务
    Console.ReadLine();
    tokenSource.Cancel();
 
    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

Result :

Press enter to cancel the task.
Task running...
 
Hello
Press enter to finish.

Canceling Several Tasks : 取消多个任务

很简单,只需要在创建任务的时候用同一个 CancallationToken 的实例就可以了。

End

Task的基本用法(复合的CancellationToken)

Title :

  • Pro .NET 4 Parallel Programming in C# (Adam Freeman) Task的基本用法
  • 复合的 Cancellation Token

Quote :

You can create a token that is composed from several CancellationTokens that will be cancelled if any of the underlying tokens is cancelled. [From “Pro .Net 4 Parallel Programming in C#”]
你可以把一些 CancellationToken 组合在一起形成一个复合的 CancellationToken,只要其中任何一个 CancellationToken 被取消,那么这个复合的就会被取消。
至于如何复合,只需调用 System.Threading.CancallationTokenSource.CreateLinkedTokenSource() 方法即可。

Code :

using System;
using System.Threading;
using System.Threading.Tasks;
 
namespace Parallel_TaskProgramming
{
    class Program
    {
        public static void Main(string[] args)
        {
            CancellationTokenSource tokenSource1 = new CancellationTokenSource();
            CancellationTokenSource tokenSource2 = new CancellationTokenSource();
            CancellationTokenSource tokenSource3 = new CancellationTokenSource();
            //用多个CancallationToken复合
            CancellationTokenSource compositeTokenSource =
                CancellationTokenSource.CreateLinkedTokenSource(
                tokenSource1.Token, tokenSource2.Token, tokenSource3.Token);
 
            Task task = new Task(() =>
            {
                int i = 0;
                while (true)
                {
                    if (compositeTokenSource.Token.IsCancellationRequested)
                    {
                        throw new OperationCanceledException(compositeTokenSource.Token);
                    }
                    else
                    {
                        Console.WriteLine(i++);
                    }
                }
            }, compositeTokenSource.Token);
 
            task.Start();
            Thread.Sleep(5);
            //只要其中一个子token被取消,任务就会被取消
            tokenSource1.Cancel();
 
            Console.WriteLine("Press enter to finish.");
            Console.ReadLine();
        }
    }
}

Result :

 0
 1
 2
 3
 4
 5
 6
 7
 8
 Press enter to finish.

End

TaskCreationOptions

Task.Factory.StartNew和创建Task类可以带TaskCreationOptions参数而Task.Run不可以带。

//
// 摘要:
//     指定应使用默认行为。
None = 0,
//
// 摘要:
//     提示 System.Threading.Tasks.TaskScheduler 以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。
PreferFairness = 1,
//
// 摘要:
//     指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。 它会向 System.Threading.Tasks.TaskScheduler
//     提示,过度订阅可能是合理的。 可以通过过度订阅创建比可用硬件线程数更多的线程。 它还将提示任务计划程序:该任务需要附加线程,以使任务不阻塞本地线程池队列中其他线程或工作项的向前推动。
LongRunning = 2,
//
// 摘要:
//     指定将任务附加到任务层次结构中的某个父级。 默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行。 可以使用 System.Threading.Tasks.TaskContinuationOptions.AttachedToParent
//     选项以便将父任务和子任务同步。 请注意,如果使用 System.Threading.Tasks.TaskCreationOptions.DenyChildAttach
//     选项配置父任务,则子任务中的 System.Threading.Tasks.TaskCreationOptions.AttachedToParent 选项不起作用,并且子任务将作为分离的子任务执行。
//     有关详细信息,请参阅附加和分离的子任务。
AttachedToParent = 4,
//
// 摘要:
//     指定任何尝试作为附加的子任务执行(即,使用 System.Threading.Tasks.TaskCreationOptions.AttachedToParent
//     选项创建)的子任务都无法附加到父任务,会改成作为分离的子任务执行。 有关详细信息,请参阅附加和分离的子任务。
DenyChildAttach = 8,
//
// 摘要:
//     防止环境计划程序被视为已创建任务的当前计划程序。 这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为 System.Threading.Tasks.TaskScheduler.Default
//     当前计划程序。
HideScheduler = 16

LongRunning

任务是长时间任务,就需要用LongRunning,可能会创建一个非线程池线程来执行该任务,防止阻塞线程池队列中的其他线程。

private static void fun8()
{
    Task.Factory.StartNew(() =>
    {
        Console.WriteLine($"task1.线程 id {Thread.CurrentThread.ManagedThreadId}. 是否为线程池线程: {Thread.CurrentThread.IsThreadPoolThread}");
    });
 
    Task.Factory.StartNew(() =>
    {
        Console.WriteLine($"task2.线程 id {Thread.CurrentThread.ManagedThreadId}. 是否为线程池线程: {Thread.CurrentThread.IsThreadPoolThread}");
    }, TaskCreationOptions.LongRunning);
}

运行结果:

task2.线程id 4.是否为线程池线程:False
task1.线程id3.是否为线程池线程:True

父子任务(AttachedToParent,DenyChildAttach)

AttachedToParent:将子任务附加到父任务上,表现为:附加到父任务上的所有子任务都结束,父任务才结束。
DenyChildAttach:不允许子任务附加到父任务上。

(1)子任务不附加到父任务

private static void fun5()
{
    Task t = Task.Factory.StartNew(() =>
    {
        Console.WriteLine("parent");
 
        Task.Factory.StartNew(() =>
        {
            Thread.Sleep(1000);
            Console.WriteLine("child");
        });
    });
    t.ContinueWith(x =>
    {
        Console.WriteLine("parent over");
    });
}

运行结果:

parent
parent over
child

(2)子任务附加到父任务上,使用AttachedToParent

private static void fun6()
{
    Task t = Task.Factory.StartNew(() =>
    {
        Console.WriteLine("parent");
 
        Task.Factory.StartNew(() =>
        {
            Thread.Sleep(1000);
            Console.WriteLine("child");
        },TaskCreationOptions.AttachedToParent);
    });
    t.ContinueWith(x =>
    {
        Console.WriteLine("parent over");
    });
}

运行结果:

parent
child
parent over

(3)拒绝子任务附加到父任务上,使用DenyChildAttach

private static void fun7()
{
    Task t = Task.Factory.StartNew(() =>
    {
        Console.WriteLine("parent");
 
        Task.Factory.StartNew(() =>
        {
            Thread.Sleep(1000);
            Console.WriteLine("child");
        }, TaskCreationOptions.AttachedToParent);
    }, TaskCreationOptions.DenyChildAttach);
    t.ContinueWith(x =>
    {
        Console.WriteLine("parent over");
    });
}

运行结果:

barent
barent over
child

WaitHandle.WaitOne 与 Thread.Sleep 区别

Title :

  • Pro .NET 4 Parallel Programming in C# (Adam Freeman)
  • C# 多核编程中 System.Threading.WaitHandle.WaitOne() 与 System.Threading.Thread.Sleep() 的区别
  • C# 线程等待,睡眠

Quote :

The key difference with this technique is that cancelling the token doesn’t immediately cancel the task, because the Thread.Sleep() method will block until the time specified has elapsed and only then check the cancellation status. [From “Pro .Net 4 Parallel Programming in C#”]
采用 Thread.Sleep() 不会立即取消 Task 的继续执行,因为它必须等到 Thread.Sleep(time) 所指定的 time 过去后才检查状态是否为已取消再去决定是否要取消 Task。
The CancellationToken.WaitHandle.WaitOne() method returns true if the token has been cancelled and false if the time elapsed, causing the task has woken up because the time specified has elapsed.
WaitHandle.WaitOne() 会返回 true 如果指定它的 token 已经被取消,会返回 false 如果它指定的时间已经过去。

Code1: [ WaitHandle.WaitOne() ]

public static void Main(string[] args)
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;

    Task task = new Task(() =>
    {
        int i = 0;
        while (true)
        {
            //如果 token 被取消,它会返回 true, 否则等 1000 ms 过去后返回 false
            bool canceled = token.WaitHandle.WaitOne(1000);
            Console.WriteLine(i++);
            if (canceled)
            {
                throw new OperationCanceledException(token);
            }                   
        }
    }, token);
    task.Start();
    //主线程睡眠 5600 ms 后取消 token
    Thread.Sleep(5600);
    tokenSource.Cancel();

    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

Result 1 :

0
1
2
3
4
5
Press enter to finish.

Code2:[ Thread.Sleep() ]

public static void Main(string[] args)
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;

    Task task = new Task(() =>
    {
        int i = 0;
        while (true)
        {
            //与 Code 1 比较,使用 Thread.Sleep()
            Thread.Sleep(1000);
            Console.WriteLine(i++);
            token.ThrowIfCancellationRequested();
        }
    }, token);
    task.Start();
    //主线程同样睡眠 5600 ms 后取消 token
    Thread.Sleep(5600);
    tokenSource.Cancel();

    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

Result 2 :

0
1
2
3
4
Press enter to finish.
5

注意与 **Result 1 的比较。解释:前面输出 0-4 这 5 个数字需要 5000 ms,主线程在 5600 ms 后调用 Cancel 方法把 Task 取消,可是在这之前 Task 已经进入第 6 个循环,但是此时 Thread.Sleep(1000) 的 1000 ms 才过去 600 ms,Thread.Sleep() 并不知道 Task 已经取消,所以等到剩余的 400 ms 过去后才输出 5 然后结束任务,所以主线程比它先结束。而对于 Code 1 **,在 Task 进入第 6 个循环的时候, WaitOne() 就因为在过了 600 ms 检测到 token 已经取消而返回 true, canceled 为 true, 所以 if 条件语句里面抛出异常,Task结束,打印 5 然后继续主线程中的打印 “Press enter to finish.”,主线程结束。

End

单个Task的等待

Title :

  • Pro .NET 4 Parallel Programming in C# (Adam Freeman) - Waiting for a Single Task
  • C#多核编程 线程等待

Quote :

Task.Wait() 的多个重载:

  • Wait()等待,直到 Task 完成,或者被取消,或者抛出异常
  • Wait(CancallationToken)等待,直到 CancellationToken 被取消,或者同上
  • Wait(Int32) 等待,直到 Int32 代表的时间过了,或者同 Wait()
  • Wait(TimeSpan) 等待,直到 TimeSpan 所代表的时间过了,或者同 Wait()
  • Wait(Int32, CancellationToken) 等待,直到…(就是第二个和第三个的组合啦)

Code1:[ Wait() ]

public static void Main(string[] args)
{
   CancellationTokenSource tokenSource = new CancellationTokenSource();
   CancellationToken token = tokenSource.Token;
 
   Task task = new Task(() =>
   {
       token.ThrowIfCancellationRequested();
       Thread.Sleep(1000);
       Console.WriteLine("task running...");
   }, token);
   task.Start();
 
   //Console.WriteLine("Wait for task to complete.");
   //task.Wait();
 
   Console.WriteLine("Press enter to finish.");
   Console.ReadLine();
}

Result1.1: 下面的 “task running…”在 1000 ms 后打印

Press enter to finish.
task running...

如果去掉代码中的注释部分,那么输出结果是:

Result1.2:主线程会等待 task 完成

Wait for task to complete.
task running...
Press enter to finish.

Code2:[ Wait(Int32) ]

public static void Main(string[] args)
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;
 
    Task task = new Task(() =>
    {
        token.ThrowIfCancellationRequested();
        Thread.Sleep(1000);
        Console.WriteLine("task running...");
    }, token);
    task.Start();
 
    //等待 task 500ms, 如果 500ms 内 task 完成, 那么会在 task 完成后马上返回 true
    //如果 500 ms 内 task 没有完成, 那么返回 false
    bool completed = task.Wait(500);
    Console.WriteLine("Task completed? {0}", completed);
 
    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

Result2: 500 ms 过去后打印 completed 的值,为 false,然后 task 在主线程结束后结束

Task completed? False
Press enter to finish.
task running...

Code3:[ Wait(Int32, CancellationToken) ]

public static void Main(string[] args)
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;
 
    Task task = new Task(() =>
    {
        bool canceled = token.WaitHandle.WaitOne(1000);
        if (canceled)
        {
            throw new OperationCanceledException(token);
        }
        Console.WriteLine("task running...");
    }, token);
    
    //timeTask 的任务是在 500 ms 过去的时候调用 tokenSource 的 Cancel 方法以取消 task
    Task timeTask = new Task(() =>
    {
        Thread.Sleep(500);
        tokenSource.Cancel();
    });
    task.Start();
    timeTask.Start();
 
    try
    {
        //task 如果在执行过程中被取消, 那么就会抛出 OperationCanceledException
        bool completed = task.Wait(2000, token);
        Console.WriteLine("Task completed? {0}", completed);
    }
    catch (OperationCanceledException ex)
    {
        Console.WriteLine("Exception: {0}", ex.GetType());
        
    }
 
    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

Result3:

Exception: System.OperationCanceledException
Press enter to finish.

End

多个Task的等待

Ttile :

  • Pro .NET 4 Parallel Programming in C# (Adam Freeman) - Waiting for Several Tasks, Waiting for One of Many Tasks
  • C#多核编程 - 线程等待

Quote :

Waiting for Several Tasks : 等待所有的 Task 完成
You can wait for a number of tasks to complete by using the static Task.WaitAll() method. This method will not return until all of the tasks passed as arguments have completed, been cancelled, or thrown an exception. [ From “Pro .Net 4 Parallel Programming in C#”]
使用 Task.WaitAll() 这个静态方法来实现等待多个 Task。这个方法只有在所有的 task 完成、被取消或者抛出异常的情况下才返回。

Sample :

public static void Main(string[] args)
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;
    
    Task task1 = new Task(() => Console.WriteLine("Task 1 complete."));
    Task task2 = new Task(() =>
    {
        //1s 后抛出一个异常
        Thread.Sleep(1000);
        throw new Exception();                
    });
    Task task3 = new Task(() =>
    {
        //3s 后打印
        Thread.Sleep(3000);
        Console.WriteLine("Task 3 complete.");
    });
    task1.Start();
    task2.Start();
    task3.Start();
    try
    {    
        Task.WaitAll(task1, task2, task3);
    }
    catch (AggregateException ex) //捕捉 task2 抛出的异常
    {
        Console.WriteLine("Exception: {0}", ex.GetType());
    }            
 
    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

Result :

Task 1 complete.
Task 3 complete. //3s后打印出来
Exception: System.AggregateException
Press enter to finish.

虽然 task2 在 1s 后就抛出了异常,但是 Task.WaitAll() 并没有马上终止并抛出异常,而是等到 3s 后 task3 结束。

Quote :

Waiting for One of Many Tasks : 等待多个 Task 中的任意一个完成就好
The Task.WaitAny() method waits for one of a set of tasks to complete. The method waits until any of the specified Tasks completes and returns the array index of the completed Task. [ From “Pro .Net 4 Parallel Programming in C#”]
Task.WaitAny() 等待多个 Task 中的最先一个完成的,然后返回其在多个 Task 中的位置/索引。

Sample :

public static void Main(string[] args)
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;
 
    Task task1 = new Task(() =>
    {
        Thread.Sleep(3000);
        Console.WriteLine("Task 1 complete.");
    });
    Task task2 = new Task(() =>
    {
        Thread.Sleep(1000);
        Console.WriteLine("Task 2 complete.");
    });
    Task task3 = new Task(() =>
    {
        Thread.Sleep(2000);
        Console.WriteLine("Task 3 complete.");
    });
    task1.Start();
    task2.Start();
    task3.Start();
    //打印第一个完成的 Task 在作为数组参数传递给 Task.WaitAny() 时的序号/索引
    Console.WriteLine("The first completed task indexed: {0}", Task.WaitAny(task1, task2, task3));
 
    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

Result :

Task 2 complete.
The first completed task indexed: 1
Press enter to finish.
Task 3 complete.
Task 1 complete.

第一个完成的是 task2,索引为 1。

End

Task的异常处理

Title :

  • Pro .NET 4 Parallel Programming in C# (Adam Freeman) – Handling Exceptions in Tasks
  • 如何处理 Task 运行过程中出现的异常

Quote : [ From “Pro .Net 4 Parallel Programming in C#”- Table 2-7 ]

Problem Solution Code
Basic Exceptions Catch System.AggregateException when call trigger member(Task.Wait(), Task.WaitAll(), Task.WaitAny(), Task.Result) and get an enumerable AggregateException.InnerExceptions Code 1
Iterative Handler Call the AggregateException.Handle() method, providing a delegate that takes a System.Exception and returns true if the exception has been handled and false if it should be escalated. Code 2
Task Properties Call the IsCompleted, IsFaulted, IsCancelled and Exception properties of the Task class. Code 3
Custom Escalation Policy Register an event handler with System.Threading.Tasks.TaskScheduler.UnobservedTaskException. Code 4
问题 解决方案 Code
基本Exceptions 调用触发器成员(Task.Wait(),Task.WaitAll(),Task.WaitAny(),Task.Result)时捕获System.AggregateException并获取可枚举的AggregateException.InnerExceptions Code 1
迭代处理程序 调用AggregateException.Handle()方法,提供一个接受System.Exception的委托,如果已处理了异常,则返回true;如果应升级异常,则返回false。 Code 2
任务属性 调用Task类的IsCompleted,IsFaulted,IsCancelled和Exception属性。 Code 3
自定义升级策略 向System.Threading.Tasks.TaskScheduler.UnobservedTaskException注册事件处理程序。 Code 4

Code 1 : Basic Exceptions

public static void Main(string[] args)
{
    Task task1 = new Task(() => Console.WriteLine("Task 1: Hello World!"));
    //task2 抛出一个异常
    Task task2 = new Task(() =>
    {
        Exception ex = new NullReferenceException("I'm thrown by task2!");
        Console.WriteLine("Task 2: Throw an exception: {0}", ex.GetType());
        throw ex;
    });
 
    task1.Start();
    task2.Start();
 
    try
    {
        Task.WaitAll(task1, task2);
    }
    //捕捉 AggregateException
    catch (AggregateException ex)
    {
        //从 InnerExceptions 属性获取一个可枚举的异常集合, 用 foreach 语句可以遍历其中包含的所有异常
        foreach(Exception inner in ex.InnerExceptions)
        {
            Console.WriteLine("Exception: {0}\nType: {2}\nFrom {2}", 
                                inner.Message, inner.GetType(), inner.Source);
        }
    }
 
    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

Result 1 :

Task 1: Hello World!
Task 2: Throw an exception: System.NullReferenceException
Exception: I'm thrown by task2!
Type: Parallel_TaskProgramming
From Parallel_TaskProgramming
Press enter to finish.

State 1 :

微软的 .NET 框架会把 Task 抛出的任何没有被 Catch 的异常储存起来,直到你调用了一些触发成员(trigger members)如 Task.Wait(), Task.WaitAll(), Task.WaitAny()或者Task.Result,然后这些成员会抛出一个 AggregateException 的实例。AggregateException 类型是为了包装一个或多个异常在一个实例里面,这是很实用的,我们想想,当你在调用 Task.WaitAll() 方法时候,所等待的 task 可能不止一个,如果这些 task 有多个抛出了异常,那么这些异常就应该全都被包装起来等我们处理。要遍历这个实例里面包含了的异常,可以获取它的 InnerExceptions 属性,它提供一个可枚举的容器 (enumerable collection) ,里面容纳了所有的异常。

Code 2 : Iterative Handler

public static void Main(string[] args)
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;
 
    //抛出 NullReferenceException
    Task task1 = new Task(() =>
    {
        Exception ex = new NullReferenceException("I'm thrown by task2!");
        Console.WriteLine("Task 2: Throw an exception: {0}", ex.GetType());
        throw ex;
    });
    //抛出 OperationCanceledException 当 task2 接收到 tokenSource 的取消任务的信号的时候
    Task task2 = new Task(() =>
    {
        token.WaitHandle.WaitOne();
        Exception ex = new OperationCanceledException(token);
    }, token);
 
    task1.Start();
    task2.Start();
    //发出取消任务执行的信号, task2 在这个时候检测到信号并抛出异常
    tokenSource.Cancel();
 
    try
    {
        Task.WaitAll(task1, task2);
    }
    catch (AggregateException ex)
    {
        //利用 Handle 方法, 传递委托或者lambda表达式作为异常处理程序来处理自己感兴趣的异常并传递自己不感兴趣的异常
        ex.Handle((inner) =>
        {
            if (inner is OperationCanceledException)
            {
                Console.WriteLine("Ok, I'll handle the OperationCanceledException here...");
                return true;
            }
            else
            {
                Console.WriteLine("No, I dont know how to handle such an exception, it will be propgated...");
                return false;
            }
        });
    }
 
    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

Result 2 :

State 2 :

上述代码中加粗的那段就是一个迭代的处理程序(Iterative Handler)的应用,Handle() 方法接受函数委托或者lambda表达式,用它们来处理异常。而且它们必须返回 true 或者 false。如果返回 true 说明你已经处理了异常,这个异常将不会被传递、抛出,返回 false 说明你无法处理异常,这个异常将继续传递下去。上述代码中处理了 OperationCancelledException,而对于 NullReferenceException 没有做出处理,那么它将继续被传递抛出并没有被捕捉,所以程序停止工作了。

Code 3 : Task Properties

public static void Main(string[] args)
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;
 
    Task task1 = new Task(() =>
    {
        Exception ex = new NullReferenceException("I'm thrown by task2!");
        Console.WriteLine("Task 2: Throw an exception: {0}", ex.GetType());
        throw ex;
    });
 
    Task task2 = new Task(() =>
    {
        token.WaitHandle.WaitOne();
        Exception ex = new OperationCanceledException(token);
    }, token);
 
    task1.Start();
    task2.Start();
 
    tokenSource.Cancel();
 
    try
    {
        Task.WaitAll(task1, task2);
    }
    catch (AggregateException ex)
    {
        //把异常忽略过去...
    }
    //打印各种属性
    Console.WriteLine("task1: IsCompleted({0}) IsCanceled({1}) IsFaulted({2}) Exception({3})",
        task1.IsCompleted, task1.IsCanceled, task1.IsFaulted, task1.Exception);
    Console.WriteLine("task2: IsCompleted({0}) IsCanceled({1}) IsFaulted({2}) Exception({3})",
        task2.IsCompleted, task2.IsCanceled, task2.IsFaulted, task2.Exception);
 
    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

Result 3 :

Task 2: Throw an exception: System.NullReferenceException
task1: IsCompleted(True) IsCanceled(False) IsFaulted(True) Exception(System.Aggr
egateException: One or more errors occurred. ---> System.NullReferenceException:
 I'm thrown by task2!
   at Parallel_TaskProgramming.Program.<Main>b__0() in D:\文档\Visual Studio 201
0\Projects\C#\Parallel_TaskProgramming\Parallel_TaskProgramming\Program.cs:line
24
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: I'm thrown by task2!
   at Parallel_TaskProgramming.Program.<Main>b__0() in D:\文档\Visual Studio 201
0\Projects\C#\Parallel_TaskProgramming\Parallel_TaskProgramming\Program.cs:line
24
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.Tasks.Task.Execute()<---
)
task2: IsCompleted(True) IsCanceled(True) IsFaulted(False) Exception()
Press enter to finish.

State 3 :

这里的 IsCompleted 属性在 task 正常或者非正常结束后为 true,为 fasle 说明 task 还在运行;IsCanceled 属性在 task 被取消后为 true,否则为 false;IsFaulted 属性在 task 抛出异常后为 true, 否则为 false;Exception 属性在抛出异常后为该异常的引用,否则空。

Code 4 : Custom Escalation Policy

public static void Main(string[] args)
{
    //给 TaskScheduler.UnobservedTaskException 添加异常处理程序
    TaskScheduler.UnobservedTaskException += 
        (object sender, UnobservedTaskExceptionEventArgs eventArgs) => {
            eventArgs.SetObserved();
            ((AggregateException) eventArgs.Exception).Handle(ex =>
            {
                 Console.WriteLine("Exception Type: {0} Message : {1}", ex.GetType(), ex.Message);
                 return true;
            });
        };
 
    Task task1 = new Task(() =>
    {
        throw new NullReferenceException("I'm thrown by task1!");
    });
 
    Task task2 = new Task(() =>
    {
        throw new ArgumentOutOfRangeException("I'm thrown by task2!");
    });
 
    task1.Start();
    task2.Start();
    //注意这里不能调用 Task.WaitAll() 等一些列的触发式成员方法
    while (!task1.IsCompleted || !task2.IsCompleted)
    {
        Thread.Sleep(500);
    }
    Console.WriteLine("Press enter to finish.");
    Console.ReadLine();
}

Result 4 :

Press enter to finish.

State 4 :

注意这里并没有打印第 9 行代码的内容,这是因为在 task 没有被 GC 回收的时候,第 4 – 12 的代码是不会执行的。另外注意的是 27 行不能调用例如 Task.WaitAll( task1, task2 )这样的方法,这样的触发式成员会抛出异常导致异常不能被捕捉而使程序不能继续运行。关于第 4 – 12 行代码的执行问题,可参考:TaskScheduler.UnobservedTaskException never gets called - StackOverflow

End

Author : Ggicci

此篇属于学习笔记,如有错误,欢迎指正!

posted @ 2020-06-05 22:26  二次元攻城狮  阅读(260)  评论(0编辑  收藏  举报