首页 > 系统相关 >.NET教程 - 进程 & 异步 & 并行 第二部分(Process & Asynchronous & Parallelization Part2)

.NET教程 - 进程 & 异步 & 并行 第二部分(Process & Asynchronous & Parallelization Part2)

时间:2022-10-03 09:23:12浏览次数:51  
标签:异步 Task Console Process 任务 Parallelization Part2 WriteLine public

更新记录
转载请注明出处:
2022年10月3日 发布。
2022年10月1日 从笔记迁移到博客。

任务-Task和Task<T>

任务概念

任务使用 Task类 进行表示

返回指定类型的任务使用 Task<T>类 表示

Task<T>类 继承自 Task类

命名空间

using System.Threading.Tasks;

Task与Thread的区别

Task表示需要执行的作业,Thread表示执行作业的执行者。Task本质上底层还是用的线程池,线程池中本质也是使用的是线程。所以Task本质是语法糖,编译器帮助我们合理的使用线程,减少代码出现问题的概率,包括async和await也本质是语法糖。

从Thread切换到Task

因为每次创建和销毁Thread,并再次重新创建非常消耗资源,并且直接操作线程容易出现错误,而使用Task实现并行操作不是去操作具体的Thread,而是使用Task,并告诉任务调度器需要执行一个异步任务,任务调度器内部有多种方式来处理Task。默认情况下Task会去线程池请求一个工作线程,线程池会自动判断如何执行任务更加高效,可能会在当前任务结束后再创建新任务,也可能将新任务的工作线程调度到特定的处理器上。线程池也会自行决定是创建新线程还是复用之前的线程。通过将异步的Task抽象到Task类型实例中,提供了相应的API以便于交互,使用任务并行可以将小任务合并成大任务,形成一个异步工作流程。

Task与Delegate对比

使用的目的不同,Task是用来实现异步工作的,Delegate本质是函数指针。虽然看起来都是用于调用方法(函数),Task类封装了异步操作,Delegate类了封装了操作,Delegate是同步执行的,Task是异步执行的。

在运行上,Delegate的操作代码在调用的地方立即进行执行,然后再返回调用点;Task的操作代码是在另一个线程上同时异步执行。

简单来说,Task将Delegate从同步执行模式转为异步执行模式。

Task使用基本使用

说明

可以使用Task.Run()方法运行任务,Task.Run()是Task.Factory.StartNew()的简写

实例:创建Task(三种方式)

可以使用三种方式创建Task

//方式一:直接使用化
Task task1 = new Task(() =>
{
    //模拟异步任务
    Console.WriteLine("Hello Task!");
});
//然后开启线程
task1.Start();
//方式二:使用Run方法,注意:创建后会直接开启执行
Task task2 = Task.Run(() => {
    //模拟异步任务
    Console.WriteLine("Hello Task!");
});
//方式三:使用Task工厂类,注意:创建后会直接开启执行
TaskFactory taskFactory = new TaskFactory();
Task task3 = taskFactory.StartNew(() => {
    //模拟异步任务
    Console.WriteLine("Hello Task!");
});

也可以不用创建任务工厂,直接使用快捷属性。

Task.Factory.StartNew(() => {
    //模拟异步任务
    Console.WriteLine("Hello Task!");
});

实例:创建Task(带返回值)

//方式一:直接使用化
Task task1 = new Task(() =>
{
    //模拟异步任务
    Console.WriteLine("Hello Task!");
});
//然后开启线程
task1.Start();
//方式二:使用Run方法
Task<string> task2 = Task.Run(() => {
    Console.WriteLine("Hello Task!");
    return "Panda666.com";
});
Console.WriteLine(task2.Result);
//方式三:使用工厂类
TaskFactory taskFactory = new TaskFactory();
Task<string> task3 = taskFactory.StartNew(() => {
    Console.WriteLine("Hello Task!");
    return "Panda666.com";
});
Console.WriteLine(task3.Result);

实例:创建Task(带参数)

//方式一:直接使用化
Task task1 = new Task((object arg) =>{
	//模拟异步任务
    Console.WriteLine(arg as string);
},"Panda666.com");
//然后开启线程
task1.Start();
//方式二:使用工厂类
TaskFactory taskFactory = new TaskFactory();
Task task3 = taskFactory.StartNew((object arg) => {
    Console.WriteLine("Hello Task3!");
    Console.WriteLine(arg as string);
}, "Panda666.com");

实例:等待任务完成(所有任务)

//等待所有的Task任务完成
Task.WaitAll(new Task[] { t1, t2 });
等待任务完成(任何一个任务)

实例:任务延迟

注意:任务延迟不要使用Thread.Sleep(),而是要使用Task.Delay()方法。

async static Task TaskDelayTest()
{
    Console.WriteLine("1");
    await Task.Delay(3000);
    Console.WriteLine("2");
}

实例:带返回值的任务

Task<string> t1 = Task.Run(() => {
    //具体的代码
    for (int i = 0; i < 1000; i++)
    {
        Console.WriteLine("Count-{0}",i);
    }
    //返回值
    return "panda666";
});

//获得返回值
Console.WriteLine(t1.Result);

实例:创建带参数带返回值的Task

//创建工厂类
TaskFactory taskFactory = new TaskFactory();
//开启线程
Task<string> task3 = taskFactory.StartNew((object arg) => {
    Console.WriteLine("Hello Task3!");
    string data = arg as string;
    return (data ?? "panda666") + ".com";
    
}, "Panda666");
//获得返回值
Console.WriteLine(task3.Result);

实例:等待任务完成后执行后续任务

使用Task实例对象的ContinueWith()方法即可。不阻塞主线程,但又能在完成指定的单个或多个任务后执行其他任务。

Task task = Task.Run(() => {
                Console.WriteLine("这是任务1");
            })
    	    .ContinueWith(t =>
            {
                Console.WriteLine("这是任务2");
            });

如果是等待多个任务中的任何一个任务完成后就执行指定任务,可以使用Task.Factory.ContinueWhenAny()方法。

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

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            List<Task> tasks = new List<Task>();
            tasks.Add(Task.Run(() => { Console.WriteLine("任务1"); }));
            tasks.Add(Task.Run(() => { Console.WriteLine("任务2"); }));
            tasks.Add(Task.Run(() => { Console.WriteLine("任务3"); }));
            //只要前面三个任务中的任何一个完成,就执行后续任务
            Task.Factory.ContinueWhenAny(tasks.ToArray(), t => {
                Console.WriteLine("这是后续任务");
            });

            //等待前面的任务完成
            Task.WaitAll(tasks.ToArray());

            //wait
            Console.WriteLine("Success");
        }
    }
}

如果是等待多个任务中的全部任务完成后就执行指定任务,可以使用Task.Factory.ContinueWhenAll()方法。

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

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            List<Task> tasks = new List<Task>();
            tasks.Add(Task.Run(() => { Console.WriteLine("任务1"); }));
            tasks.Add(Task.Run(() => { Console.WriteLine("任务2"); }));
            tasks.Add(Task.Run(() => { Console.WriteLine("任务3"); }));
            //只要前面三个任务中的任何一个完成,就执行后续任务
            Task.Factory.ContinueWhenAll(tasks.ToArray(), t => {
                Console.WriteLine("这是后续任务");
            });

            //等待全部任务完成
            Task.WaitAll(tasks.ToArray());

            //wait
            Console.WriteLine("Success");
        }
    }
}

实例:等待任务完成(任何一个任务)再向下执行

//等待任何一个Task任务完成
Task.WaitAny(new Task[] { t1, t2 });

//后续代码

实例:等待任务完成(全部)再向下执行

//等待任何一个Task任务完成
Task.WaitAll(new Task[] { t1, t2 });

//后续代码

实例:2个任务并行执行

using System.Threading.Tasks;
//任务1
Task t1 = Task.Run(() =>
{
    for (int i = 0; i < 101; i++)
    {
        Console.WriteLine("Task t1 Count-{0}", i);
    }
});

//任务2
Task t2 = Task.Run(() =>
{
    for (int i = 0; i < 101; i++)
    {
        Console.WriteLine("Task t2 Count-{0}", i);
    }
});

//等待t1完成
t1.Wait();
//等待t2完成
t2.Wait();

实例:从数值对象获得Task对象

Task<int> t1 = Task.FromResult(0);
Task<float> t2 = Task.FromResult(66.66F);
Task<double> t3 = Task.FromResult(66.66);
Task<decimal> t5 = Task.FromResult(100M);

实例:控制同时执行的任务数量

通过检测正在运行的任务数量来安排是否继续执行后续的任务。

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

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //任务列表
            //把要进行批量运行的任务放入该任务列表中
            List<Task> taskList = new List<Task>();

            //设置同时执行的任务数量
            int taskRunIntimeNumber = 2;

            //新建任务模拟多个任务用于批量执行
            Task t1 = new Task(() => { Thread.Sleep(3000); System.Console.WriteLine("任务1"); });
            Task t2 = new Task(() => { Thread.Sleep(5000); System.Console.WriteLine("任务2"); });
            Task t3 = new Task(() => { Thread.Sleep(2000); System.Console.WriteLine("任务3"); });
            Task t4 = new Task(() => { Thread.Sleep(7000); System.Console.WriteLine("任务4"); });
            Task t5 = new Task(() => { Thread.Sleep(8000); System.Console.WriteLine("任务5"); });
            taskList.AddRange(new[] { t1, t2, t3, t4, t5 });

            //筛选出正在执行的任务数量是否小于于等于2
            int i = 0;
            while (true)
            {
                if(taskList.Where(item => item.Status == TaskStatus.Running).Count() <= taskRunIntimeNumber)
                {
                    //如果任务数量小于等于2了,就开启需要工作的新任务
                    var needRunTask = taskList.Where(item => item.Status != TaskStatus.Running && item.Status == TaskStatus.Created).Take(1).FirstOrDefault();

                    //如果取出的任务为null了,说明没用要执行的任务了
                    if (needRunTask == null)
                    {
                        //所有任务都开始执行了
                        Console.WriteLine("所有任务都开始执行了");
                        break;
                    }

                    //启动需要工作的任务
                    needRunTask.Start();
                }
            }

            //等待开启的任务执行完毕
            Task.WaitAll(taskList.ToArray());

            //success
            Console.WriteLine("Success");
        }
    }
}

除了通过上面这种方式控制任务的线程数量,还可以通过Parallel.For()实现。

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

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //任务列表
            //把要进行批量运行的任务放入该任务列表中
            List<Task> taskList = new List<Task>();

            //新建任务模拟多个任务用于批量执行
            Task t1 = new Task(() => { Thread.Sleep(3000); System.Console.WriteLine("任务1"); });
            Task t2 = new Task(() => { Thread.Sleep(5000); System.Console.WriteLine("任务2"); });
            Task t3 = new Task(() => { Thread.Sleep(2000); System.Console.WriteLine("任务3"); });
            Task t4 = new Task(() => { Thread.Sleep(7000); System.Console.WriteLine("任务4"); });
            Task t5 = new Task(() => { Thread.Sleep(8000); System.Console.WriteLine("任务5"); });
            taskList.AddRange(new[] { t1, t2, t3, t4, t5 });

            //设置同时执行的任务数量
            ParallelOptions parallelOptions = new ParallelOptions()
            {
                //设置同时执行的任务数量
                MaxDegreeOfParallelism = 2,
            };

            //开始处理任务
            Parallel.For(0, taskList.Count(), parallelOptions,item => {
                //执行任务
                taskList[item].Start();
                Console.WriteLine($"任务{item+1}执行");
            });

            //等待开启的任务执行完毕
            Task.WaitAll(taskList.ToArray());
            
            //success
            Console.WriteLine("Success");
        }
    }
}

长时间运行的任务

说明

长时间运行的任务不应大量霸占整个处理器,所以需要使用额外的方式创建任务。

实现方式

在创建任务时使用TaskCreationOptions.LongRunning选项。

注意:使用Task.Factory.StartNew()方法,因为Task.Run()不支持该选项。

实例

Task task = Task.Factory.StartNew(() =>
{
    //进行的任务操作
    //...
},TaskCreationOptions.LongRunning);//指定长时间任务

任务的状态

说明

通过Task类型的Status属性来获得任务的状态。其值是一个TaskStatus类型的枚举值。

TaskStatus.WaitingForActivation
TaskStatus.WaitingToRun
TaskStatus.Running
TaskStatus.WaitingForChildrenToComplete
TaskStatus.RanToCompletion
TaskStatus.Canceled
TaskStatus.Faulted

只有RanToCompletion、Canceled、Faulted三个状态是最终状态。其他的状态都会发生改变,不是最终状态。

实例:获得任务的状态

//检测任务是否完成(不论是否正常完成)
Console.WriteLine(t1.IsCompleted);
//检测任务是否成功完成
Console.WriteLine(t1.IsCompletedSuccessfully);
//检测任务是否失败
Console.WriteLine(t1.IsFaulted);
//检测任务是否取消
Console.WriteLine(t1.IsCanceled);
//通过Status属性
switch (t1.Status)
{
    case TaskStatus.Created:
        break;
    case TaskStatus.WaitingForActivation:
        break;
    case TaskStatus.WaitingToRun:
        break;
    case TaskStatus.Running:
        break;
    case TaskStatus.WaitingForChildrenToComplete:
        break;
    case TaskStatus.RanToCompletion:
        break;
    case TaskStatus.Canceled:
        break;
    case TaskStatus.Faulted:
        break;
    default:
        break;
}

任务的Id

实例Task对象可以使用Id属性(只读)

Console.WriteLine(t1.Id);

也可以使用静态属性(只读)

Console.WriteLine(Task.CurrentId);

任务延续

说明

在普通的C#程序中,给定的代码会在代码完成后立即执行。即上一句完成了,就立即执行下一句。实际上任何给定的代码都有2个延续,正常执行延续 和 异常延续。

每个控制点都有一个延续,如果涉及到了异步任务,则会添加新的延续。意味着多个异步任务时,接着发生的事情是多维的,异步延续只是多了一个并行的执行路径。

异步任务使得能够将较小的任务合并成较大的任务,只需要定义并描述好异步延续的流程就可以了,使用Task实例的.ContinueWith()方法进行延续。

注意:同一个先驱任务两次调用ContinueWith(),先驱任务就会有2个延续任务。当先驱任务完成后,延续任务会异步执行。ContinueWith()方法还有第二个参数,是TaskCreationOptions枚举,用于指定在什么情况下进行任务延续。

实例:串联任务

上一个任务完成后再进行下一个任务。

Task<int> task1 = Task.Run(() => { return 10; })
    .ContinueWith((preivewTask) => { return preivewTask.Result + 10; })
    .ContinueWith((previewTask) => { return previewTask.Result + 10; });
Console.WriteLine(task1.Result); //30

实例:任务延续,任务123会按顺序执行

using System.Threading.Tasks;
//任务1
Task t1 = Task.Run(() => {
    Console.WriteLine("T1 Begin Task");
}).ContinueWith(t1=> {
    Console.WriteLine("T1 Continue Task");
});
//任务2
Task t2 = t1.ContinueWith(t1 => {
    Console.WriteLine("T2 Continue Task");
});
//任务3
Task t3 = t2.ContinueWith(t2 => {
    Console.WriteLine("T3 Continue Task");
});

//等待所有任务完成
Task.WaitAll(new Task[] { t1, t2, t3 });

//output
// T1 Begin Task
// T1 Continue Task
// T2 Continue Task
// T3 Continue Task
// 执行完成

实例:任务按次序运行

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

namespace PandaTestDemo
{
    class Program
    {
        /// <summary>
        /// 第一个任务
        /// </summary>
        public static void DoTask1()
        {
            Console.WriteLine("第一个任务开始");
            Thread.Sleep(1000);
        }

        /// <summary>
        /// 第二个任务
        /// </summary>
        /// <param name="t"></param>
        public static void DoTask2(Task t)
        {
            Console.WriteLine("任务1完成,执行任务2");
            Thread.Sleep(3000);
        }

        /// <summary>
        /// 第三个任务
        /// </summary>
        /// <param name="t"></param>
        public static void DoTask3(Task t)
        {
            Console.WriteLine("任务2完成,执行任务3");
            Thread.Sleep(3000);
        }

        static void Main(string[] args)
        {
            //新建线程1
            Task t1 = new Task(DoTask1);
            //任务1开始
            t1.Start();
            //任务2开始
            Task t2 = t1.ContinueWith(DoTask2);
            //任务3开始
            Task t3 = t2.ContinueWith(DoTask3);

            //wait
            Console.WriteLine("Success");
            Console.ReadKey();
        }
    }
}

取消任务

说明

有时需要提前终止任务,比如:请求超时、用户取消请求。很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号。野蛮的中止线程不是一个好选择,TPL使用的是协作式取消(Cooperative Cancellation)。这种方式可以安全的取消不需要的任务。

取消任务的语法

新建CancellationTokenSource取消源对象用于辅助任务取消。

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

所在命名空间

using System.Treading;

支持的参数:

CancellationTokenSource.CancelAfter()	//超时后发出取消信号
CancellationTokenSource.Cancel()		//发出取消信号
CancellationTokenSource.CancellationToken //Token对象

在具体的任务执行中监视CancellationTokenSource对象的成员,Token对象的IsCancellationRequested属性来确定是否需要取消任务。通过任务定期轮询它,检查是否发出了取消请求:

token.IsCancellationRequested

如果调用CancellationTokenSource.Cancel()发出了取消任务请求,任务立即返回,停止继续任务

CancellationTokenSource cansrc = new CancellationTokenSource();
cansrc.Cancel();

CancellationToken对象的成员

CancellationToken.IsCancellationRequested //是否取消
CancellationToken.None					//空,无取消操作
CancellationToken.ThrowlfCancellationRequested() //如果任务被取消,执行到这句话就抛异常

实例:任务可以被取消1

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

namespace Demo
{
    class Program
    {
        static async Task Main(string[] args)
        {
            //新建任务取消源实例
            CancellationTokenSource cansrc = new CancellationTokenSource();

            //异步监听输入
            Task t1 = Task.Run(() => {
                Console.WriteLine("按 C 键取消任务。");
                while (true)
                {
                    //监听用户输入确定是否要取消任务
                    if ((Console.ReadKey(true)).Key == ConsoleKey.C)
                    {
                        //取消任务
                        cansrc.Cancel();
                        break;
                    }
                }
            });

            //执行异步任务
            int result = await RunAsync(200, cansrc.Token);

            Console.WriteLine("计算结果:{0}", result);
            // 释放资源
            cansrc.Dispose();

            //wait
            Console.ReadKey();
        }

        /// <summary>
        /// 测试使用的异步任务
        /// </summary>
        /// <param name="maxNum"></param>
        /// <param name="token"></param>
        /// <returns></returns>
        static Task<int> RunAsync(int maxNum, CancellationToken token = default)
        {
            //用于返回任务完成后的值
            TaskCompletionSource<int> tcl = new TaskCompletionSource<int>();
            int x = 0;
            int res = 0;
            while (x < maxNum)
            {
                //检测任务是否已经被取消
                if (token.IsCancellationRequested)
                {
                    break;
                }
                res += x;
                x += 1;
                Console.WriteLine($"当前执行到{x},当前值{res}");
                Task.Delay(50).Wait();
            }
            //设置要返回的值
            tcl.SetResult(res);
            //返回值
            return tcl.Task;
        }
    }
}

实例:任务可以被取消2

//新建一个任务取消源实例
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

//新建任务
Task.Run(() =>
{
    //注意:这里传进去的是Token
    SomeTask(cancellationTokenSource.Token);
}, cancellationTokenSource.Token); //在任务中定义取消Token标记

//等待用户按下按键(任意按键)
if((Console.ReadKey()).Key == ConsoleKey.Enter)
{
    Console.WriteLine("按下了Enter键");
    //一但用户输入了数据就进行取消任务
    cancellationTokenSource.Cancel();
}

//测试使用的任务
//注意:参数是Token
public static void SomeTask(CancellationToken cancellationToken)
{
    for (int i = 0; i < 10000; i++)
    {
        for (int j = 0; j < 1000; j++)
        {
            //检测是否有取消任务的请求
            if (cancellationToken.IsCancellationRequested)
            {
                //取消任务,直接返回
                return;
            }
            else
            {
                Console.WriteLine("Task WriteLine -{0}", i);
            }
        }
    }
}

ASP.NET Core中的任务取消

ASP.NET Core开发中,一般不需要自己处理CancellationToken、CancellationTokenSource这些,只要做到“能转发CancellationToken就转发”即可。ASP.NET Core会对于用户请求中断进行处理。

对任务进行资源清理

Task在等待完成时会分配一个WaitHandle,WaitHandle是支持IDisposable的。所以Task也支持IDisposable接口,Task在退出时会自动调用WaitHandle终结器。

任务异常处理

在任务内进行捕获异常

using System;

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {

            Task t1 = Task.Run(() => {
                //在线程内部捕获异常
                try
                {
                    throw new Exception("Panda Test");
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }

            });

            Task.WaitAll(t1);

            Console.WriteLine("Success");
            Console.ReadKey();
        }
    }
}

基于Task的异步模式(TAP,Task-Based Asynchronous Pattern)

说明

处理异步工作,任务提供了比线程更好的抽象,任务会自动调度恰当数量的线程。但使用任务抽象的缺点是它颠倒了程序逻辑。为了解决这个问题出现了基于任务的异步模式/

注意:从C# 5.0开始才支持基于Task的异步模式。

基于任务的异步模式可以轻松的将同步的程序转为异步程序,底层由C#进行自动转换,将异步方法转为任务延续。

基于任务的异步模式实现

使用asyncawait关键字实现,在方法上使用async关键字修饰。并且方法只能返回Task、Task<T>、void之一。在需要等待的(创建新线程)任务使用await修饰。

注意:

  • 使用async修饰的方法不一定会创建新线程。因为方法内不一定有需要异步工作的代码。
  • await修饰的位置并不一定造成阻塞。
  • await位置和其他代码是并行执行的。

实例:

public async static Task<long?> GetPageLength()
{
    HttpClient client = new HttpClient();
    var httpMessage = await client.GetAsync("http://apress.com");
    return httpMessage.Content.Headers.ContentLength;
}

实例2:

public async Task<ViewResult> Index()
{
    List<string> output = new List<string>();
    await foreach (long? len in MyAsyncMethods.GetPageLengths(output,
    "apress.com", "microsoft.com", "amazon.com"))
    {
        output.Add($"Page length: { len}");
    }
    return View(output);
}

实例:无返回值

public static async void SomeTask2()
{
    await Task.Run(() => {

    });
}

实例:返回Task

public static async Task SomeTask2()
{
    await Task.Run(() => {

    });
}

实例:返回Task

public static async Task<string> SomeTask2()
{
    await Task.Run(() => {
        return "Panda666";
    });

    return "Panda666";
}

实例:在异步方法中调用其他异步方法

async Task<int> DoSomething()
{
    await DoSomethingAsync1();
    await DoSomethingAsync2();
    return await DoSomethingAsync3() + 1;
}

异步方法(async method)

说明

“异步方法”是指用async关键字修饰的方法(Asynchronous Function)。异步方法的方法名一般以Async结尾。调用异步方法一般在前面加上await关键字,表示等待该异步方法执行完成。使用await修饰后,返回的是异步方法Task<TResult>中的TResult类型,而不再是Task<TResult>类型。异步方法具有传染性。一个方法中如果由await调用,则这个方法必须使用async修饰。

static async Task<string> SomeMethod()
{
    await Task.Delay(TimeSpan.FromSeconds(2));
    return "Panda666.com";
}

异步方法支持的返回值

async methods can have the following return types:

返回类型 说明
Task<TResult> 用于异步方法( async method)返回指定值。最常用的返回类型,TResult是异步方法真正返回的类型。
Task 用于异步方法(async method)不返回值,但可以被等待。
void 用于异步方法(async method)不返回值,但不可以被等待。

注意:使用Task类型必须引入命名空间using System.Threading.Tasks;

注意:Task和void区别,都用于不返回值,但返回void的方法不可以被await等待,而返回Task就可以。

语法说明

方法使用async关键字修饰,异步方法内部使用await关键字修饰异步操作,在异步操作的地方使用await关键字修饰。异步函数内部必须至少一个await关键字,await之间不是同时运行的,而是按代码顺序运行的,即前一个await完成后才会运行后一个await。

//声明一个异步方法(async method)
async Task<int> GetLeisureHours()
{
    // Task.FromResult is a placeholder for actual work that returns a string.  
    var today = await Task.FromResult<string>(DateTime.Now.DayOfWeek.ToString());
    // The method then can process the result in some way.  
    int leisureHours;
    if (today.First() == 'S')
        leisureHours = 16;
    else
        leisureHours = 5;
    return leisureHours;
}

异步方法限制

  • 不可以在catch、finally、lock、unsafe代码块中使用await操作符。
  • 不可以对异步函数的参数使用ref、out修饰。

异步函数性能

  • 能用普通函数就不要使用异步函数,普通调用比async方法快40-~50倍,异步方法本质是转为一个带状态机的类,执行效率不高。
  • 可能会占用非常多的线程。

Async/await Main

在C# 7.1中加入了Async/await关键字对Main函数的支持。

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

namespace PandaTest
{
    public class Program
    {
        public async static Task Main(string[] args)
        {
            Console.WriteLine("Success");
        }
    }
}

异步任务转为非异步执行

如果同样的功能,既有同步方法,又有异步方法,最好优先使用异步方法。便于调试和提高运行效率。最新的.NET Core中的操作一般都支持异步操作,但有一些方法中没用办法使用异步方法,为此需要将异步任务转为同步执行的方式来运行。

注意:虽然可以将异步方法以非异步的方式执行,但容易发生死锁问题。

异步任务转为非异步执行主要方法如下:

  • 调用异步方法返回的Task实例对象的Wait()方法,适合于没用返回值的异步方法。
  • 调用异步方法返回的Task实例对象的Result属性,适合于由返回值的异步方法。

实例:使用Wait()方法

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

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //调用异步方法
            string filePath = @"C:\Users\Administrator\Downloads\Text.txt";
            ReadAllTextAsync(filePath, CancellationToken.None).Wait();

            //wait
            Console.WriteLine("Success");
        }

        /// <summary>
        /// 读取文本文件(异步)
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public async static Task ReadAllTextAsync(string filePath,CancellationToken cancellationToken)
        {
            string result = await File.ReadAllTextAsync(filePath, cancellationToken);
            Console.WriteLine(result);
        }
    }
}

实例:使用Result属性

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

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //调用异步方法
            string filePath = @"C:\Users\Administrator\Downloads\Text.txt";
            //获得异步方法结果结果
            string result = ReadAllTextAsync(filePath, CancellationToken.None).Result;

            Console.WriteLine("文本结果");
            Console.WriteLine(result);
            //wait
            Console.WriteLine("Success");
        }

        /// <summary>
        /// 读取文本文件(异步)
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public async static Task<string> ReadAllTextAsync(string filePath,CancellationToken cancellationToken)
        {
            return await File.ReadAllTextAsync(filePath, cancellationToken);
        }
    }
}

异步Lambda

说明

类似异步方法,转为Lambda表达式即可

注意:

async Lambda表达式和具有和async方法一样的限制

async Lambda表达式必须返回void、Task、Task的委托

实例:基本异步Lambda

//定义
Func<int,int,Task> t1 = async (int i, int j) =>
{
    await Task.Run(() => { });
};
//使用
t1(666, 888);//使用
t1(666, 888);

实例:异步委托注册到线程池

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

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //线程池中使用异步Lambda
            ThreadPool.QueueUserWorkItem(async (obj) => {
                string filePath = @"C:\Users\Administrator\Downloads\Text.txt";
                await ReadAllTextAsync(filePath, CancellationToken.None);
            });
            
            Console.WriteLine("Success");
            Console.ReadKey();
        }

        /// <summary>
        /// 读取文本文件(异步)
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public async static Task ReadAllTextAsync(string filePath,CancellationToken cancellationToken)
        {
            await Task.Delay(3000);
            string result = await File.ReadAllTextAsync(filePath, cancellationToken);
            Console.WriteLine(result);
        }
    }
}

异步方法的本质

await、async是“语法糖”,最终编译成“状态机调用”。即转为正常的方法调用,但是一个状态机,内部存在多个状态变化。

用await看似是“等待”,经过编译后,其实没有wait。async的方法会被C#编译器编译成一个类、会主要根据await调用进行切分为多个状态,对async方法的调用会被拆分为对MoveNext的调用。

await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码。可以使用Thread.CurrentThread.Ma
nagedThreadId获得当前线程Id来检测前后执行的线程是否是同一个线程。就像餐馆中的服务员,如果餐馆的生意并不好,就始终是一个服务员进行服务,没有必要让多个服务员进行服务。但如果是生意好,每个流程可能都是不同的服务员。

异步与yield

yield return不仅能够简化数据的返回,而且可以让数据处理“流水线化”,提升性能。在旧版C#中,async方法中不能用yield.从C#8.0开始,把返回值声明为IAsyncEnumerable(不要带Task),然后遍历的时候用await foreach()即可。

static async Task Main(string args)
{
    await foreach(var s in Test())
    {
        Console.WriteLine(s);
    }
}

//异步yield
static async IAsyncEnumerable<string> Test()
{
     yield "returnyzk";
	yield "returnyouzack";
}

异步方法注意

  • 异步方法并不是一定都在新线程中执行,如果任务执行的快有可能都在主线程上执行。

  • async方法中的异步方法如果没用使用await关键字修饰就会被当做同步方法执行。

public async static Task ReadAllTextAsync(string filePath,CancellationToken cancellationToken)
{
    //被当作同步方法,不会等待
    File.ReadAllTextAsync(filePath, cancellationToken);

    //被当作同步方法,不会等待
    Task.Delay(3000);
}
  • 异步方法不一定都使用async修饰,比如可以直接返回内部异步操作的结果,而不用再次封装。只甩手Task,不“拆完了再装”(异步方法本质仍是转为普通方法调用)。这样的优点是运行效率更高,不会造成线程浪费。
//定义不用async修饰的异步方法
public static Task ReadAllTextAsync(int flag, string filePath,CancellationToken cancellationToken)
        {
            if(flag == 0)
            {
                return File.ReadAllTextAsync(filePath, cancellationToken);
            }
            else if(flag == 1)
            {
                return File.ReadAllTextAsync(filePath, cancellationToken);
            }
            else
            {
                return File.ReadAllTextAsync(filePath, cancellationToken);
            }

        }
//在其他异步方法中使用
await ReadAllTextAsync(1,filePath, CancellationToken.None);

  • 返回Task的不一定都要标注async,标注async只是让我们更加方便的使用await,让编译器为我们代劳。
  • 如果一个异步方法只是对别的异步方法调用的转发,并没有太多复杂的逻辑(比如等待A的结果,再调用B;把A调用的返回值拿到内部做一些处理再返回),那么就可以去掉async。
  • 如果想在异步方法中暂停一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程,而要用await Task.Delay()
  • async是提示编译器为异步方法中的await代码进行分段处理的,而一个异步方法是否修饰了async对于方法的调用者来讲没区别的,因此对于接口中的方法或者抽象方法不能修饰为async。

并行迭代(Parallel)

说明

Parallel类是一个轻量级的并行操作执行类,主要用在for或foreach循环的并行代码上。通过调配多个处理器的资源来运行循环提升性能。

提示:使用并行迭代,每次迭代之间不可以有关联。并且不要对同一个资源进行竞争。

What is the difference between the Parallel For loop and Standard C# for loop?

image

1.In the case of the standard C# for loop, the loop is going to run using a single thread whereas, in the case of Parallel For loop, the loop is going to execute using multiple threads.

2.The second difference is that, in the case of the standard C# for loop, the loop is iterated in sequential order whereas, in the case of Parallel For loop, the order of the iteration is not going to be in sequential order.

Note: When the iterations are independent of each other, means, subsequent iterations do not need the state updates made by previous iterations, then in such cases, we need to use Task Parallel Library (TPL) to run each iteration in parallel on all the available cores. Moreover, the iteration should be an expensive iteration otherwise we will get negative performance, which we will also discuss as part of this article.

并行版本的Parallel.For

支持的API

public static ParallelLoopResult For(int fromInclusive, int toExclusive, Action<int, ParallelLoopState> body)
public static ParallelLoopResult For(int fromInclusive, int toExclusive, Action<int> body)
public static ParallelLoopResult For(int fromInclusive, int toExclusive, ParallelOptions parallelOptions, Action<int, ParallelLoopState> body)
public static ParallelLoopResult For(int fromInclusive, int toExclusive, ParallelOptions parallelOptions, Action<int> body)
public static ParallelLoopResult For(long fromInclusive, long toExclusive, Action<long, ParallelLoopState> body)
public static ParallelLoopResult For(long fromInclusive, long toExclusive, Action<long> body)
public static ParallelLoopResult For(long fromInclusive, long toExclusive, ParallelOptions parallelOptions, Action<long, ParallelLoopState> body)
public static ParallelLoopResult For(long fromInclusive, long toExclusive, ParallelOptions parallelOptions, Action<long> body)
public static ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally)
public static ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, ParallelOptions parallelOptions, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally)
public static ParallelLoopResult For<TLocal>(long fromInclusive, long toExclusive, Func<TLocal> localInit, Func<long, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally)
public static ParallelLoopResult For<TLocal>(long fromInclusive, long toExclusive, ParallelOptions parallelOptions, Func<TLocal> localInit, Func<long, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally)

实例:循环个10次

Parallel.For(0, 10, i => {
    Console.WriteLine(i);
});

实例:循环个10000次

Parallel.For(0, 10000, (i) => {
    Console.WriteLine(i);
});

实例:控制同时执行的任务数量

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

namespace PandaTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //任务列表
            //把要进行批量运行的任务放入该任务列表中
            List<Task> taskList = new List<Task>();

            //新建任务模拟多个任务用于批量执行
            Task t1 = new Task(() => { Thread.Sleep(3000); System.Console.WriteLine("任务1"); });
            Task t2 = new Task(() => { Thread.Sleep(5000); System.Console.WriteLine("任务2"); });
            Task t3 = new Task(() => { Thread.Sleep(2000); System.Console.WriteLine("任务3"); });
            Task t4 = new Task(() => { Thread.Sleep(7000); System.Console.WriteLine("任务4"); });
            Task t5 = new Task(() => { Thread.Sleep(8000); System.Console.WriteLine("任务5"); });
            taskList.AddRange(new[] { t1, t2, t3, t4, t5 });

            //设置同时执行的任务数量
            ParallelOptions parallelOptions = new ParallelOptions()
            {
                //设置同时执行的任务数量
                MaxDegreeOfParallelism = 2,
            };

            //开始处理任务
            Parallel.For(0, taskList.Count(), parallelOptions,item => {
                //执行任务
                taskList[item].Start();
                Console.WriteLine($"任务{item+1}执行");
            });

            //等待开启的任务执行完毕
            Task.WaitAll(taskList.ToArray());
            
            //success
            Console.WriteLine("Success");
        }
    }
}

ParallelOptions 迭代选项

用于配置迭代的设置。

实例:设置迭代最大的线程数量。

ParallelOptions parallelOptions = new ParallelOptions()
{
    //设置最大线程为5
    MaxDegreeOfParallelism = 5,
};

//在迭代中使用
Parallel.For(0, 10, parallelOptions, i => {
    Console.WriteLine(i);
});

中断并行迭代

说明

Break()表示在当前线程上完成当前迭代之前的所有线程上的所有迭代,然后退出循环。

Stop()表示在方便的时候停止所有的迭代。

实例:Break()停止迭代

Parallel.For(0, 10, (i,state) => {
    if(i == 2)
    {
        state.Break();
    }
    else
    {
        Console.WriteLine(i);
    }
});

实例:Stop()停止迭代

Parallel.For(0, 10, (i,state) => {
    if(i == 2)
    {
        state.Stop();
    }
    else
    {
        Console.WriteLine(i);
    }
});

并行版本的Parallel.ForEach

支持的API

public static ParallelLoopResult ForEach<TSource>(OrderablePartitioner<TSource> source, Action<TSource, ParallelLoopState, long> body)
public static ParallelLoopResult ForEach<TSource>(OrderablePartitioner<TSource> source, ParallelOptions parallelOptions, Action<TSource, ParallelLoopState, long> body)
public static ParallelLoopResult ForEach<TSource>(Partitioner<TSource> source, Action<TSource, ParallelLoopState> body)
public static ParallelLoopResult ForEach<TSource>(Partitioner<TSource> source, Action<TSource> body)
public static ParallelLoopResult ForEach<TSource>(Partitioner<TSource> source, ParallelOptions parallelOptions, Action<TSource, ParallelLoopState> body)
public static ParallelLoopResult ForEach<TSource>(Partitioner<TSource> source, ParallelOptions parallelOptions, Action<TSource> body)
public static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, Action<TSource, ParallelLoopState, long> body)
public static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, Action<TSource, ParallelLoopState> body)
public static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, Action<TSource> body)
public static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Action<TSource, ParallelLoopState, long> body)
public static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Action<TSource, ParallelLoopState> body)
public static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Action<TSource> body)
public static ParallelLoopResult ForEach<TSource, TLocal>(OrderablePartitioner<TSource> source, Func<TLocal> localInit, Func<TSource, ParallelLoopState, long, TLocal, TLocal> body, Action<TLocal> localFinally)
public static ParallelLoopResult ForEach<TSource, TLocal>(OrderablePartitioner<TSource> source, ParallelOptions parallelOptions, Func<TLocal> localInit, Func<TSource, ParallelLoopState, long, TLocal, TLocal> body, Action<TLocal> localFinally)
public static ParallelLoopResult ForEach<TSource, TLocal>(Partitioner<TSource> source, Func<TLocal> localInit, Func<TSource, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally)
public static ParallelLoopResult ForEach<TSource, TLocal>(Partitioner<TSource> source, ParallelOptions parallelOptions, Func<TLocal> localInit, Func<TSource, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally)
public static ParallelLoopResult ForEach<TSource, TLocal>(IEnumerable<TSource> source, Func<TLocal> localInit, Func<TSource, ParallelLoopState, long, TLocal, TLocal> body, Action<TLocal> localFinally)
public static ParallelLoopResult ForEach<TSource, TLocal>(IEnumerable<TSource> source, Func<TLocal> localInit, Func<TSource, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally)
public static ParallelLoopResult ForEach<TSource, TLocal>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TLocal> localInit, Func<TSource, ParallelLoopState, long, TLocal, TLocal> body, Action<TLocal> localFinally)
public static ParallelLoopResult ForEach<TSource, TLocal>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TLocal> localInit, Func<TSource, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)

实例:遍历集合

List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6 };
Parallel.ForEach(list, item => {
    Console.WriteLine(item);
});

实例:遍历集合

var numbers = Enumerable.Range(1, 100000);
Parallel.ForEach(numbers, (i) =>
{
    Console.WriteLine(i);
});

ParallelOptions 迭代选项

用于配置迭代的设置。

设置迭代最大的线程数量。

ParallelOptions parallelOptions = new ParallelOptions()
{
    //设置最大线程为5
    MaxDegreeOfParallelism = 5,
};

在迭代中使用

List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6 };
Parallel.ForEach(list, parallelOptions,item => {
    Console.WriteLine(item);
});

中断并行迭代

说明

Break()表示在当前线程上完成当前迭代之前的所有线程上的所有迭代,然后退出循环。

Stop()表示在方便的时候停止所有的迭代。

实例:Break()停止迭代

List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6 };
Parallel.ForEach(list, (item,state) => {
    if (item == 2)
    {
        state.Break();
    }
    else
    {
        Console.WriteLine(item);
    }
});

实例:Stop()停止迭代

List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6 };
Parallel.ForEach(list, (item,state) => {
    if (item == 2)
    {
        state.Stop();
    }
    else
    {
        Console.WriteLine(item);
    }
});

并行任务同时执行Parallel.Invoke

说明

同时运行安排的任务。注意:不保证先后顺序

public static void Invoke(params Action[] actions)
public static void Invoke(ParallelOptions parallelOptions, params Action[] actions)

实例:

Parallel.Invoke(
    //lambda表达式
    () => { Console.WriteLine("ABC"); },
    //匿名方法
    delegate { Console.WriteLine("DEF"); }
    //...里面可以放任何Action委托
);

ParallelOptions 迭代选项

用于配置迭代的设置。

设置迭代最大的线程数量。

ParallelOptions parallelOptions = new ParallelOptions()
{
    //设置最大线程为5
    MaxDegreeOfParallelism = 1,
};

使用

ParallelOptions parallelOptions = new ParallelOptions()
{
    //设置最大线程为5
    MaxDegreeOfParallelism = 1,
};
Parallel.Invoke(parallelOptions,
                //lambda表达式
                () => { Console.WriteLine("ABC"); },
                //匿名方法
                delegate { Console.WriteLine("DEF"); }
                //...里面可以放任何Action委托
);

异步等待

说明

.NET程序中的异步编程使用关键字:async 和 await

async关键字加在方法声明上,目的是使方法内的await关键字生效

C# 5(.NET 4.5)引入的语法糖
C# 7.1开始 Main方法也支持
C# 8.0开始,可以使用await foreach和await using

返回值:

如果async方法有返回值,应返回Task

如果没有返回值,应返回 Task

这些Task类型相当于 Future,用来在异步方法结束时通知主程序

注意

为了保持向后兼容,不要用 void 作为async方法的返回类型

async方法可以返回 void,但是这仅限于编写事件处理程序

async方法如果没有返回值,要返回Task,而不是void

规范

​ 异步方法以Async结尾

具体原理

异步方法需要使用async修饰,即async方法

async方法需要返回Task或者Task类型

在async方法内调用异步操作时,需要加上await关键字,表示异步需要等待

当async方法执行到await位置时,当前线程进入等待状态,但不会阻塞

当await位置的异步任务执行完成后,会回到当前线程中继续执行后面的代码

桌面软件与async/await

一般情况下任何UI组件的交互都是在同一个UI线程中发生的

如果一个UI事件操作事件的执行需要大量时间会造成UI线程卡顿

用户会明显感到软件界面卡死

解决办法就是使用异步方式执行事件处理函数

//使用async修饰方法
private async void SomeControl_Click(object sender, EventArgs e)
{
    //内部对异步操作使用await修饰
    await xxxx.xxxAsync();
}

标签:异步,Task,Console,Process,任务,Parallelization,Part2,WriteLine,public
From: https://www.cnblogs.com/cqpanda/p/16749422.html

相关文章