iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C#多线程系列之任务基础(一)
  • 676
分享到

C#多线程系列之任务基础(一)

2024-04-02 19:04:59 676人浏览 安东尼
摘要

目录多线程编程多线程编程模式探究优点任务操作两种创建任务的方式Task.Run() 创建任务取消任务父子任务任务返回结果以及异步获取返回结果捕获任务异常全局捕获任务异常多线程编程 多

多线程编程

多线程编程模式

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

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

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

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

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

探究优点

我们现在来探究一下多线程编程的复杂性。

  • 传递数据和返回结果

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

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

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

  • 性能

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

[以上总结可参考《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,此属性用于创建和配置 Task 和 Task<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 这个枚举类型,这个枚举用于控制任务的创建以及设定任务的行为。

其枚举如下:

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

这个枚举在 TaskFactory 和 TaskFactory<TResult> 、Task 和 Task<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

到此这篇关于C#多线程系列之任务基础(一)的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持编程网。

--结束END--

本文标题: C#多线程系列之任务基础(一)

本文链接: https://www.lsjlt.com/news/139049.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • C#多线程系列之任务基础(一)
    目录多线程编程多线程编程模式探究优点任务操作两种创建任务的方式Task.Run() 创建任务取消任务父子任务任务返回结果以及异步获取返回结果捕获任务异常全局捕获任务异常多线程编程 多...
    99+
    2024-04-02
  • C#多线程系列之任务基础(三)
    目录TaskAwaiter延续的另一种方法另一种创建任务的方法实现一个支持同步和异步任务的类型Task.FromCanceled()如何在内部取消任务Yield 关键字补充知识点Ta...
    99+
    2024-04-02
  • C#多线程系列之任务基础(二)
    目录判断任务状态再说父子任务组合任务/延续任务复杂的延续任务并行(异步)处理任务并行(同步)处理任务并行任务的 Task.WhenAny并行任务状态循环中值变化问题定时任务 Task...
    99+
    2024-04-02
  • c#多线程之线程基础
    目录一、简介二、创建线程三、暂停线程四、线程等待五、终止线程六、检测线程状态七、线程优先级八、前台线程和后台线程九、向线程传递参数十、使用C# Lock 关键字十一、使用Monito...
    99+
    2024-04-02
  • C#多线程系列之线程池
    目录线程池ThreadPool 常用属性和方法线程池说明和示例线程池线程数线程池线程数说明不支持的线程池异步委托任务取消功能计时器线程池 线程池全称为托管线程池,线程池受 .NET ...
    99+
    2024-04-02
  • C#多线程系列之线程等待
    目录前言volatile 关键字三种常用等待再说自旋和阻塞SpinWait 结构属性和方法自旋示例新的实现SpinLock 结构属性和方法示例等待性能对比前言 volatile 关键...
    99+
    2024-04-02
  • C#多线程系列之线程通知
    AutoRestEvent 类用于从一个线程向另一个线程发送通知。 微软文档是这样介绍的:表示线程同步事件在一个等待线程释放后收到信号时自动重置。 其构造函数只有一个: 构造函数里面...
    99+
    2024-04-02
  • C#多线程系列之线程完成数
    解决一个问题 假如,程序需要向一个 Web 发送 5 次请求,受网路波动影响,有一定几率请求失败。如果失败了,就需要重试。 示例代码如下: class Program ...
    99+
    2024-04-02
  • C#多线程学习之基础入门
    目录同步方式异步多线程方式异步多线程优化异步回调异步信号量异步多线程返回值异步多线程返回值回调线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进...
    99+
    2024-04-02
  • C#多线程系列之多线程锁lock和Monitor
    目录1,Locklock 原型lock 编写实例2,Monitor怎么用呢解释一下示例设置获取锁的时效1,Lock lock 用于读一个引用类型进行加锁,同一时刻内只有一个线程能够访...
    99+
    2024-04-02
  • C#多线程系列之多阶段并行线程
    前言 这一篇,我们将学习用于实现并行任务、使得多个线程有序同步完成多个阶段的任务。 应用场景主要是控制 N 个线程(可随时增加或减少执行的线程),使得多线程在能够在 M 个阶段中保持...
    99+
    2024-04-02
  • C#多线程系列之读写锁
    本篇的内容主要是介绍 ReaderWriterLockSlim 类,来实现多线程下的读写分离。 ReaderWriterLockSlim ReaderWriterLock 类:定义支...
    99+
    2024-04-02
  • C#多线程系列之手动线程通知
    区别与示例 AutoResetEvent 和 ManualResetEvent 十分相似。两者之间的区别,在于前者是自动(Auto),后者是手动(Manua)。 你可以先运行下面的示...
    99+
    2024-04-02
  • C#多线程开发实战记录之线程基础
    目录前言线程基础 1、创建线程2、暂停线程3、线程等待4、线程终止C#中的lock关键字总结前言 最近由于工作的需要,一直在使用C#的多线程进行开发,其中也遇到了很多问题,但也都解决...
    99+
    2024-04-02
  • C#多线程系列之原子操作
    目录知识点竞争条件线程同步CPU时间片和上下文切换阻塞内核模式和用户模式Interlocked类1,出现问题2,Interlocked.Increment()3,Interlocke...
    99+
    2024-04-02
  • C#多线程系列之进程同步Mutex类
    Mutex 中文为互斥,Mutex 类叫做互斥锁。它还可用于进程间同步的同步基元。 Mutex 跟 lock 相似,但是 Mutex 支持多个进程。Mutex 大约比 lock 慢 ...
    99+
    2024-04-02
  • C#多线程系列之资源池限制
    Semaphore、SemaphoreSlim 类 两者都可以限制同时访问某一资源或资源池的线程数。 这里先不扯理论,我们从案例入手,通过示例代码,慢慢深入了解。 Semaphore...
    99+
    2024-04-02
  • C#多线程系列之工作流实现
    目录前言节点ThenParallelScheduleDelay试用一下顺序节点并行任务编写工作流接口构建器工作流构建器依赖注入实现工作流解析前言 前面学习了很多多线程和任务的基础知识...
    99+
    2024-04-02
  • C#多线程之任务的用法详解
    目录一.启动任务1.使用线程池的任务2.同步任务3.使用单独线程的任务二.任务的结果————Future三.连续的任务四.任务的层次结...
    99+
    2024-04-02
  • C# 异步多线程入门基础
    目录进程、线程1. 进程 2. 线程 分时、分片同步、异步异步、多线程异步多线程效率多线程无序性扩展异步多线程版本下一篇:C# 异步多线程入门到精通之Thread篇 进程、线程 1....
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作