美文网首页
C#多线程之Task

C#多线程之Task

作者: 小羊爱学习 | 来源:发表于2024-04-10 09:47 被阅读0次

一 简介:

多线程的原理我就不讲了,做开发处处用到。C#也提供了几种多线程的使用方式,分别是Thread类、ThreadPool类、Parallel 类和Task类,这里只讲Task,其他的知道有就可以了,就像当初做iOS开发的时候,也把苹果原生的几种线程都学了,也做了博客记录,但是在实际开发当中这么多年却只用了GCD这一种方式来处理,问就是好用、性能高。就像人吃饭一样,有肉不吃干嘛非要去喝汤,对吧?

二 Task介绍

Task 是 .NET 中用于表示异步操作的类,出现在C#4.0时代,可以简单看作相当于Thead+TheadPool,其性能比直接使用Thread要好太多,它可以适用于很多种场景和功能。
比如:

  • 异步执行代码:Task 允许在单独的线程上执行代码块,从而避免阻塞主线程,提高程序的响应性和并发性。
  • 等待异步操作完成:通过 await 关键字可以等待 Task 完成,从而实现非阻塞的异步操作。
  • 并行执行多个任务:Task 可以用于并行执行多个独立的任务,从而加快任务的完成速度,提高系统的性能。
  • 处理异常:Task 提供了异常处理机制,可以在异步操作中捕获和处理异常。
  • 轻量级的线程管理:Task 使用线程池来管理线程,避免了频繁创建和销毁线程的开销,使得线程的管理更加高效。
  • 取消异步操作:Task 支持取消操作,可以通过 CancellationToken 来取消正在进行的异步操作。

总之,Task在多线程开发中起着非常重要的作用,一定要学好并用好。

三 Task开启方式

  • Task对象的Start()方法
            Console.WriteLine("当前线程1:{0}", Thread.CurrentThread.ManagedThreadId);
            Task task1 = new Task(() =>// 无返回值
            {
                Console.WriteLine("当前线程2:{0}", Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(2000);
            });
            task1.Start();
            Console.WriteLine("主线程不受影响");

            Task<string> task2 = new Task<string>(() => {//  有返回值string类型
                return "task2";
            });
            task2.Start();
            string str = task2.Result;
            Console.WriteLine(str);
  • Task的静态方法(函数)Run()
            Task.Run(() =>
            {
                Console.WriteLine("这里是子线程喽");
            });
            Task<string> task = Task.Run<string>(() =>
            {
                return"这里是子线程喽";
            });
            string str = task.Result;
            Console.WriteLine($"{str}");
  • Task工厂
            TaskFactory factory = Task.Factory;
            factory.StartNew(() =>
            {
                Console.WriteLine("你好 task");
            });

            Task<string> task = factory.StartNew<string>(() =>
            {
                return "你好 task";
            });
            string str = task.Result;
            Console.WriteLine($"{str}");

四 Task线程等待(阻塞线程)

  • Wait()
            Task task1 = Task.Run(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("我排第二");
            });
            Console.WriteLine("我先打印");
            task1.Wait(2000); //等待2000毫秒后再往下走 如果写task1.Wait()就是必须要等task1子线程走完才能往下走(那我有必要开子线程嘛)
            Task task2 = Task.Run(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("我垫底了嘛");
            });
            task2.Wait(500);
            Console.WriteLine("我当老三");
  • WaitAll()
            Console.WriteLine("来任务咯");
            Task task1 = Task.Run(() => {
                Thread.Sleep(1000);
                Console.WriteLine("我耗时1秒");
            });
            Task task2 = Task.Run(() => {
                Thread.Sleep(2000);
                Console.WriteLine("我耗时2秒");
            });
            Task.WaitAll(task1, task2);// 等待task1和task2任务全走完才能往后走
            Console.WriteLine("没事,我等你们走完我再走");
  • WaitAny()
            Console.WriteLine("来任务咯");
            Task task1 = Task.Run(() => {
                Thread.Sleep(1000);
                Console.WriteLine("我耗时1秒");
            });
            Task task2 = Task.Run(() => {
                Thread.Sleep(2000);
                Console.WriteLine("我耗时2秒");
            });
            Task.WaitAny(task1, task2);// task1和task2只要有一个任务完成就往后走
            Console.WriteLine("只要有一个任务完成我就走喽");

五 Task任务的延续

  • TaskAwaiter<TResult> GetAwaiter():Task类的实例方法,返回TaskAwaiter对象。
  • TResult GetResult():TaskAwaiter类的实例方法,返回线程任务的返回结果。
            Task<int> task1 = Task.Run<int>(() => { return 1; });
            Task<int> task2 = Task.Run<int>(() => { return 2; });
            TaskAwaiter<int> task1Awaiter = task1.GetAwaiter();
            TaskAwaiter<int> task2Awaiter = task2.GetAwaiter();
            Task<int> task = Task.Run<int>(() =>
            {
                return task1Awaiter.GetResult() + task2Awaiter.GetResult();
            });
            Console.WriteLine($"让我来看看你们给的数字和是:{task.Result}");
            Console.WriteLine("会阻塞主线程哦");
  • WhenAll(task1,task2,...):Task的静态方法,作用是异步等待指定任务完成后,返回结果。当线程任务有返回值时,返回Task<TResult[]>对象,否则返回Task对象。
  • WhenAny()用法与WhenAll()是一样的,不同的是只要指定的任意一个线程任务完成则立即返回结果。
            // WhenAll WhenAny + ContinueWith 不阻塞主线程
            Task<int> task1 = Task.Run<int>(() => { Thread.Sleep(5000); return 1; });
            Task<int> task2 = Task.Run<int>(() => { Thread.Sleep(5000); return 2; });
            Task<String> task = Task<int>.WhenAll(task1, task2).ContinueWith((t) => {
                // t:ContinueWith的返回值,一个新的延续task
                return "3";
            });
            Console.WriteLine($"我立马打印,对我没影响");
            Console.WriteLine($"我要等task走完才能打印哦:{task.Result}");
            Console.WriteLine($"上面堵了,我也要等了");
        }
  • ContinueWith():Task类的实例方法,异步创建当另一任务完成时可以执行的延续任务。也就是当调用对象的线程任务完成后,执行ContinueWith()中的任务。
  • ContinueWhenAny:某一个任务执行结束后,去触发一个动作,不卡主线程,TaskFactory实例方法,等价于WhenAny+ContinueWith
  • ContinueWhenAll:所有任务执行完成后,去触发一个动作,不卡主线程,TaskFactory实例方法,等价于WhenAll+ContinueWith

六 Task枚举

  • AttachedToParent:父子任务
    假设遇到如下需求,线程parentTask中开启了线程task1和task2,希望在开启parentTask线程任务的主线程中阻塞等待parentTask、task1和task2的任务完成。
    此时可以将task1和task2线程依附到parentTask线程上作为parentTask的子线程,这样主线程在等待parentTask线程完成时,就必须同步等待task1和task2线程的任务完成
Task parentTask = new Task(() => {
    Task task1 = new Task(() => { Console.WriteLine("task1任务。。。。。。"); }, TaskCreationOptions.AttachedToParent);
    Task task2 = new Task(() => { Console.WriteLine("task2任务。。。。。。"); }, TaskCreationOptions.AttachedToParent);
    task1.Start();
    task2.Start();
});
parentTask.Start();
parentTask.Wait();
Console.WriteLine("我是主线程");
  • LongRunning:耗时任务
    当要执行的线程任务比较耗时时,建议在创建线程对象时传入参数TaskCreationOptions.LongRunning,以此来声明为长时间运行的线程任务。

默认情况下,新建Task线程是从线程池ThreadPool中分配出来的,当使用TaskCreationOptions.LongRunning声明后则是直接新建一个线程。这样就可以避免耗时任务一直占用线程池资源的情况。当然了,也可以直接使用Thread,效果上是一样的。

            Task task = new Task(() => { ...}, TaskCreationOptions.LongRunning);
            task.Start();

六 Task中使用取消令牌

  • 介绍说明
    Task中的取消功能使用的是CanclelationTokenSource,即取消令牌源对象,可用于解决多线程任务中协作取消和超时取消。
    CancellationToken Token:CanclelationTokenSource类的属性成员,返回CancellationToken对象,可以在开启或创建线程时作为参数传入。
    bool IsCancellationRequested:CanclelationTokenSource类的属性成员,表示当前任务是否已经请求取消。Token类中也有此属性成员,两者互相关联。
    void Cancel():CanclelationTokenSource类的实例方法,取消线程任务,同时将自身以及关联的Token对象中的IsCancellationRequested属性置为true。
    void CancelAfter(int millisecondsDelay):CanclelationTokenSource类的实例方法,用于延迟取消线程任务。
    CancellationTokenRegistration Register(Action callback):Token类的实例方法,用于注册取消任务后的回调任务。
    Dispose (): 当不再需要取消令牌时,应调用 CancellationTokenSource 的 Dispose 方法来释放资源。这是很重要的,特别是在长时间运行的应用程序中,以确保不会发生资源泄露
            // Task任务的取消和判断
            CancellationTokenSource cst = new CancellationTokenSource();
            Task task = Task.Run(() => {
                while (!cst.IsCancellationRequested)
                {
                    Console.WriteLine("持续时间:" + DateTime.Now);
                }
            }, cst.Token);//这里第二个参数传入取消令牌

            Thread.Sleep(2000);
            cst.Cancel(); //两秒后结束
            // 任务的延时取消可以用于访问超时、执行超时等情况下的任务强制终止
            CancellationTokenSource cst = new CancellationTokenSource();
            Task task = Task.Run(() => {
                while (!cst.IsCancellationRequested)
                {
                    Console.WriteLine("持续时间:" + DateTime.Now);
                }
            }, cst.Token);//这里第二个参数传入取消令牌
            cst.CancelAfter(2000); //两秒后结束 也是异步进行
            // Task任务取消回调:如果取消任务后希望做一些处理工作。
            // 此时可以使用CancellationToken类的Register()函数来注册一个委托(回调函数),用于取消线程后调用。
            CancellationTokenSource cst = new CancellationTokenSource();
            Task task = Task.Run(() => {
                while (!cst.IsCancellationRequested)
                {
                    Console.WriteLine("持续时间:" + DateTime.Now);
                    Thread.Sleep(500);
                }
            }, cst.Token);//这里第二个参数传入取消令牌
            cst.Token.Register(() => {
                Console.WriteLine("开始处理工作......");
                Thread.Sleep(2000);
                Console.WriteLine("处理工作完成......");
            });
            Thread.Sleep(2000);
            cst.Cancel(); //两秒后结束
  • 常规用法:
        private Task task;
        private CancellationTokenSource cancellationSource;

        private void TestTokenSource()
        {
            // 常规使用
            cancellationSource = new CancellationTokenSource();
            CancellationToken token = cancellationSource.Token;
            task = Task.Run(() =>
            {
                while (!token.IsCancellationRequested)
                {
                    Console.WriteLine("持续工作");
                }
            }, token);
            task.Wait(5000);
            cancellationSource?.Cancel();
            cancellationSource?.Dispose();
        }
  • CancellationTokenSource和CancellationToken区别:
    CancellationTokenSource(CTS): 这是生成取消令牌的主要对象。可以通过调用它的 Cancel 方法来请求取消一个或多个操作。
    CancellationToken(CT): 这是一个轻量级的结构,由 CancellationTokenSource 生成,并可以传递到多个操作中。操作可以检查此令牌的状态以确定是否应该取消其执行。
            cancellationSource = new CancellationTokenSource();
            cancellationSource.Cancel();
            cancellationSource.Dispose();


            if (cancellationSource.Token.IsCancellationRequested)// 报错 System.ObjectDisposedException:“CancellationTokenSource 已释放。”
            {
                Console.WriteLine("cancle");
            }
            else
            {
                Console.WriteLine("not cancle");
            }


            if (cancellationSource.IsCancellationRequested)
            {
                Console.WriteLine("cancle");// 打印
            }
            else
            {
                Console.WriteLine("not cancle");
            }


            if (cancellationSource != null)
            {
                Console.WriteLine("not null");// 打印
            }
            else
            {
                Console.WriteLine(" null");
            }

七 Task跨线程访问控件

在使用Winform或WPF编写程序时,经常会遇到跨线程访问控件的情况,除了使用Invoke和委托等方法外,还可以有以下两种解决方法。

  • 方式一:
    直接将TaskScheduler对象做为参数传给Start()函数(使用TaskScheduler.FromCurrentSynchronizationContext()可以获得TaskScheduler对象),以此来将线程任务传送到指定的调度程序中运行,结合WPF编程宝典多线程章节中的内容,应该是将线程任务丢给控件元素所在线程的调度程序中运行。这样做虽然可以跨线程访问控件,但是带来的弊端就是,如果线程任务耗时,就会让整个窗体卡住。
            Task task = new Task(() =>
            {
                Thread.Sleep(5000);//模拟耗时处理
                txt_Info.Text = "test"; //此为文本控件
            });
            task.Start(TaskScheduler.FromCurrentSynchronizationContext());
  • 方式二:
    针对线程耗时的情况,如果直接使用方式一,会导致整个UI界面都卡住,等到控件处理完成才恢复,这样显然是不可以的。因此要改变一下用法,利用线程延续,将耗时的任务与访问UI控件的任务分为两个线程,访问UI的线程放到延续的线程中。
            txt_Info.Text = "数据正在处理中......";
            txt_Info.Text = "数据正在处理中......";
            Task.Run(() =>
            {
                Thread.Sleep(5000);
            }).ContinueWith(t => {
                txt_Info.Text = "test";
            }, TaskScheduler.FromCurrentSynchronizationContext());

八 Task的异常处理

1.线程外部使用Wait:Task线程的异常处理不能直接将线程对象相关代码try-catch来捕获,那样是捕获不到异常的,因为开始异常还没发生,主线程已经执行完毕,需要通过调用线程对象的wait()函数,通过wait()函数来进行线程的异常捕获。此外,线程的异常会聚合到AggregateException异常对象中(AggregateException是专门用来收集线程异常的异常类),需要通过遍历该异常对象,获取正确的异常信息。如果捕获到线程异常之后,还想继续往上抛出,就需要调用AggregateException对象的Handle函数,并返回false。(Handle函数遍历了一下AggregateException对象中的异常)

            Task task1 = Task.Run(() =>
            {
                throw new Exception("线程1的异常抛出");
            });
            Task task2 = Task.Run(() =>
            {
                throw new Exception("线程2的异常抛出");
            });
            Task task3 = Task.Run(() =>
            {
                throw new Exception("线程3的异常抛出");
            });

            try
            {
                task1.Wait();
                task2.Wait();
                task3.Wait();
            }
            catch (AggregateException ex)
            {
                foreach (var item in ex.InnerExceptions)
                {
                    Console.WriteLine(item.Message);
                }
                //如果希望再将异常往外抛出,可以调用AggregateException的Handle函数
                //ex.Handle(p => false);
            }
            Console.Read();

2.线程内部可直接try-catch

            try
            {
                Task task = Task.Run(() =>
                {
                    try
                    {
                        int i = 0;
                        int j = 10;
                        int k = j / i; //尝试除以0,会异常
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine($"线程内异常{ex.Message}");
                    }
                });
            }
            catch (AggregateException aex)
            {
                foreach (var exception in aex.InnerExceptions)
                {
                    Debug.WriteLine($"线程不等待:异常{exception.Message}");
                }
            }

相关文章

  • C# 多线程的使用

    此篇文章简单总结了C#中主要的多线程实现方法,包括Thread、ThreadPool、Parallel和Task类...

  • Unity C#基础之 多线程的前世今生(下) 扩展篇

    在前面两篇Unity C#基础之 多线程的前世今生(上) 科普篇和Unity C#基础之 多线程的前世今生(中) ...

  • Unity 之如何写出强壮的代码

    【反射】 Unity C#基础之 反射反射,程序员的快乐 Unity C#基础之 特性,一个灵活的小工具 【多线程...

  • 多线程知识点记录

    c# Thread、ThreadPool、Task有什么区别,什么时候用,以及Task的使用先说 Thread与T...

  • C# Task

    Task是一种基于任务的编程模型。它与thread的主要区别是,它更加方便对线程进程调度和获取线程的执行结果。 T...

  • C#之Task开启线程

  • C#并行和多线程编程

    —— 第五天 多线程编程大总结一、多线程带来的问题1、死锁问题 前面我们学习了Task的使用方法,其中Task的等...

  • Java线程学习笔记(1)

    对于一个从C#转到Kotlin的菜鸟,C#的时候只知道async和await Task这样的操作,Kotlin也是...

  • 29、C#的多线程Task的使用

    使用案例 无返回值使用方式总结 Start方式启动 Run方式启动 Factory方式启动 综合示例 使用asyn...

  • C#学习笔记

    C#中的线程(一)入门 C#中的线程(二) 线程同步基础 C#中的线程(三) 使用多线程 20190130补充: ...

网友评论

      本文标题:C#多线程之Task

      本文链接:https://www.haomeiwen.com/subject/fosfxjtx.html