Task的使用(未完)

Action和Func

两者其实都是对delegate的封装,使得声明委托的过程更简单,使用起来也更方便。而delegate、Action、Func都使得处理数据更加灵活,因为我们可以创建一个函数接收开发者或客户的数据参数的同时还能根据不同场景接受不同的处理方式。

Action,顾名思义就是一个“动作”,简单来说就是一个包装了不带返回值函数的委托。可声明不带返回值、接受零个或多个参数的方法的委托。

  • 声明和调用一个Action
 // 1.Lambda表达式
Action action = () => Console.WriteLine("Hello");
Action action2 = new Action(() => Console.WriteLine("Hello"));
action();
action2();

// 2.用方法声明
static void PrintText(string text)
{
    Console.WriteLine(text);
}
Action<string> action = PrintText;
action("Hello");

Func跟Action的唯一区别是Func声明的委托必须有返回值,返回值的类型由<>中的由最右边的类型指定

  • 声明和调用一个Func
 // 1.Lambda表达式
Func<string> func = () => { 
    Console.WriteLine("Hello");
    return "Hello";
};
Func<string> func2 = new Func<string>(() => {
    Console.WriteLine("Hello");
    return "Hello";
});
func();
func2();

// 2.用函数声明
static int PrintText(string text)
{
    Console.WriteLine(text);
    return text.Length;
}
Func<string, int> func = PrintText;
Console.WriteLine(func("Hello"));

Task的出现和基本语法

以往要并行处理一些事务,我们通常会用到线程Thread,每当要并行处理一个事务就要创建一个线程,就要进行这些的工作:
1、创建线程并分配资源;
2、调度和执行线程;
3、结束线程并回收资源。
当一个进程里同时存在的线程数比CPU核心线程数量多的时候,其实超出的部分线程是无法同时执行的,也就是创建再多的线程也无法提高处理器效率。但是为了并行处理事务还是需要创建这些线程。
线程池ThreadPool的出现就是为了解决当这种情况出现时,可以用同一线程去并行处理不同事务而处理器无需浪费多余的性能和内存资源去做第一步和第三步。
而任务Task就是.Net对线程池的一种更人性化的封装,让我们可以更简单地并行处理事务。

  • 声明和调用一个Task
 // 1.Lambda表达式
Task task = new Task(() => {
    Console.WriteLine("Hello");
}); // action
Task<int> task = new Task<int>(() => {
    Console.WriteLine("Hello");
    return "Hello".Length;
}); // func
task.Start();

// 2.声明成一个函数(这种声明方式可以接受参数)
static Task<int> PrintText(string text)
{
    return new Task<int>(() => {
        Console.WriteLine(text);
        return text.Length;
    });
}
PrintText("Hello").Start();

// 3.直接创建并运行Task
Task<int> task = Task.Run(() =>
{
    Console.WriteLine("Hello");
    return "Hello".Length;
});
Console.WriteLine(task.Result); // 获取返回值
  • 通过几个简单例子初步理解Task的工作方式
static Task<int> PrintText(string text)
{
    Console.WriteLine(DateTime.Now.ToString("mm:ss:ffff") + " PrintText Started");
    return new Task<int>(() => {
        Thread.Sleep(500);
        Console.WriteLine(DateTime.Now.ToString("mm:ss:ffff") + " " + text);
        return text.Length;
    });
}
static void Main(string[] args)
{
    Console.WriteLine(DateTime.Now.ToString("mm:ss:ffff") + " create task");
    Task task = PrintText("Hello");
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} task status: {task.Status}");
    task.Start();
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} task status: {task.Status}");
    task.Wait();
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} task status: {task.Status}");
    Console.WriteLine(DateTime.Now.ToString("mm:ss:ffff") + " continue");
}

结果

23:02:7737 create task
23:02:7767 PrintText Started
23:02:7767 task status: Created
23:02:7827 task status: WaitingToRun
23:03:2837 Hello
23:03:2837 task status: RanToCompletion
23:03:2837 continue

通过task.Status可以看出正常运行的任务有三种状态,分别是:
1、Created: 完成创建
2、WaitingToRun: 在任务队列中等待被执行
3、RanToCompletion: 执行完毕
现在,任务已经是和主线程并行执行了。接下来改一下Thread.Sleep(500)的位置再看看结果

static Task<int> PrintText(string text)
{
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} ManagedThreadId: {Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} PrintText Started");
    Thread.Sleep(500);
    return new Task<int>(() => {
        Console.WriteLine($"{DateTime.Now:mm:ss:ffff} ManagedThreadId: {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"{DateTime.Now:mm:ss:ffff} {text}");
        return text.Length;
    });
}
static void Main(string[] args)
{
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} ManagedThreadId: {Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine(DateTime.Now.ToString("mm:ss:ffff") + " create task");
    Task task = PrintText("Hello");
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} task status: {task.Status}");
    task.Start();
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} task status: {task.Status}");
    task.Wait();
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} task status: {task.Status}");
    Console.WriteLine(DateTime.Now.ToString("mm:ss:ffff") + " continue");
}

结果

13:39:9913 ManagedThreadId: 1
13:39:9933 create task
13:39:9943 ManagedThreadId: 1
13:39:9943 PrintText Started
13:40:4957 task status: Created
13:40:5047 task status: WaitingToRun
13:40:5047 ManagedThreadId: 3
13:40:5047 Hello
13:40:5057 task status: RanToCompletion
13:40:5057 continue

可以看出来,PrintText("Hello").Start()执行的是returnTask里面的内容,对PrintText函数里的代码跟普通函数一样,由主线程执行。那如果我们希望能够异步执行我们希望异步执行的代码该怎么做?这时候就要引入两个关键字asyncawait

async和await

async和await关键字的引入大大增加了异步编程的灵活性。

static async Task<int> PrintText(string text)
{
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} PrintText Started");
    Thread.Sleep(300);
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} had slept 300 ms");
    await Task.Run(() => {
        Thread.Sleep(500);
        Console.WriteLine($"{DateTime.Now:mm:ss:ffff} {text}");
    });
    return text.Length;
}
static void Main(string[] args)
{
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} create task");
    PrintText("Hello");
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} continue");
}

结果

54:27:0703 create task
54:27:0743 PrintText Started
54:27:3753 had slept 300 ms
54:27:4003 continue
54:27:9023 Hello

我们用关键字asyncawaitPrintText函数改写成上面的样子,这个时候PrintText变成了一个任务函数。无需用.Start()就可以直接执行。
通过输出结果可以看到,对于没有加await前缀的代码行,实际还是由主线程执行,而await Task.Run()这一句则跟主线程并行执行。当我们想在任务函数里实现同步延迟,而又不希望阻塞主线程时,我们就可以用await Task.Run(() => { code });去执行代码。
如果单纯想在任务函数内进行延迟而不影响主线程,可以用Task类提供的Task.Delay(int millisecondsDelay),实质上返回的是一个由System.Threading.Timer实现的定时器任务。await Task.Delay(500);效果约等于await Task.Run(() => Thread.Sleep(500));
同样地,如果希望在Main中阻塞执行任务函数,需要将其改成

static async Task Main(string[] args)
{
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} create task");
    await PrintText("Hello");   // 在这里的效果相当于`PrintText("Hello").Wait();`,但本质却大有不同,这会在另一篇博文详细说
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} continue");
}

由于带返回值的任务函数要在执行完整个任务函数后才能获取结果,所以还可以通过这种方式进行阻塞

static void Main(string[] args)
{
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} create task");
    Console.WriteLine(PrintText("Hello").Result);
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} continue");
}

Task的简单应用

假设一个场景,你要到奶茶店点一杯宇治抹茶,但是因为你接下来有其他事情要处理,所以你只能等待2分半钟,如果超过这个时间就取消订单。

static readonly Dictionary<string, int> timeTable = new Dictionary<string, int>() { { "宇治抹茶", 3 }, { "珍珠奶茶", 2 } };

static Task OrderMilkTea(string tea, CancellationToken cancellationToken)
{
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} 下单");
    return Task.Run(() => MakeTea(tea, cancellationToken));
}

// 等待制作奶茶
static void MakeTea(string tea, CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
    {
        Console.WriteLine($"{DateTime.Now:mm:ss:ffff} 开始制作前已取消订单");
        cancellationToken.ThrowIfCancellationRequested();
    }
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} 开始制作 {tea} -> ");
    for (int time = 1; time <= timeTable[tea]; time++) // 模拟制作奶茶过程
    {
        Thread.Sleep(1000);
        if (cancellationToken.IsCancellationRequested)
            cancellationToken.ThrowIfCancellationRequested();
        Console.WriteLine($"{DateTime.Now:mm:ss:ffff} 目前用时 " + time + " 分钟");
    }
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} {tea} 已经做好");
}

// 模拟手机计时
static async Task TikTok(int milli)
{
    await Task.Delay(milli);
}

static async Task Main(string[] args)
{
    CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
    Task task1 = OrderMilkTea("宇治抹茶", cancelTokenSource.Token); // 下单任务
    Task task2 = TikTok(2500);  // 计时任务
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} 处理其他事情");
    Thread.Sleep(1000);
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} 处理完毕");
    int completedTask = Task.WaitAny(task1, task2) + 1; // 开始下单并等待
    if (completedTask == 2)
    {
        Console.WriteLine($"{DateTime.Now:mm:ss:ffff} 奶茶做太久了,取消订单 -> x");
        cancelTokenSource.Cancel();
    }

    try
    {
        await task1;
    }
    catch (OperationCanceledException oce)
    {
        Console.WriteLine($"{DateTime.Now:mm:ss:ffff} 订单已取消");
    }
    finally
    {
        cancelTokenSource.Dispose();
    }
}

结果

44:31:8562 下单
44:31:8722 处理其他事情
44:31:8728 开始制作 宇治抹茶 ->
44:32:8728 处理完毕
44:32:8738 目前用时 1 分钟
44:33:8757 目前用时 2 分钟
44:34:3802 奶茶做太久了,取消订单 -> x
44:34:8849 订单已取消

在这个例子里,我们新建了两个Task,task1代表制作奶茶,task2代表手机的计时。下单后,在等待制作奶茶的时候,我们可以处理我们想要处理的事情(在这个例子里处理了1分钟),当2分半的计时结束后,奶茶还没制作完成,于是我们取消了订单。

1、Task.WaitAny(params Task[] tasks): 阻塞等待参数中最先完成的任务,返回最先完成的任务的下标(按参数从左到右的顺序)
2、CancellationTokenSource: 这个类的逻辑是非常简单的信号操作。通过CancellationTokenSource.Cancel()设置取消信号,在任务中检查CancellationTokenSource.Token.IsCancellationRequested的值,若为true,则通过Token.ThrowIfCancellationRequested()抛出异常打断任务。事实上,通过throw new OperationCanceledException()以及OperationCanceledException()的相关重载函数也能达到一样的效果。
3、Task.Run(Action action, CancellationToken cancellationToken): 假设我们在去到奶茶店后临时改变计划而取消,也就是在Task.WaitAny(task1, task2)前就调用cancelTokenSource.Cancel(),那通过Task.Run(Action action, CancellationToken cancellationToken)可以在执行任务前就取消掉任务。本示例演示了通过Task.Run(Action action)并在任务开始时就检查信号达到类似的效果。

Task的取消

CancellationTokenSource

如上例子。

Thread.Abort

上面“点奶茶”的例子通过调用CancellationTokenSource.Cancel()去取消任务时,并不是立刻就取消掉任务,而需要在Thread.Sleep(1000)(也就是制作一段时间)结束等待后才能结束。Task类本身并没有提供立即取消任务的机制,但如果我们想要“立刻”中止任务,有没有办法呢?下面利用Thread.Abort尝试一下。
下面的例子参考自Is it possible to abort a Task like aborting a Thread (Thread.Abort method)?上的回答

class HardAborter
{
    public bool WasAborted { get; private set; }
    public System.Threading.ThreadState? ThreadState => TaskThread?.ThreadState;
    private Thread TaskThread;
    private CancellationTokenSource Canceller { get; set; }
    private Task<object> Worker { get; set; }

    public void Start(Func<object> DoFunc)
    {
        WasAborted = false;

        // start a task with a means to do a hard abort (unsafe!)
        Canceller = new CancellationTokenSource();

        Worker = Task.Factory.StartNew(() =>
        {
            try
            {
                // specify this thread's Abort() as the cancel delegate
                using (Canceller.Token.Register(Thread.CurrentThread.Abort))
                {
                    TaskThread = Thread.CurrentThread;
                    return DoFunc();
                }
            }
            catch (ThreadAbortException)
            {
                WasAborted = true;
                return false;
            }
        }, Canceller.Token);
    }

    public void Abort()
    {
        Canceller.Cancel();
    }
}

static void Main(string[] args)
{
    HardAborter hardAborter = new HardAborter();
    hardAborter.Start(() =>
    {
        Console.WriteLine($"{DateTime.Now:mm:ss:ffff} task started");
        Thread.Sleep(2000);
        Console.WriteLine($"{DateTime.Now:mm:ss:ffff} task finished");
        return true;
    });
    Thread.Sleep(50);   // 稍微等待一段时间让任务开始执行
    hardAborter.Abort();
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} abort task, task state: {hardAborter.ThreadState}");
    while (!hardAborter.WasAborted) { Thread.CurrentThread.Join(0); }
    Console.WriteLine($"{DateTime.Now:mm:ss:ffff} task aborted");
}

结果

21:16:2408 task started
21:16:3018 abort task, task state: Background, WaitSleepJoin, AbortRequested
21:16:3028 task aborted

通过Canceller.Token.Register(Thread.CurrentThread.Abort)在任务创建时,把任务所在线程的Abort()注册为Token.Cancel()的action,也就是当执行Token.Cancel()时会把任务所在线程Abort掉。

然而,这其实存在三个问题。第一,虽然看起来任务在调用hardAborter.Abort()时马上就被中止了,但事实上,任务所在的线程还在运行。因为如果一个线程正在进行IO的调用或者等待,哪怕通过Thread.Abort去中止线程,线程也无法立刻中止,而需要等IO调用返回或者等待结束才能中止。第二,这种“不透明”的操作对于开发者都是不应该的。第三,由于线程池里的线程经常负责多个任务的执行,如果为了中止一个任务而中止了所在的线程,很可能造成其他问题(未验证)。

但是,假设我们开启的一个线程调用了一个第三方的方法,而这个方法由于某种原因超时工作,而这个第三方库又没有提供中止的方法(当然这是不应该的),我们必需主动去中止这个线程。这个时候,利用Thread.Abort去中止线程是必要的。

但是,从ASP.NET Core开始,Thread.Abort就被弃用了,如果在之后的版本中尝试调用会抛出PlatformNotSupportedExceptionSecurityExceptionThreadStateException等异常,那怎么去解决上述这种特殊情况呢?

kernel32 API

这里的例子直接使用 How to abort a Task like aborting a Thread 答案提供的扩展方法。

static async Task Main(string[] args)
{
    new Action(PrintElapsed).RunWithAbort(5000);	// 线程会如期在5000毫秒左右中止
    static void PrintElapsed()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        while (true)
        {
            Console.WriteLine(sw.ElapsedMilliseconds);
        }
    }
}

// 调用API的方法
public static class TestExtensions
{
    [DllImport("kernel32")]
    private static extern bool TerminateThread(IntPtr hThread, int dwExitCode);

    [DllImport("kernel32")]
    private static extern IntPtr CreateThread(IntPtr lpThreadAttributes, IntPtr dwStackSize, Delegate lpStartAddress, IntPtr lpParameter, int dwCreationFlags, out int lpThreadId);

    [DllImport("kernel32")]
    private static extern bool CloseHandle(IntPtr hObject);

    [DllImport("kernel32")]
    private static extern int WaitForSingleObject(IntPtr hHandle, int dwMilliseconds);
    
    // returns true if the call went to completion successfully, false otherwise
    public static bool RunWithAbort(this Action action, int milliseconds) => RunWithAbort(action, new TimeSpan(0, 0, 0, 0, milliseconds));
    public static bool RunWithAbort(this Action action, TimeSpan delay)
    {
        if (action == null)
            throw new ArgumentNullException(nameof(action));

        var source = new CancellationTokenSource(delay);
        var success = false;
        var handle = IntPtr.Zero;
        var fn = new Action(() =>
        {
            using (source.Token.Register(() => TerminateThread(handle, 0)))
            {
                action();
                success = true;
            }
        });

        handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
        WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
        CloseHandle(handle);
        return success;
    }

    // returns what's the function should return if the call went to completion successfully, default(T) otherwise
    public static T RunWithAbort<T>(this Func<T> func, int milliseconds) => RunWithAbort(func, new TimeSpan(0, 0, 0, 0, milliseconds));
    public static T RunWithAbort<T>(this Func<T> func, TimeSpan delay)
    {
        if (func == null)
            throw new ArgumentNullException(nameof(func));

        var source = new CancellationTokenSource(delay);
        var item = default(T);
        var handle = IntPtr.Zero;
        var fn = new Action(() =>
        {
            using (source.Token.Register(() => TerminateThread(handle, 0)))
            {
                item = func();
            }
        });

        handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
        WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
        CloseHandle(handle);
        return item;
    }
}
posted @ 2021-01-09 12:22  Dirt·in·firework  阅读(242)  评论(1)    收藏  举报