美文网首页
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#多线程之Task

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