task笔记

1. Task 任务创建:3种核心写法(冷启动/热启动)

核心区分 :冷启动(先实例化后启动)、热启动(创建即执行)StartNew 和Run,工业开发中优先使用热启动 ,冷启动仅在需要「延迟执行任务」时使用。Task.Run 是通用首选,但工业低延迟通信场景禁止使用(基于线程池,有调度延迟,之前讲过的PLC/数控通信需用专用线程)。

csharp 复制代码
# 1. 冷启动:new Task() + Start(),需手动调用Start才执行
Task task1 = new Task(() => { Console.WriteLine("冷启动任务执行"); });
task1.Start(); // 关键:未调用Start时,任务状态为Created

# 2. 热启动:Task.Factory.StartNew(),创建即执行,支持指定任务选项(工业开发常用)
// 可传入TaskCreationOptions,适合长时间运行/附加子任务等场景
Task task2 = Task.Factory.StartNew(() => { Console.WriteLine("Factory热启动执行"); },
                                   TaskCreationOptions.LongRunning);

# 3. 热启动:Task.Run(),.NET 4.5+新增,简化版Factory.StartNew,推荐通用场景使用
// 底层封装了Factory.StartNew,默认使用线程池,语法更简洁
Task task3 = Task.Run(() => { Console.WriteLine("Run热启动执行"); });

2. TaskCreationOptions 任务创建选项:含义+常用项

创建任务时的可选配置,用于指定任务的调度/执行特性 ,仅Task.Factory.StartNew()/new Task()支持,Task.Run()不支持直接指定(需通过重载间接配置),标注★为常用项

常用组合示例:长时间运行的任务+强制异步延续

选项 核心含义 适用场景
PreferFairness 指示调度器尽量按「任务创建顺序」执行,避免后创建的任务先执行 对执行顺序有轻微要求的批量任务(如MES批量处理生产数据)
LongRunning 标记为长时间运行的任务 任务执行时间超过1秒(如大文件解析、远程接口调用),调度器会为其创建新线程(而非复用线程池),避免占用线程池导致其他短任务阻塞
AttachedToParent 将子任务附加到父任务,父任务需等待子任务完成才会进入完成状态 嵌套任务场景(如父任务处理订单,子任务处理订单下的多个商品)
DenyChildAttach 禁止当前任务的子任务使用AttachedToParent附加,子任务为独立任务 避免嵌套任务导致父任务阻塞,适合独立子任务场景
HideScheduler 阻止环境调度器成为当前任务的调度器,子任务使用默认线程池调度器 自定义调度器场景,通用开发几乎不用
RunContinuationsAsynchronously 强制延续任务异步执行,避免延续任务在触发任务的线程上同步执行导致阻塞 所有延续任务场景(避免主线程/任务线程被延续任务阻塞,工业开发必加)
csharp 复制代码
Task.Factory.StartNew(() => { Thread.Sleep(2000); Console.WriteLine("长时间任务执行"); },
                       TaskCreationOptions.LongRunning | TaskCreationOptions.RunContinuationsAsynchronously);

3. Task 的状态有哪些?如何快速判断任务是正常完成/异常/取消?

答案

  • 核心状态:Created、WaitingToRun、Running、RanToCompletion、Canceled、Faulted(共8种,其余为过渡状态);
  • 快捷判断:无需直接判断TaskStatus,使用内置属性更简洁:
    • IsCompletedSuccessfully:正常完成(RanToCompletion);
    • IsCanceled:已取消;
    • IsFaulted:异常终止;
    • IsCompleted:任务已完成(包含以上三种状态)。
      通过task.Status获取,标注★为开发中高频关注状态 ,状态流转:CreatedWaitingToRunRunningRanToCompletion/Canceled/Faulted

状态判断快捷方法(比直接判断Status更简洁):

csharp 复制代码
task.IsCompleted; // 任务是否完成(RanToCompletion/Canceled/Faulted均返回true)
task.IsCompletedSuccessfully; // 任务是否正常完成(仅RanToCompletion返回true)
task.IsCanceled; // 任务是否取消
task.IsFaulted; // 任务是否异常
状态 核心含义 触发场景
Created 任务已创建,未执行 new Task()后未调用Start()
WaitingForActivation 任务等待激活 由系统创建的任务(如延续任务、async-await生成的任务),无需手动Start
WaitingToRun 任务已入队,等待调度器分配线程 热启动任务创建后,线程池暂无空闲线程
Running 任务正在执行中 调度器已为任务分配线程,执行业务代码
WaitingForChildrenToComplete 父任务执行完成,等待附加的子任务完成 父任务使用AttachedToParent,子任务未完成
RanToCompletion 任务正常完成 业务代码执行完毕,无异常、未取消
Canceled 任务已取消 触发CancellationToken取消信号,且任务检测到取消并正常退出
Faulted 任务异常终止 任务执行过程中抛出未处理异常

4. 泛型任务 Task:带返回值的任务

核心 :只有泛型Task才有返回值,通过task.Result获取,非泛型Task无Result属性 ,继承关系:Task<TResult> : Task
注意task.Result阻塞式 的,与task.Wait()等效,获取结果前会一直阻塞当前线程(主线程/其他任务线程)。

csharp 复制代码
// 泛型任务创建:指定返回值类型,lambda表达式返回对应类型
Task<int> task = Task.Run(() =>
{
    Thread.Sleep(500);
    return 10 + 20; // 返回int类型
});
int result = task.Result; // 阻塞当前线程,直到任务完成并获取返回值
Console.WriteLine($"任务返回值:{result}"); // 输出:30

5. Task 任务的等待:单任务+多任务等待

用于阻塞当前线程 ,等待任务完成后再继续执行,工业开发中常用于批量任务执行后统一处理结果(如MES批量采集设备数据后汇总)。单任务等待:Wait() 重载。多任务等待:WaitAll/WaitAny(静态方法)

1. 单任务等待:Wait() 重载

csharp 复制代码
Task task = Task.Run(() => { Thread.Sleep(1000); });
task.Wait(); // 无参数:无限阻塞,直到任务完成(正常/取消/异常)
bool isCompleted = task.Wait(5000); // 带超时时间(毫秒):返回bool,true=超时前完成,false=超时
// 带取消令牌:等待过程中可被取消
CancellationTokenSource cts = new CancellationTokenSource();
task.Wait(cts.Token);

2. 多任务等待:WaitAll/WaitAny(静态方法)

csharp 复制代码
Task task1 = Task.Run(() => { Thread.Sleep(500); });
Task task2 = Task.Run(() => { Thread.Sleep(1000); });
Task task3 = Task.Run(() => { Thread.Sleep(1500); });
Task[] tasks = new[] { task1, task2, task3 };

Task.WaitAll(tasks); // 等待**所有任务**完成(耗时=最长任务时间,此处1500ms)
Task.WaitAny(tasks); // 等待**任意一个任务**完成(耗时=最短任务时间,此处500ms)
// 多任务等待也支持超时
bool allCompleted = Task.WaitAll(tasks, 2000); // 2秒内是否所有任务完成

6. task.AsyncState:获取任务的异步状态对象

核心作用 :创建任务时传入自定义状态对象 ,通过AsyncState获取,用于向任务传递额外数据 (如设备ID、工单实体),无需通过lambda闭包(闭包在循环中易出坑)。
工业开发要点 :批量处理设备/工单数据时,用AsyncState传递对象(如设备ID、工单实体),避免闭包导致的变量引用问题

经典用法:循环创建任务,传递循环变量(避免闭包陷阱)

csharp 复制代码
    class Entity
    {
        public int Result { get; set; }
        public int Number { get; }
        public Entity(int number)
        {
            Number = number;
        }
    }
    internal class Program
    {
        static void Main(string[] args)
        {
            Task.Run(() =>
            {

            });

            Console.WriteLine("主线程开始");
            Stopwatch stopwatch = Stopwatch.StartNew();
            List<Entity> collection = new List<Entity>();
            int max = 100;
            for (int i = 1; i <= max; i++)
            {
                collection.Add(new Entity(i));
            }

            List<Task> tasks = new List<Task>();
            foreach (Entity item in collection)
            {
                Task task = Task.Factory.StartNew(param =>
                {
                    if(param is Entity entity)
                    {
                        Thread.Sleep(50);//模拟耗时
                        entity.Result = entity.Number * entity.Number;
                    }
                },item);
                tasks.Add(task);
            }

            Task.WaitAll(tasks.ToArray());//等待100个任务完成
            int sum = 0;
            foreach (Task task in tasks)
            {
                var entity = task.AsyncState as Entity;//获取传入的参数对象
                sum += entity.Result;
            }
            Console.WriteLine($"1-{max}的平方和等于{sum}");
            Console.WriteLine($"总耗时:{stopwatch.ElapsedMilliseconds}毫秒");
            Console.WriteLine("主线程结束");
            Console.ReadKey();

//主线程开始
//1 - 100的平方和等于338350
//总耗时:436毫秒
//主线程结束


        }
    }

7.Task 任务的取消:CancellationTokenSource(CTS)

核心 :通过「取消令牌」实现任务的优雅取消 (非强制终止线程),工业开发中用于手动终止耗时任务 (如PLC通信超时、远程接口调用中断),禁止使用Abort()终止线程 (会导致资源泄漏)。

CancellationTokenSource, CancelAfter, ThrowIfCancellationRequested, Cancel,

取消回调,取消令牌可多任务共享

完整实现步骤(3步):

csharp 复制代码
# 1. 创建取消令牌源,可选设置「超时自动取消」
CancellationTokenSource cts = new CancellationTokenSource();
// 可选:设置5秒后自动取消(无需手动调用Cancel)
// cts.CancelAfter(5000);

# 2. 将令牌(cts.Token)传入任务,任务中**主动检测取消信号**
Task task = Task.Run(() =>
{
    while (true)
    {
        // 关键1:检测取消信号,若已取消,抛出OperationCanceledException
        cts.Token.ThrowIfCancellationRequested();
        // 模拟业务执行
        Console.WriteLine("任务执行中...");
        Thread.Sleep(200);
    }
}, cts.Token); // 关键2:将令牌传入任务

# 3. 手动发送取消信号(外部触发,如按钮点击、超时)
Thread.Sleep(1000);
cts.Cancel(); // 发送取消信号

进阶:取消回调(任务取消时执行自定义逻辑)

csharp 复制代码
CancellationTokenSource cts = new CancellationTokenSource();
// 注册取消回调:令牌触发取消时,执行指定方法
cts.Token.Register(() =>
{
    Console.WriteLine("任务已被取消,执行资源释放逻辑");
    // 工业开发中:释放通信连接、关闭文件流、释放硬件资源等
});

Task task = Task.Run(() =>
{
    while (!cts.Token.IsCancellationRequested) // 另一种检测取消的方式
    {
        Console.WriteLine("任务执行中...");
        Thread.Sleep(200);
    }
    // 手动抛出取消异常,让任务状态变为Canceled(否则为RanToCompletion)
    cts.Token.ThrowIfCancellationRequested();
}, cts.Token);

Thread.Sleep(1000);
cts.Cancel();

关键要点

  1. 任务中必须主动检测取消信号ThrowIfCancellationRequested()/IsCancellationRequested),否则取消信号无效;
  2. 检测到取消后,必须抛出OperationCanceledException ,任务状态才会变为Canceled,否则为RanToCompletion
  3. 取消令牌可多任务共享 ,一个CTS的Cancel()可取消所有传入该令牌的任务。

8. Task 任务的异常捕获:3种核心方式?Task 的异常为什么是AggregateException?如何捕获?

Task的异常为包装异常(AggregateException) ,因为单个任务可能抛出多个异常(如子任务异常),工业开发中必须捕获所有任务异常 ,避免进程崩溃。

方式1:try-catch包裹Wait()/Result,捕获AggregateException并遍历InnerExceptions;

方式2:延续任务ContinueWith结合TaskContinuationOptionsOnlyOnFaulted,通过t.Exception获取;

方法3:async-await会自动解包AggregateException,直接捕获原始异常,无需遍历InnerExceptions,语法和同步代码一致。

方式1:Wait()/Result 捕获(阻塞场景,try-catch包裹)

核心 :非异步场景(如主线程等待任务),用try-catch捕获AggregateException,通过e.InnerExceptions获取所有原始异常。

csharp 复制代码
Task task = Task.Run(() =>
{
    throw new DivideByZeroException("除数不能为0"); // 任务抛出异常
});
try
{
    task.Wait(); // 或 task.Result(泛型任务)
}
catch (AggregateException ex)
{
    // 遍历所有原始异常(工业开发中需逐个处理/记录)
    foreach (var innerEx in ex.InnerExceptions)
    {
        Console.WriteLine($"任务异常:{innerEx.Message}"); // 输出:除数不能为0
    }
}

方式2:延续任务捕获(非阻塞场景,推荐)

通过ContinueWith捕获前驱任务的异常,结合TaskContinuationOptions.OnlyOnFaulted仅在前驱任务异常时执行。

csharp 复制代码
Task task = Task.Run(() =>
{
    throw new Exception("前驱任务抛出异常");
});
// 延续任务:仅当前驱任务异常时执行,t为前驱任务对象
task.ContinueWith(t =>
{
    // t.Exception为AggregateException,获取原始异常
    foreach (var ex in t.Exception.InnerExceptions)
    {
        Console.WriteLine($"延续任务捕获异常:{ex.Message}");
    }
}, TaskContinuationOptions.OnlyOnFaulted);

方式3:async-await 捕获(.NET 4.5+,最简洁,推荐开发首选)

核心 :async-await会自动解包AggregateException,直接捕获原始异常,无需遍历InnerExceptions,语法和同步代码一致。

csharp 复制代码
static async Task Main(string[] args) // 主线程标记为async
{
    try
    {
        await Task.Run(() =>
        {
            throw new DivideByZeroException("除数不能为0");
        });
    }
    catch (DivideByZeroException ex) // 直接捕获原始异常
    {
        Console.WriteLine($"Await捕获异常:{ex.Message}");
    }
}

工业开发要点 :批量任务异常需遍历所有任务的Exception属性,示例:

csharp 复制代码
Task[] tasks = new[]
{
    Task.Run(() => { throw new Exception("任务1异常"); }),
    Task.Run(() => { throw new Exception("任务2异常"); })
};
try
{
    Task.WaitAll(tasks);
}
catch (AggregateException)
{
    // 遍历所有任务,捕获异常的任务
    foreach (var task in tasks)
    {
        if (task.IsFaulted)
        {
            foreach (var ex in task.Exception.InnerExceptions)
            {
                Console.WriteLine($"批量任务异常:{ex.Message}");
            }
        }
    }
}

9. Task 延续任务:ContinueWith(任务完成后执行后续逻辑)

核心 :前驱任务完成(正常/异常/取消)后,自动执行延续任务,支持指定执行条件 (仅正常/仅异常/仅取消时执行),工业开发中用于任务完成后的资源释放、结果处理

1. 基础用法:单个前驱任务的延续

csharp 复制代码
Task<int> preTask = Task.Run(() => { return 10 + 20; });
// 延续任务:preTask完成后执行,t为前驱任务对象,可获取前驱任务的结果/状态
Task continueTask = preTask.ContinueWith(t =>
{
    int result = t.Result; // 获取前驱任务返回值
    Console.WriteLine($"前驱任务结果:{result},延续任务执行");
});

2. 关键:TaskContinuationOptions 延续选项(常用★)

用于指定延续任务的执行条件 ,仅当前驱任务满足条件时,延续任务才会执行,避免在延续任务中手动判断状态。默认值是None(这是枚举的默认常量,值为 0)特点是无任何执行条件限制------ 无论前驱任务是正常完成、异常终止还是被取消,延续任务都会被调度执行,是最宽松的延续规则。

OnlyOnRanToCompletion, OnlyOnFaulted, OnlyOnCanceled

选项 执行条件 适用场景
OnlyOnRanToCompletion 前驱任务正常完成(RanToCompletion) 前驱任务正常执行后,处理结果(如MES数据入库)
OnlyOnFaulted 前驱任务异常(Faulted) 捕获前驱任务异常,记录日志/执行容错逻辑
OnlyOnCanceled 前驱任务取消(Canceled) 任务取消后,释放资源/提示用户
NotOnRanToCompletion 前驱任务未正常完成(异常/取消) 非成功场景的统一处理
NotOnFaulted 前驱任务未异常(正常/取消) 无异常场景的处理
NotOnCanceled 前驱任务未取消(正常/异常) 未取消场景的处理

3. 多前驱任务的延续:WhenAll/WhenAny

多个任务完成后执行延续任务,替代Task.WaitAll(非阻塞)。

csharp 复制代码
Task task1 = Task.Run(() => { Thread.Sleep(500); });
Task task2 = Task.Run(() => { Thread.Sleep(1000); });
// 方式1:所有任务完成后执行延续任务
Task.WhenAll(task1, task2).ContinueWith(t =>
{
    Console.WriteLine("所有任务完成,执行延续逻辑");
}, TaskContinuationOptions.OnlyOnRanToCompletion);

// 方式2:任意一个任务完成后执行延续任务
Task.WhenAny(task1, task2).ContinueWith(t =>
{
    Console.WriteLine("有任务完成,执行延续逻辑");
});

4. 延续任务的核心注意事项

  1. 延续任务默认在前驱任务的线程 执行,若需异步执行,创建前驱任务时加TaskCreationOptions.RunContinuationsAsynchronously
  2. 延续任务可嵌套,但避免多层嵌套(可读性差,建议用async-await替代);
  3. 多延续任务:一个前驱任务可绑定多个延续任务(不同条件),彼此独立执行。

10. Task 并行任务创建:循环批量创建(工业开发高频)

批量处理数据时(如MES批量采集多台设备、批量处理工单),循环创建任务并统一等待,重点避免闭包陷阱 (用AsyncState或循环内临时变量解决)。

工业标准写法(用AsyncState传递参数,无闭包陷阱)

csharp 复制代码
// 模拟工业场景:批量处理10台设备数据
List<Device> devices = Enumerable.Range(1, 10).Select(i => new Device { DeviceId = i, Number = i * 10 }).ToList();
List<Task> tasks = new List<Task>();

foreach (var dev in devices)
{
    // 将设备实体作为AsyncState传入,避免闭包陷阱
    Task task = Task.Factory.StartNew(state =>
    {
        Device device = state as Device;
        if (device == null) return;
        // 模拟耗时操作:PLC/设备数据采集
        Thread.Sleep(50);
        device.Result = device.Number * 2; // 处理结果
        Console.WriteLine($"设备{device.DeviceId}处理完成,结果:{device.Result}");
    }, dev);
    tasks.Add(task);
}

// 等待所有并行任务完成,再统一处理结果
Task.WaitAll(tasks.ToArray());
// 批量获取处理结果
var completedDevices = devices.Where(d => d.Result != 0).ToList();
Console.WriteLine($"所有设备处理完成,共{completedDevices.Count}台");

// 设备实体类
public class Device
{
    public int DeviceId { get; set; } // 设备ID
    public int Number { get; set; } // 原始数据
    public int Result { get; set; } // 处理结果
}

11. Task 的3种创建方式有什么区别?冷启动和热启动的适用场景?【必背】

答案

  • 方式1:new Task() + Start():冷启动,任务创建后需手动Start才执行,状态初始为Created,适用于需要延迟执行的场景;
  • 方式2:Task.Factory.StartNew():热启动,创建即执行,支持指定TaskCreationOptions,适用于需要自定义任务特性(如长时间运行、附加子任务)的场景;
  • 方式3:Task.Run():.NET4.5+新增,热启动,底层封装Factory.StartNew,语法简洁,通用场景首选,但不支持直接指定TaskCreationOptions;
  • 核心区别:冷启动支持延迟执行,热启动创建即入队等待调度。

12. TaskCreationOptions.LongRunning 的作用是什么?为什么不建议随便用?

答案

  • 作用:标记任务为长时间运行,调度器会为其创建新的独立线程,而非复用线程池线程;
  • 原因:线程池的核心是「线程复用」,减少线程创建/销毁的开销,若随便用LongRunning,会创建大量独立线程,导致系统线程切换频繁,性能下降;
  • 适用场景:任务执行时间超过1秒(如大文件解析、远程接口调用),避免占用线程池导致短任务阻塞。

13 . Task.WaitAll 和 Task.WhenAll 的区别是什么?

答案:(高频考点,区分阻塞/非阻塞)

  • Task.WaitAll阻塞式,静态方法,调用后会阻塞当前线程,直到所有任务完成,返回void;
  • Task.WhenAll非阻塞式 ,静态方法,调用后返回一个新的Task,当前线程继续执行,新Task在所有任务完成后才完成,支持async-await,推荐开发使用;
  • 示例:await Task.WhenAll(t1, t2)(非阻塞),替代Task.WaitAll(t1, t2)(阻塞)。

14. 循环创建Task时,为什么会出现"变量值重复"的问题?如何解决?(闭包陷阱,高频)

答案

  • 原因:C#的for循环变量是按引用传递的,lambda表达式闭包捕获的是变量的引用,而非当前值,循环结束后变量指向最后一个值,导致所有任务获取到相同的变量值;
  • 3种解决方法:
    1. 循环内创建临时变量,将循环变量赋值给临时变量,闭包捕获临时变量;
    2. 使用Task.Factory.StartNew的第二个参数,将循环变量作为AsyncState传入,任务内通过state获取(工业开发首选,无闭包);
    3. 使用foreach循环(.NET 5+),foreach的循环变量默认按值传递,无闭包陷阱。

15. Task.Run 和 async-await 的关系是什么?async-await 是开启新线程吗?(高频考点)

答案

  • 关系:Task.Run用于开启后台线程执行耗时操作 ,async-await是异步编程语法糖,用于简化Task的延续任务,无需手动写ContinueWith;
  • 核心:async-await 本身不会开启新线程 ,仅当方法内调用了Task.Run/Task.Factory.StartNew等开启线程的方法时,才会在新线程执行,否则在当前线程执行(如异步IO操作)。

16. 如何优雅取消Task?为什么禁止使用Thread.Abort()?

答案

  • 优雅取消方式:使用CancellationTokenSource(CTS),步骤为:创建CTS→将cts.Token传入Task→任务内主动检测取消信号(ThrowIfCancellationRequested())→外部调用cts.Cancel()发送取消信号;
  • 禁止Thread.Abort()的原因:Abort()强制终止线程 ,线程执行到任意位置都会被中断,导致资源泄漏 (如未释放的文件流、通信连接、硬件资源),甚至引发进程崩溃,而CTS是优雅取消,任务主动检测并退出,可执行资源释放逻辑。

17. TaskCreationOptions.AttachedToParent 的作用是什么?父任务和子任务的状态关系是什么?

答案

  • 作用:将子任务附加到父任务,形成任务层级,父任务会等待所有附加的子任务完成后,才会进入完成状态;
  • 状态关系:父任务执行完毕后,状态变为WaitingForChildrenToComplete,直到所有子任务完成(正常/异常/取消),父任务才会进入对应的完成状态(RanToCompletion/Canceled/Faulted);
  • 若子任务未使用AttachedToParent,父任务执行完毕后直接进入完成状态,无需等待子任务。

18. Task 的 Result 属性和 await 有什么区别?

答案:(工业开发易踩坑,区分阻塞/非阻塞)

  • task.Result阻塞式,调用后会阻塞当前线程,直到任务完成,若任务异常,抛出AggregateException;
  • await task非阻塞式,async-await语法,调用后当前线程会释放,继续执行其他逻辑,任务完成后再回到该位置执行,若任务异常,自动解包并抛出原始异常;
  • 工业开发:主线程/UI线程禁止使用Result(会导致界面卡顿),优先使用await;后台业务线程可根据场景使用,但推荐await。

19. 在C#工业上位机开发中,为什么PLC/数控设备的低延迟通信禁止使用Task?(结合之前的通信知识)

答案

  • 核心原因:Task基于线程池 实现,线程池的核心是「动态调度、线程复用」,存在调度延迟(0~几十毫秒),且线程池线程可能被其他任务占用,导致通信任务等待;
  • 工业通信要求1~10ms的稳定低延迟,调度延迟会导致通信延迟突增,无法满足硬指标;
  • 补充:Task仅适用于上位机的非实时业务层 (如MES数据上报、日志异步写入、批量数据处理),现场通信层必须使用独占专用线程(无调度延迟、无GC)。

20. 工业开发中,批量采集100台PLC设备数据,如何使用Task实现?需要注意哪些点?

答案

  • 实现方式:循环创建Task,每台设备对应一个Task,使用Task.WhenAll等待所有任务完成,统一处理采集结果;
  • 核心注意事项(工业开发必守):
    1. 避免闭包陷阱:用AsyncState传递设备ID/IP等参数,而非闭包;
    2. 限制并发数:禁止100个Task同时执行(会导致网络拥塞/PLC连接拒绝),用信号量(SemaphoreSlim) 限制并发数(如同时执行10个任务);
    3. 异常处理:逐个捕获每个Task的异常,记录异常设备(避免一台设备异常导致批量采集失败);
    4. 资源释放:每个Task执行完毕后,释放PLC通信连接(避免连接泄漏);
    5. 禁止使用LongRunning:采集任务为短任务(几十毫秒),复用线程池即可,无需创建独立线程。

21. 工业上位机中,Task执行耗时操作(如远程接口调用)时,如何设置超时?

答案

  • 2种常用方式:
    1. 使用CancellationTokenSource.CancelAfter:设置超时自动取消,任务内检测取消信号,超时后抛出OperationCanceledException;
    2. 使用Task.WhenAny:将耗时任务和延迟任务组合,超时后终止等待;
  • 工业推荐方式(更灵活):
csharp 复制代码
// 模拟PLC远程调用,设置3秒超时
CancellationTokenSource cts = new CancellationTokenSource(3000); // 3秒后自动取消
try
{
    await Task.Run(() =>
    {
        // 模拟耗时操作:PLC远程通信
        while (!cts.Token.IsCancellationRequested)
        {
            Thread.Sleep(100);
        }
        cts.Token.ThrowIfCancellationRequested();
    }, cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("PLC通信超时"); // 工业开发中:记录超时日志、标记设备离线
}

三、核心要点回顾(笔记+面试题高频考点)

  1. Task创建分冷启动 (new+Start)和热启动(Factory.StartNew/Run),工业开发优先用热启动,冷启动仅用于延迟执行;
  2. 常用配置:LongRunning(长时间任务)、AttachedToParent(附加子任务)、RunContinuationsAsynchronously(异步延续);
  3. 任务状态关注RanToCompletion/Canceled/Faulted,用快捷属性(IsCompletedSuccessfully/IsCanceled/IsFaulted)判断更简洁;
  4. 多任务等待:WaitAll(阻塞)、WhenAll(非阻塞,推荐),工业开发优先用WhenAll + await
  5. 异常捕获:async-await自动解包AggregateException,是开发首选,批量任务需遍历每个Task的Exception;
  6. 任务取消:用CancellationTokenSource实现优雅取消,任务内必须主动检测取消信号,禁止使用Thread.Abort();
  7. 工业开发禁忌:低延迟通信层禁止使用Task (线程池调度延迟),仅用于非实时业务层;循环创建Task避免闭包陷阱(用AsyncState);
  8. 核心坑点:Result是阻塞式的,主线程/UI线程禁止使用,优先用await;批量任务需限制并发数(避免资源拥塞)。

async-await 是异步编程语法糖,用于简化 Task 的延续任务,无需手动写 ContinueWith; 怎么理解?

async-await的核心特性,关键抓住两句话:它是简化异步代码的「语法糖」(替代繁琐的ContinueWith),而非「创建线程的工具」(线程由await后面的任务决定)。

async-await的底层是C#编译器自动帮我们生成了ContinueWith的延续任务逻辑,让我们能用同步代码的线性写法 写异步代码,替代ContinueWith多层嵌套写法(嵌套多了就是「回调地狱」,可读性极差)。

对比案例:实现「先执行任务1→用任务1结果执行任务2→用任务2结果执行任务3」
方式1:手动写ContinueWith(原生Task,嵌套繁琐,可读性差)

多层延续任务会层层嵌套,代码向右缩进,逻辑越复杂越难维护(这就是异步编程的「回调地狱」):

csharp 复制代码
static void Main(string[] args)
{
    // 任务1:计算10+20
    Task<int> task1 = Task.Run(() => { Console.WriteLine("执行任务1"); return 10 + 20; });
    
    // 任务1完成后执行任务2:用任务1结果*2
    task1.ContinueWith(t1 =>
    {
        int res1 = t1.Result;
        Console.WriteLine($"执行任务2,入参:{res1}");
        Task<int> task2 = Task.Run(() => res1 * 2);
        
        // 任务2完成后执行任务3:用任务2结果+5
        task2.ContinueWith(t2 =>
        {
            int res2 = t2.Result;
            Console.WriteLine($"执行任务3,入参:{res2}");
            int res3 = res2 + 5;
            Console.WriteLine($"最终结果:{res3}"); // 输出65
        });
    });

    Console.ReadKey();
}
方式2:用async-await(语法糖,线性写法,和同步代码一样直观)

编译器会自动将这段代码拆解为延续任务,底层还是ContinueWith,但代码完全无嵌套,可读性拉满:

csharp 复制代码
// 主线程标记为async(.NET 4.5+支持async Main)
static async Task Main(string[] args)
{
    // 任务1:计算10+20
    Console.WriteLine("执行任务1");
    int res1 = await Task.Run(() => 10 + 20);
    
    // 任务2:用任务1结果*2(自动等待任务1完成后执行)
    Console.WriteLine($"执行任务2,入参:{res1}");
    int res2 = await Task.Run(() => res1 * 2);
    
    // 任务3:用任务2结果+5(自动等待任务2完成后执行)
    Console.WriteLine($"执行任务3,入参:{res2}");
    int res3 = res2 + 5;
    
    Console.WriteLine($"最终结果:{res3}"); // 输出65
    Console.ReadKey();
}

核心结论async-await没有改变异步的本质(还是基于Task的延续任务),只是让异步代码的写法和同步代码一致 ,彻底解决了ContinueWith的嵌套问题------这就是「语法糖」的核心价值。

二、核心重点:async-await 本身不开启新线程,线程由await 后面的任务决定

这是最容易误解的点,很多人以为async-await一用就会开新线程,其实**async-await只是「任务调度的管理者」,不是「线程的创建者」**。

场景1:await 后是「由Task.Run/Factory.StartNew创建的任务」→ 新线程执行
原因Task.Run/Factory.StartNew会从线程池 获取新线程执行任务,async-await只是等待这个"新线程任务"完成,再继续执行后续代码。
验证代码(工业场景:后台计算设备数据,不阻塞主线程):

场景2:await 后是「无新线程的任务(如已完成任务、异步IO)」→ 原线程执行

核心 :如果await的任务不需要新线程 (比如任务已完成、异步IO操作),那么整个异步方法都会在调用方的原线程 执行,全程无新线程创建

文件/网络/数据库的异步IO操作 ,底层由操作系统内核 处理,无需.NET托管线程,await时会让出当前线程 (线程去做其他事),IO完成后再回到原线程 执行后续代码,全程无新线程创建 ,且不阻塞线程

场景3:纯async方法,无await → 同步执行(无异步效果)

如果一个方法标记了async,但内部没有任何await语句 ,那么这个方法会完全同步执行 ,和普通方法没有区别,编译器会给出警告:此异步方法缺少 await 运算符,将以同步方式运行

三、补充:WinForm/WPF上位机的关键特性------await 后回到UI线程

你做工业上位机开发(WinForm/WPF)时,会遇到UI线程不能阻塞 的问题(阻塞会导致界面卡顿),async-await有一个非常友好的特性:

UI线程 调用async方法,await后会自动回到UI线程 执行后续代码,无需手动切换线程(底层由UI同步上下文SynchronizationContext实现)。

  1. await 的核心作用让出当前线程 (而非阻塞),任务完成后回到原线程(WPF) 控制台可能使用新线程(线程池里的也许和task用的同一个)来执行后续代码
相关推荐
ruxshui5 小时前
个人笔记: 星环Inceptor/hive普通分区表与范围分区表核心技术总结
hive·hadoop·笔记
慾玄5 小时前
渗透笔记总结
笔记
CS创新实验室6 小时前
关于 Moltbot 的学习总结笔记
笔记·学习·clawdbot·molbot
孞㐑¥7 小时前
算法—分治
开发语言·c++·经验分享·笔记·算法
xqqxqxxq7 小时前
《智能仿真无人机平台(多线程 V4.0)技术笔记》(集群进阶:多无人机任务分配与碰撞规避)
笔记·无人机·cocos2d
我命由我123457 小时前
Git 初始化本地仓库并推送到远程仓库解读
运维·服务器·经验分享·笔记·git·学习·学习方法
Aliex_git7 小时前
Claude Code 使用笔记(四)- GitHub Claude 审查助手
人工智能·笔记·学习·github·ai编程
暴躁小师兄数据学院8 小时前
【WEB3.0零基础转行笔记】基础知识篇—第一讲:区块链基础
笔记·web3·区块链
爱码小白8 小时前
Git学习笔记
笔记·git·学习
浅念-8 小时前
数据结构——栈和队列
c语言·数据结构·经验分享·笔记·算法