【To .NET】C#异步编程,从线程到async/await

本文介绍了C#中的线程操作、线程池管理、Task Parallel Library (TPL)以及异步编程的async/await,包括创建线程、线程状态控制、线程池优化、任务的使用、线程池优势和TPL的原理与实践。
摘要由CSDN通过智能技术生成

大家好!我是未来村村长,就是那个“请你跟我这样做,我就跟你这样做!”的村长👨‍🌾!

👩‍🌾“人生苦短,你用Python”,“Java内卷,我用C#”。

​ 从Java到C#,不仅仅是语言使用的改变,更是我从理想到现实,从象牙塔到大熔炉的第一步。.NET是微软的一盘棋,而C#是我的棋子,只希望微软能下好这盘棋,而我能好好利用这个棋子。

一、线程与线程池

1、线程的创建、暂停、等待及终止

(1)线程的创建

​ 使用线程类需要导入命名空间System.Threading,我们通过new Thread(方法名)的方式来创建线程,然后通过线程对象.Start来启动线程。

using System;
using System.Threading;

namespace CSharp.Learn
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Main线程启动...");
            //线程创建
            Thread t = new Thread(PrintNumbers);
            //线程启动
            t.Start();
            Console.WriteLine("Main线程结束");
        }
        static void PrintNumbers()
        {
            Console.WriteLine("非主线程启动...");
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("非主线程:"+i);
            }
        }
    }
}

(2)线程的暂停

​ 线程调用sleep()后会处于休眠状态。

using System;
using System.Threading;

namespace CSharp.Learn
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Main线程启动...");
            Thread t = new Thread(PrintNumbers);
            t.Start();
            Console.WriteLine("Main线程结束");
        }
        static void PrintNumbers()
        {
            Console.WriteLine("非主线程启动...");
            for (int i = 0; i < 10; i++)
            {
                //线程暂停
                Thread.Sleep(TimeSpan.FromSeconds(2));
                Console.WriteLine("非主线程:"+i);
            }
        }
    }
}

(3)线程等待

​ 在一个线程中,另一个线程调用Join方法会让当前线程进入等待状态,直到另一个线程执行完毕后,当前线程才能执行。

using System;
using System.Threading;

namespace CSharp.Learn
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Main线程启动...");
            Thread t = new Thread(PrintNumbers);
            t.Start();
            //线程等待
            t.Join();
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("主线程:" + i);
            }
            Console.WriteLine("Main线程结束");
        }
        static void PrintNumbers()
        {
            Console.WriteLine("非主线程启动...");
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(TimeSpan.FromSeconds(2));
                Console.WriteLine("非主线程:"+i);
            }
        }
    }
}
(4)线程终止

​ 使用Abort方法将给线程注入ThreadAbortException,导致线程被终结,但因为该异常可以在任何时刻发生并可能彻底摧毁应用程序。

Thread.Abort();

2、线程的状态、优先级

​ 我们可以通过以下语句来查看当前线程的线程状态。

Thread.CurrentThread.ThreadState.ToString();

​ 我们可以通过设置Thread中的Priority属性来设置线程的优先级。

t.Priority = ThreadPriority.Highest;
t.Priority = ThreadPriority.Lowest;

3、前台线程与后台线程

​ 默认情况下,显式创建的线程为前台线程,我们通过修改其属性IsBackground属性为true来创建一个后台线程。

t.IsBackground = true;

​ 前台线程和后台线程的主要区别:进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。

4、lock关键字

​ 当多个线程同时操作同一对象时就可能发生线程安全问题,我们可以通过lock关键字将一个代码段设置为临界资源,当出现竞争时,后进入线程会进行排队等待,直到占有临界资源的线程释放该资源。实际上关键字lock是类Monitor用例中的一个语法糖。

​ 例如:List的内部类中SynchronizedList的Add方法就采用lock关键字来达成线程同步。

private Object _root;

public void Add(T item) {
	lock (_root) { 
		_list.Add(item); 
	}
}

5、线程池

​ .NET Framework的ThreadPool类提供一个线程池,该线程池可用于执行任务、发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器。那么什么是线程池?线程池其实就是一个存放线程对象的“池子(pool)”,他提供了一些基本方法,如:设置pool中最小/最大线程数量、把要执行的方法排入队列等等。ThreadPool是一个静态类,因此可以直接使用,不用创建对象。

线程池的特点

  • 一个进程有且只能管理一个线程池,线程池线程都是后台线程(即不会阻止进程的停止)。
  • 每个线程都使用默认堆栈大小,以默认的优先级运行,并处于多线程单元中。超过最大值的其他线程需要排队,但它们要等到其他线程完成后才启动。
  • 线程上限可以改变,通过使用ThreadPool.GetMax+Threads和ThreadPool.SetMaxThreads方法,可以获取和设置线程池的最大线程数。
  • 默认情况下,每个处理器维持一个空闲线程,即默认最小线程数 = 处理器数。
  • 当进程启动时,线程池并不会自动创建。当第一次将回调方法排入队列(比如调用ThreadPool.QueueUserWorkItem方法)时才会创建线程池。在对一个工作项进行排队之后将无法取消它。
  • 线程池中线程在完成任务后并不会自动销毁,它会以挂起的状态返回线程池,如果应用程序再次向线程池发出请求,那么这个挂起的线程将激活并执行任务,而不会创建新线程,这将节约了很多开销。只有线程达到最大线程数量,系统才会以一定的算法销毁回收线程。

线程池的优势

  • 可以避免创建和销毁消除的开支,从而可以实现更好的性能和系统稳定性。
  • 把线程交给系统进行管理,程序员不需要费力于线程管理,可以集中精力处理应用程序任务。

二、TPL

​ 官方文档:https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/task-parallel-library-tpl

1、TPL介绍

​ TPL(Task Parallel Library) 的目的是通过简化将并行和并发添加到应用程序的过程来提高开发人员的工作效率。

​ 使用线程池可以减少并行操作时操作系统资源的开销,然而使用线程池并不简单,从线程池的工作线程中获取结果也并不容易。于是就有了TPL,TPL可被认为是线程池上的又一个抽象层,其对开发人员隐藏了与线程池交互的底层代码,并提供了更细粒度的API。

​ 任务并行库 (TPL) 以“任务”的概念为基础,后者表示异步操作。 在某些方面,任务类似于线程或 ThreadPool 工作项,但是抽象级别更高。 术语“任务并行”是指一个或多个独立的任务同时运行。 任务提供两个主要好处:

  • 系统资源的使用效率更高,可伸缩性更好。

    在后台,任务排队到已使用算法增强的 ThreadPool,这些算法能够确定线程数并随之调整,提供负载平衡以实现吞吐量最大化。 这会使任务相对轻量,你可以创建很多任务以启用细化并行。

  • 对于线程或工作项,可以使用更多的编程控件。

    任务和围绕它们生成的框架提供了一组丰富的 API,这些 API 支持等待、取消、继续、可靠的异常处理、详细状态、自定义计划等功能。

​ 出于这两个原因,在 .NET 中,TPL 是用于编写多线程、异步和并行代码的首选API。

2、任务

(1)创建任务
using System;
using System.Threading;
using System.Threading.Tasks;

namespace CSharp.Learn
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Task t1 = new Task(() => TaskMethod("t1"));
            Task t2 = new Task(() => TaskMethod("t2"));
            t1.Start();
            t2.Start();
            Task.Run(() => TaskMethod("t3"));
            Task.Factory.StartNew(() => TaskMethod("t5"));
            Task.Factory.StartNew(() => TaskMethod("t6"), TaskCreationOptions.LongRunning);
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        static void TaskMethod(string name)
        {
            Console.WriteLine(
                $"{name}:" +
                $"-ManagedThreadId:{Thread.CurrentThread.ManagedThreadId} \n" +
                $"-isThreadPoolThread: {Thread.CurrentThread.IsThreadPoolThread}");
        }
    }
}

​ 我们使用Task的构造函数创建了两个任务,然后通过Start方法运行。使用Task.Run和Task.Factory.StartNew也可以运行任务。

(2)从任务中获取结果值
using System;
using System.Threading;
using System.Threading.Tasks;

namespace CSharp.Learn
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Task<int> task = CreateTask("Task-1");
            task.Start();
            int result = task.Result;
            Console.WriteLine($"Task-1-result:{result}");
            Console.WriteLine($"Task-1-status:{task.Status}");

            task = CreateTask("Task-2");
            task.RunSynchronously();
            result = task.Result;
            Console.WriteLine($"Task-2-result:{result}");
            Console.WriteLine($"Task-2-status:{task.Status}");

        }

        static Task<int> CreateTask(string name)
        {
            return new Task<int>(()=>TaskMethod(name));
        }
        
        static int TaskMethod(string name)
        {
           Console.WriteLine(
                $"{name}:" +
                $"-ManagedThreadId:{Thread.CurrentThread.ManagedThreadId} \n" +
                $"-isThreadPoolThread: {Thread.CurrentThread.IsThreadPoolThread}");
            Thread.Sleep(TimeSpan.FromSeconds(2));
            return 42;
        }
    }
}

​ 任务运行后,我们可以通过Task的Result属性来获取任务运行的返回值,Result的类型为Task<T>的指定泛型类型。通过RunSynchronously()运行的Task在主线程中运行,而Start开启的运行在线程池中运行。

三、async/await

​ 官方文档:C# 中的异步编程 | Microsoft Docs

1、异步与async/await原理说明

(1)异步说明

​ 同步:当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法。

​ 异步:当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。如下图,官方使用了做早餐为例子进行同步与异步的说明。

在这里插入图片描述

在这里插入图片描述

(2)异步函数说明

异步函数编写

  • C#5.0引入了异步函数,要创建一个异步函数,首先需要用async关键字标注一个方法,才能拥有async属性。除此,异步函数必须返回Task或Task<T>类型。

  • 在async关键字标注的方法内部,可以使用await操作符,异步函数中至少要拥有一个await操作符,否则会出现编译警告。(如果程序中有两个连续await操作符,它们实际上依旧是顺序执行)

  • 最好在异步函数方法名加Async,表明该方法为异步函数

调用过程:await调用的等待期间,.NET会把当前的线程返回给线程池,工作线程继续执行后续操作,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码。

使用限制

  • 不能在catch、finally、lock或unsafe代码块中使用await操作符
  • 不允许对任何异步函数使用ref或out参数

异步方法也可以不标注async和使用await关键字:当该方法只需要返回Task或Task<>类型的参数时,可以(最好)不加async和await关键字。因为async和await是语法糖,在编译过程中会重新创建一个类,在不必要的时刻可以不适用async。

注意:使用了async后不要使用sleep来进行线程执行的暂停,因为Thread.Sleep()会使导致线程阻塞,线程切换会增加程序运行负担。通常使用如下语句。

await Task.Delay();
(3)async/await原理

​ 如果使用 async 修饰符将某种方法指定为异步方法:标记的异步方法可以使用await来指定暂停点。 await运算符通知编译器异步方法:在等待的异步过程完成后才能继续通过该点。 同时,控制返回至异步方法的调用方。

​ await运算符暂停对其所属的await方法的求值,直到其操作数表示的异步操作完成。

​ 异步操作完成后,await运算符将返回操作的结果(如果有)。 当await运算符应用到表示已完成操作的操作数时,它将立即返回操作的结果,而不会暂停其所属的方法。

2、异步返回类型

(1)Task 返回类型

​ 不包含 return 语句的异步方法或包含不返回操作数的 return 语句的异步方法通常具有返回类型 Task。 如果此类方法同步运行,它们将返回 void。 如果在异步方法中使用 Task 返回类型,调用方法可以使用 await 运算符暂停调用方的完成,直至被调用的异步方法结束。

​ 下例中的 WaitAndApologizeAsync 方法不包含 return 语句,因此该方法会返回 Task 对象。 返回 Task 可等待 WaitAndApologizeAsync。Task 类型不包含 Result 属性,因为它不具有任何返回值。

public static async Task DisplayCurrentInfoAsync()
{
    await WaitAndApologizeAsync();

    Console.WriteLine($"Today is {DateTime.Now:D}");
    Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
    Console.WriteLine("The current temperature is 76 degrees.");
}

static async Task WaitAndApologizeAsync()
{
    await Task.Delay(2000);

    Console.WriteLine("Sorry for the delay...\n");
}

​ 通过使用 await 语句而不是 await 表达式等待 WaitAndApologizeAsync,类似于返回 void 的同步方法的调用语句。 Await 运算符的应用程序在这种情况下不生成值。 当 await 的右操作数是 Task<T> 时,await 表达式生成的结果为 T。 当 await 的右操作数是 Task 时,await 及其操作数是一个语句。

(2)Task<TResult> 返回类型

​ Task<TResult> 返回类型用于某种异步方法,此异步方法包含 return 语句,其中操作数是 TResult。

​ 在下面的示例中,GetLeisureHoursAsync 方法包含返回整数的 return 语句。 该方法声明必须指定 Task<int> 的返回类型。 FromResult 异步方法是返回 DayOfWeek 的操作的占位符。

public static async Task ShowTodaysInfoAsync()
{
    int i = await GetLeisureHoursAsync()
    Console.WriteLine(i);
}

static async Task<int> GetLeisureHoursAsync()
{
    DayOfWeek today = await Task.FromResult(DateTime.Now.DayOfWeek);

    int leisureHours =
        today is DayOfWeek.Saturday || today is DayOfWeek.Sunday
        ? 16 : 5;
    return leisureHours;
}

3、WhenAll与WhenAny:高效等待

(1)WhenAll

​ 可以通过使用 Task 类的方法改进上述代码末尾的一系列 await 语句。 其中一个 API 是 WhenAll,它将返回一个其参数列表中的所有任务都已完成时才完成的 Task,如以下代码中所示:

await Task.WhenAll(eggsTask, baconTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Bacon is ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");
(2)WhenAny

​ 另一个选项是使用 WhenAny,它返回在其任何参数完成时完成的一个 Task<Task> 。 你可以等待返回的任务,了解它已经完成了。 以下代码展示了可以如何使用WhenAny等待第一个任务完成,然后再处理其结果。 处理已完成任务的结果之后,可以从传递给 WhenAny 的任务列表中删除此已完成的任务。

var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
    Task finishedTask = await Task.WhenAny(breakfastTasks);
    if (finishedTask == eggsTask)
    {
        Console.WriteLine("Eggs are ready");
    }
    else if (finishedTask == baconTask)
    {
        Console.WriteLine("Bacon is ready");
    }
    else if (finishedTask == toastTask)
    {
        Console.WriteLine("Toast is ready");
    }
    breakfastTasks.Remove(finishedTask);
}

4、取消令牌:CancellationToken在Web API的请求应用

作者:BeckJin
链接:https://www.jianshu.com/p/f1b9960a8b39

​ 前端调用后端的接口一般是基于 Ajax 来实现,当浏览器网页被 连续 F5 刷新页面加载中被停止Ajax 请求被主动 abort 时,控制台 network 看板中会出现一些状态为 canceled 的请求,如下:

img

​ 对于这类请求,客户端虽然主动放弃了,如果服务端没有相应处理,其实接口对应的后端程序还是在不停的执行,只是这个执行结果不会被使用而已,所以这其实是非常浪费服务器资源的。

​ 实际上浏览器取消请求时,服务端会将 HttpContext.RequestAborted 中的 Token 绑定到 Action 的 CancellationToken 参数。我们只需在接口中增加参数 CancellationToken,并将其传入其他接口调用中,程序识别到令牌被取消就会自动放弃继续执行。

[HttpGet]
public async Task<string> Index(CancellationToken cancellationToken)
{
  try
  {
    await _userService.GetListAsync(cancellationToken);
    await Task.Delay(5000); // 等待期间,取消请求(Postman 即可模拟)
    await _githubService.GetHomeAsync(cancellationToken);
  }
  catch (Exception ex)
  {
    Console.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace);
  }

  return "ok";
}

5、EFCore中的Async方法

​ 异步方法大部分是定义在Microsoft.EntityFrameworkCore这个命名空间下EntityFrameworkQueryableExtensions等类中的扩展方法。

​ 常用EF Core的异步方法:SaveChanges()、SaveChangesAsync()、AddAsync()、AddRangeAsync()、AllAsync()、AnyAsync、AverageAsync、ContainsAsync、CountAsync、FirstAssync、FirstOrDefaultAsync、ForEachAsync、LongCountAsync、MaxAsync、MinAsync、SingleAsync、SingleOrDefaultAsync、SumAsync等

​ 一般只有“立即执行方法”才有对应的Async方法:lQueryable的这些异步的扩展方法都是“立即执行"方法,而GroupBy、OrderBy、Join、Where等“非立即执行"方法则没有对应的异步方法。为什么?“非立即执行"方法并没有实际执行SQL语句,并不是消耗IO的操作。

6、Web API中使用异步函数示例

仓储层

public Task<List<Project>> GetAll(CancellationToken cancellationToken)
{
	return _dbContext.Projects.ToListAsync(cancellationToken);
}

服务层

public Task<List<Project>> GetAll(CancellationToken cancellationToken)
{
	return _repository.GetAll(cancellationToken);
}

控制层

[HttpGet]
[Route("getAllProject")]
public async Task<UnifiedResponseResult<List<Project>>> GetAllProjects(CancellationToken cancellationToken)
{
	var result = await _projectService.GetAll(cancellationToken);
	return new UnifiedResponseResult<List<Project>> {
                Status = 200,
                Message = "获取成功",
                Data = result };
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未来村村长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值