C#多线程(13):任务基础①

简介: C#多线程(13):任务基础①

多线程编程模式


.NET 中,有三种异步编程模式,分别是基于任务的异步模式(TAP)、基于事件的异步模式(EAP)、异步编程模式(APM)。


  • 基于任务的异步模式 (TAP) :.NET 推荐使用的异步编程方法,该模式使用单一方法表示异步操作的开始和完成。包括我们常用的 async 、await 关键字,属于该模式的支持。
  • 基于事件的异步模式 (EAP) :是提供异步行为的基于事件的旧模型。《C#多线程(12):线程池》中提到过此模式,.NET Core 已经不支持。
  • 异步编程模型 (APM) 模式:也称为 IAsyncResult 模式,,这是使用 IAsyncResult 接口提供异步行为的旧模型。.NET Core 也不支持,请参考 《C#多线程(12):线程池》


前面,我们学习了三部分的内容:

  • 线程基础:如何创建线程、获取线程信息以及等待线程完成任务;
  • 线程同步:探究各种方式实现进程和线程同步,以及线程等待;
  • 线程池:线程池的优点和使用方法,基于任务的操作;

这篇开始探究任务和异步,而任务和异步是十分复杂的,内容错综复杂,笔者可能讲不好。。。


探究优点

在前面中,学习多线程(线程基础和线程同步),一共写了 10 篇,写了这么多代码,我们现在来探究一下多线程编程的复杂性。


  1. 传递数据和返回结果

传递数据倒是没啥问题,只是难以获取到线程的返回值,处理线程的异常也需要技巧。

  1. 监控线程的状态

新建新的线程后,如果需要确定新线程在何时完成,需要自旋或阻塞等方式等待。

  1. 线程安全

设计时要考虑如果避免死锁、合理使用各种同步锁,要考虑原子操作,同步信号的处理需要技巧。

  1. 性能

玩多线程,最大需求就是提升性能,但是多线程中有很多坑,使用不当反而影响性能。

[以上总结可参考《C# 7.0本质论》19.3节,《C# 7.0核心技术指南》14.3 节]

我们通过使用线程池,可以解决上面的部分问题,但是还有更加好的选择,就是 Task(任务)。另外 Task 也是异步编程的基础类型,后面很多内容要围绕 Task 展开。


原理的东西,还是多参考微软官方文档和书籍,笔者讲得不一定准确,而且不会深入说明这些。


任务操作


任务(Task)实在太多 API 了,也有各种骚操作,要讲清楚实在不容易,我们要慢慢来,一点点进步,一点点深入,多写代码测试。

下面与笔者一起,一步步熟悉、摸索 Task 的 API。


两种创建任务的方式

通过其构造函数创建一个任务,其构造函数定义为:

public Task (Action action);


其示例如下:

class Program
    {
        static void Main()
        {
            // 定义两个任务
            Task task1 = new Task(()=> 
            {
                Console.WriteLine("① 开始执行");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("① 执行中");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("① 执行即将结束");
            });
            Task task2 = new Task(MyTask);
            // 开始任务
            task1.Start();
            task2.Start();
            Console.ReadKey();
        }
        private static void MyTask()
        {
            Console.WriteLine("② 开始执行");
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine("② 执行中");
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine("② 执行即将结束");
        }
    }


.Start() 方法用于启动一个任务。微软文档解释:启动 Task,并将它安排到当前的 TaskScheduler 中执行。

TaskScheduler 这个东西,我们后面讲,别急。


另一种方式则使用 Task.Factory,此属性用于创建和配置 TaskTask<TResult> 实例的工厂方法。


使用https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--可以添加任务。


当需要对长时间运行、计算限制的任务(计算密集型)进行精细控制时才使用 StartNew() 方法;

官方推荐使用 Task.Run 方法启动计算限制任务。

Task.Factory.StartNew() 可以实现比 Task.Run() 更细粒度的控制。


Task.Factory.StartNew() 的重载方法是真的多,你可以参考: https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--


这里我们使用两个重载方法编写示例:

public Task StartNew(Action action);
public Task StartNew(Action action, TaskCreationOptions creationOptions);


代码示例如下:

class Program
    {
        static void Main()
        {
            // 重载方法 1
            Task.Factory.StartNew(() =>
            {
                Console.WriteLine("① 开始执行");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("① 执行中");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("① 执行即将结束");
            });
            // 重载方法 1
            Task.Factory.StartNew(MyTask);
            // 重载方法 2
            Task.Factory.StartNew(() =>
            {
                Console.WriteLine("① 开始执行");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("① 执行中");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("① 执行即将结束");
            },TaskCreationOptions.LongRunning);
            Console.ReadKey();
        }
        // public delegate void TimerCallback(object? state);
        private static void MyTask()
        {
            Console.WriteLine("② 开始执行");
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine("② 执行中");
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine("② 执行即将结束");
        }
    }


通过 Task.Factory.StartNew() 方法添加的任务,会进入线程池任务队列然后自动执行,不需要手动启动。

TaskCreationOptions.LongRunning 是控制任务创建特性的枚举,后面讲。


Task.Run() 创建任务

Task.Run() 创建任务,跟 Task.Factory.StartNew() 差不多,当然 Task.Run() 还有很多重载方法和骚操作,我们后面再来学。


Task.Run() 创建任务示例代码如下:

static void Main()
        {
            Task.Run(() =>
            {
                Console.WriteLine("① 开始执行");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("① 执行中");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("① 执行即将结束");
            });
            Console.ReadKey();
        }


取消任务

取消任务,《C#多线程(12):线程池》 中说过一次,不过控制太自由,全靠任务本身自觉判断是否取消。


这里我们通过 Task 来实现任务的取消,其取消是实时的、自动的,并且不需要手工控制。


其构造函数如下:

public Task StartNew(Action action, CancellationToken cancellationToken);


代码示例如下:

按下回车键的时候记得切换字母模式。

class Program
    {
        static void Main()
        {
            Console.WriteLine("任务开始启动,按下任意键,取消执行任务");
            CancellationTokenSource cts = new CancellationTokenSource();
            Task.Factory.StartNew(MyTask, cts.Token);
            Console.ReadKey();
            cts.Cancel();       // 取消任务
            Console.ReadKey();
        }
        // public delegate void TimerCallback(object? state);
        private static void MyTask()
        {
            Console.WriteLine(" 开始执行");
            int i = 0;
            while (true)
            {
                Console.WriteLine($" 第{i}次任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("     执行中");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("     执行结束");
                i++;
            }
        }
    }


父子任务

前面创建任务的时候,我们碰到了 TaskCreationOptions.LongRunning 这个枚举类型,这个枚举用于控制任务的创建以及设定任务的行为。


其枚举如下:

枚举 说明
AttachedToParent 4 指定将任务附加到任务层次结构中的某个父级。
DenyChildAttach 8 指定任何尝试作为附加的子任务执行的子任务都无法附加到父任务,会改成作为分离的子任务执行。
HideScheduler 16 防止环境计划程序被视为已创建任务的当前计划程序。
LongRunning 2 指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。
None 0 指定应使用默认行为。
PreferFairness 1 提示 TaskScheduler 以一种尽可能公平的方式安排任务。
RunContinuationsAsynchronously 64 强制异步执行添加到当前任务的延续任务。


这个枚举在 TaskFactoryTaskFactory<TResult>TaskTask<TResult>

StartNew()FromAsync()TaskCompletionSource<TResult> 等地方可以使用到。


笔者在这里犯了一个错误,在写下一篇文章时重新测试发现的。文档的中文翻译实在太可怕了。。。

子任务使用了 TaskCreationOptions.AttachedToParent ,并不是指父任务要等待子任务完成后,父任务才能继续完往下执行;而是指父任务如果先执行完毕,那么必须等待子任务完成后,父任务才算完成。


这里来探究 TaskCreationOptions.AttachedToParent的使用。代码示例如下:

// 父子任务
            Task task = new Task(() =>
            {
                // TaskCreationOptions.AttachedToParent
                // 将此任务附加到父任务中
                // 父任务需要等待所有子任务完成后,才能算完成
                Task task1 = new Task(() =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    for (int i = 0; i < 5; i++)
                    {
                        Console.WriteLine("     内层任务1");
                        Thread.Sleep(TimeSpan.FromSeconds(0.5));
                    }
                }, TaskCreationOptions.AttachedToParent);
                task1.Start();
                Console.WriteLine("最外层任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
            });
            task.Start();
            task.Wait();
            Console.ReadKey();


TaskCreationOptions.DenyChildAttach 则不允许其它任务附加到外层任务中。

static void Main()
        {
            // 不允许出现父子任务
            Task task = new Task(() =>
            {
                Task task1 = new Task(() =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    for (int i = 0; i < 5; i++)
                    {
                        Console.WriteLine("  内层任务1");
                        Thread.Sleep(TimeSpan.FromSeconds(0.5));
                    }
                }, TaskCreationOptions.AttachedToParent);
                task1.Start();
                Console.WriteLine("最外层任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }, TaskCreationOptions.DenyChildAttach); // 不收儿子
            task.Start();
            task.Wait();
            Console.ReadKey();
        }


然后,这里也学习了一个新的 Task 方法:Wait() 等待 Task 完成执行过程。Wait() 也可以设置超时时间。


如果父任务是通过调用 Task.Run 方法而创建的,则可以隐式阻止子任务附加到其中。

关于附加的子任务,请参考:https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/attached-and-detached-child-tasks?view=netcore-3.1


任务返回结果以及异步获取返回结果

要获取任务返回结果,要使用泛型类或方法创建任务,例如 Task<Tresult>Task.Factory.StartNew<TResult>()Task.Run<TResult>

通过 其泛型的 的 Result 属性,可以获得返回结果。


异步获取任务执行结果:

class Program
    {
        static void Main()
        {
            // *******************************
            Task<int> task = new Task<int>(() =>
            {
                return 666;
            });
            // 执行
            task.Start();
            // 获取结果,属于异步
            int number = task.Result;
            // *******************************
            task = Task.Factory.StartNew<int>(() =>
            {
                return 666;
            });
            // 也可以异步获取结果
            number = task.Result;
            // *******************************
            task = Task.Run<int>(() =>
              {
                  return 666;
              });
            // 也可以异步获取结果
            number = task.Result;
            Console.ReadKey();
        }
    }


如果要同步的话,可以改成:

int number = Task.Factory.StartNew<int>(() =>
            {
                return 666;
            }).Result;


捕获任务异常

进行中的任务发生了异常,不会直接抛出来阻止主线程执行,当获取任务处理结果或者等待任务完成时,异常会重新抛出。


示例如下:

static void Main()
        {
            // *******************************
            Task<int> task = new Task<int>(() =>
            {
                throw new Exception("反正就想弹出一个异常");
            });
            // 执行
            task.Start();
            Console.WriteLine("任务中的异常不会直接传播到主线程");
            Thread.Sleep(TimeSpan.FromSeconds(1));
            // 当任务发生异常,获取结果时会弹出
            int number = task.Result;
            // task.Wait(); 等待任务时,如果发生异常,也会弹出
            Console.ReadKey();
        }


乱抛出异常不是很好的行为噢~可以改成如下:

static void Main()
        {
            Task<Program> task = new Task<Program>(() =>
            {
                try
                {
                    throw new Exception("反正就想弹出一个异常");
                    return new Program();
                }
                catch
                {
                    return null;
                }
            });
            task.Start();
            var result = task.Result;
            if (result is null)
                Console.WriteLine("任务执行失败");
            else Console.WriteLine("任务执行成功");
            Console.ReadKey();
        }


全局捕获任务异常

TaskScheduler.UnobservedTaskException 是一个事件,其委托定义如下:


public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);


下面是一个示例:

请发布程序后,打开目录执行程序。

class Program
    {
        static void Main()
        {
            TaskScheduler.UnobservedTaskException += MyTaskException;
            Task.Factory.StartNew(() =>
             {
                 throw new ArgumentNullException();
             });
            Thread.Sleep(100);
            GC.Collect();
            GC.WaitForPendingFinalizers();
            Console.WriteLine("Done");
            Console.ReadKey();
        }
        public static void MyTaskException(object sender, UnobservedTaskExceptionEventArgs eventArgs)
        {
            // eventArgs.SetObserved();
            ((AggregateException)eventArgs.Exception).Handle(ex =>
            {
                Console.WriteLine("Exception type: {0}", ex.GetType());
                return true;
            });
        }
    }

TaskScheduler.UnobservedTaskException 到底怎么用,笔者不太清楚。而且效果难以观察。

请参考:

https://stackoverflow.com/search?q=TaskScheduler.UnobservedTaskException


相关文章
|
3月前
|
数据采集 XML JavaScript
C# 中 ScrapySharp 的多线程下载策略
C# 中 ScrapySharp 的多线程下载策略
|
16天前
|
C# UED SEO
C# 异步方法async / await任务超时处理
通过使用 `Task.WhenAny`和 `Task.Delay`方法,您可以在C#中有效地实现异步任务的超时处理机制。这种方法允许您在指定时间内等待任务完成,并在任务超时时采取适当的措施,如抛出异常或执行备用操作。希望本文提供的详细解释和代码示例能帮助您在实际项目中更好地处理异步任务超时问题,提升应用程序的可靠性和用户体验。
44 3
|
1月前
|
缓存 负载均衡 Java
c++写高性能的任务流线程池(万字详解!)
本文介绍了一种高性能的任务流线程池设计,涵盖多种优化机制。首先介绍了Work Steal机制,通过任务偷窃提高资源利用率。接着讨论了优先级任务,使不同优先级的任务得到合理调度。然后提出了缓存机制,通过环形缓存队列提升程序负载能力。Local Thread机制则通过预先创建线程减少创建和销毁线程的开销。Lock Free机制进一步减少了锁的竞争。容量动态调整机制根据任务负载动态调整线程数量。批量处理机制提高了任务处理效率。此外,还介绍了负载均衡、避免等待、预测优化、减少复制等策略。最后,任务组的设计便于管理和复用多任务。整体设计旨在提升线程池的性能和稳定性。
79 5
|
3月前
|
前端开发 JavaScript 大数据
React与Web Workers:开启前端多线程时代的钥匙——深入探索计算密集型任务的优化策略与最佳实践
【8月更文挑战第31天】随着Web应用复杂性的提升,单线程JavaScript已难以胜任高计算量任务。Web Workers通过多线程编程解决了这一问题,使耗时任务独立运行而不阻塞主线程。结合React的组件化与虚拟DOM优势,可将大数据处理等任务交由Web Workers完成,确保UI流畅。最佳实践包括定义清晰接口、加强错误处理及合理评估任务特性。这一结合不仅提升了用户体验,更为前端开发带来多线程时代的全新可能。
81 1
|
3月前
|
存储 监控 Java
|
2月前
|
安全 数据库连接 API
C#一分钟浅谈:多线程编程入门
在现代软件开发中,多线程编程对于提升程序响应性和执行效率至关重要。本文从基础概念入手,详细探讨了C#中的多线程技术,包括线程创建、管理及常见问题的解决策略,如线程安全、死锁和资源泄露等,并通过具体示例帮助读者理解和应用这些技巧,适合初学者快速掌握C#多线程编程。
82 0
|
3月前
|
安全 C# 开发者
【C# 多线程编程陷阱揭秘】:小心!那些让你的程序瞬间崩溃的多线程数据同步异常问题,看完这篇你就能轻松应对!
【8月更文挑战第18天】多线程编程对现代软件开发至关重要,特别是在追求高性能和响应性方面。然而,它也带来了数据同步异常等挑战。本文通过一个简单的计数器示例展示了当多个线程无序地访问共享资源时可能出现的问题,并介绍了如何使用 `lock` 语句来确保线程安全。此外,还提到了其他同步工具如 `Monitor` 和 `Semaphore`,帮助开发者实现更高效的数据同步策略,以达到既保证数据一致性又维持良好性能的目标。
50 0
|
4月前
|
Java Linux
Java演进问题之1:1线程模型对于I/O密集型任务如何解决
Java演进问题之1:1线程模型对于I/O密集型任务如何解决
|
3月前
|
Cloud Native Java 调度
项目环境测试问题之线程同步器会造成执行完任务的worker等待的情况如何解决
项目环境测试问题之线程同步器会造成执行完任务的worker等待的情况如何解决