一篇文章彻底了解c#异步多线程Task的使用

1,634 阅读5分钟

一、概念

在C#中,Task是一种异步编程模型,它允许您在调用方法时不会阻塞线程,而是将工作放入队列中,允许该线程继续执行其他操作。这种模型在处理I/O、网络请求、数据库操作等操作时非常有用,可以提高应用程序的性能和响应能力。Task是.NET框架的一部分,可以轻松使用,同时它也提供了许多与线程相关的API,例如同步、等待、取消、报告进度等。

二、功能使用

我这里定义了一个一会可供多线程调用的方法

public class TaskClass2 
{
    public static void A()
    {
        Thread.Sleep(2000);
        Console.WriteLine("我是TaskClass2类的A方法d等待两秒钟后的输出:当前线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
    }

}

1、开启线程

在c#中, Task开启线程有三种方法

(1) 实例化 Task 然后通过 task.start(); 开启线程
(2) 直接Task.Run 新跑一个线程
(3) 创建一个线程对象Task.Factory,使用的时候通过 .StartNew(委托: 方法参数)调用

我这里写了一个接口,分别用三种不认同的方式开启了线程,打印了不同的数据

    /// <summary>
    /// 开启线程
    /// </summary>
    /// <returns></returns>
    [HttpGet("StartTask")]
    public string StartTask()
    {
        Console.WriteLine("主线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        //第一种方法: 实例化 Task 然后通过 task.start(); 开启线程
        //默认Task实例 需要传递一个委托: 方法参数
        Task task = new Task(TaskClass2.A);
        task.Start();

        //第二种方法: 直接Task.Run 新跑一个线程
        Task.Run(() =>
        {
            TaskClass2.A();
        });

        //第三种方法: 创建一个线程对象Task.Factory,使用的时候通过 .StartNew(委托: 方法参数)调用
        TaskFactory taskFactory = Task.Factory;
        Task taskf = taskFactory.StartNew(TaskClass2.A);

        Console.WriteLine("主线程执行完毕");
        return "value";
    }
    
    

image.png 如图,可以看到这三个方式开启的线程,打印了不同的线程id。

2、等待线程 Thread.Sleep()

Thread.Sleep()等待方法,在哪个线程调用,就会阻塞哪个线程进行等待

例: A线程中嵌套了一个B线程 在B中Thread.Sleep() 只会阻塞B 并不会影响A的运行,反之也是

    /// <summary>
    /// 等待线程
    /// Thread.Sleep()等待方法,在哪个线程调用,就会阻塞哪个线程进行等待
    /// 例: A线程中嵌套了一个B线程  在B中Thread.Sleep() 只会阻塞B 并不会影响A的运行,反之也是
    /// </summary>
    /// <returns></returns>
    [HttpGet("SleepTask")]
    public string SleepTask()
    {
        Console.WriteLine("当前时间为:" + DateTime.Now);

        Thread.Sleep(10000);

        Console.WriteLine("等待10秒钟后的当前时间为:" + DateTime.Now);

        return "value";
    }
 

image.png 如图可以看到,我们成功阻塞了线程,让他等待了我们指定的时间 再进行后续代码的执行。

3、线程回调

(1)异步单线程回调 ContinueWith

异步线程执行完之后,执行需要的方法

    /// <summary>
    /// 异步单线程回调
    /// 异步线程执行完之后,执行需要的方法
    /// </summary>
    /// <returns></returns>
    [HttpGet("ContinueWithTask")]
    public string ContinueWithTask()
    {
        Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        //单线程回调
        Task.Run(TaskClass2.A).ContinueWith(t =>
        {
            Console.WriteLine($"我是TaskClass2.A方法执行完毕之后的单线程回调函数,当前id为: {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        });

        Console.WriteLine("主线程执行完毕");

        return "value";
    }
    

image.png 如图,这是异步单线程回调结果。在执行完A方法之后,自动执行一遍我们设置的回调函数。

(2)异步多线程回调 ContinueWhenAny

只完成其中一个执行回调

    /// <summary>
    /// 异步多线程回调
    /// 只完成其中一个执行回调
    /// </summary>
    /// <returns></returns>
    [HttpGet("ContinueWhenAnyTask")]
    public string ContinueWhenAnyTask()
    {
        Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        //多线程,执行回调
        TaskFactory taskFactory = new TaskFactory();
        List<Task> taskList = new List<Task>();
        taskList.Add(taskFactory.StartNew(() =>
        {
            Thread.Sleep(1000);
            Console.WriteLine("我是同时开启的线程1,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(taskFactory.StartNew(() =>
        {
            Thread.Sleep(2000);
            Console.WriteLine("我是同时开启的线程2,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(taskFactory.StartNew(() =>
        {
            Thread.Sleep(3000);
            Console.WriteLine("我是同时开启的线程3,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(taskFactory.StartNew(() =>
        {
            Thread.Sleep(4000);
            Console.WriteLine("我是同时开启的线程4,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));

        //多线程,只完成其中一个执行回调
        taskFactory.ContinueWhenAny(taskList.ToArray(), t =>
        {
            Console.WriteLine("我是多线程,只完成其中一个执行的回调");
        });

        Console.WriteLine("主线程执行完毕");

        return "value";
    }

image.png 如图,在执行完多个线程中的其中一个,就执行了回调函数。

(3)异步多线程回调 ContinueWhenAll

全部完成再执行回调

    /// <summary>
    /// 异步多线程回调
    /// 全部完成再执行回调
    /// </summary>
    /// <returns></returns>
    [HttpGet("ContinueWhenAllTask")]
    public string ContinueWhenAllTask()
    {
        Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));

        //多线程,执行回调
        TaskFactory taskFactory = new TaskFactory();
        List<Task> taskList = new List<Task>();
        taskList.Add(taskFactory.StartNew(() =>
        {
            Thread.Sleep(1000);
            Console.WriteLine("我是同时开启的线程1,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(taskFactory.StartNew(() =>
        {
            Thread.Sleep(2000);
            Console.WriteLine("我是同时开启的线程2,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(taskFactory.StartNew(() =>
        {
            Thread.Sleep(3000);
            Console.WriteLine("我是同时开启的线程3,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(taskFactory.StartNew(() =>
        {
            Thread.Sleep(4000);
            Console.WriteLine("我是同时开启的线程4,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));

        //多线程,完成全部执行回调
        taskFactory.ContinueWhenAll(taskList.ToArray(), t =>
        {
            Console.WriteLine("我是多线程,完成全部执行的回调");
        });
        Console.WriteLine("主线程执行完毕");

        return "value";
    }

image.png 如图,该回调函数会等在所有的线程执行完之后再调用。

4、阻塞当前线程,

(1)等着任意一个任务完成 WaitAny
    /// <summary>
    /// 阻塞当前线程,等着任意一个任务完成
    /// </summary>
    /// <returns></returns>
    [HttpGet("WaitAnyTask")]
    public string WaitAnyTask()
    {
        Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));

        List<Task> taskList = new List<Task>();
        taskList.Add(Task.Run(() =>
        {
            Thread.Sleep(1000);
            Console.WriteLine("我是同时开启的线程1,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(Task.Run(() =>
        {
            Thread.Sleep(2000);
            Console.WriteLine("我是同时开启的线程2,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(Task.Run(() =>
        {
            Thread.Sleep(3000);
            Console.WriteLine("我是同时开启的线程3,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(Task.Run(() =>
        {
            Thread.Sleep(4000);
            Console.WriteLine("我是同时开启的线程4,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));

        //阻塞当前线程,等着任意一个任务完成
        Task.WaitAny(taskList.ToArray());


        Console.WriteLine("主线程执行完毕");

        return "value";
    }

image.png 如图,主线程会等待所有子线程中其中一个完成,再继续往下执行主线程。

(2)等待全部线程执行完成,继续执行 WaitAll
    /// <summary>
    /// 阻塞当前线程,等待全部线程执行完成,继续执行
    /// </summary>
    /// <returns></returns>
    [HttpGet("WaitAllTask")]
    public string WaitAllTask()
    {
        Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));

        List<Task> taskList = new List<Task>();
        taskList.Add(Task.Run(() =>
        {
            Thread.Sleep(1000);
            Console.WriteLine("我是同时开启的线程1,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(Task.Run(() =>
        {
            Thread.Sleep(2000);
            Console.WriteLine("我是同时开启的线程2,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(Task.Run(() =>
        {
            Thread.Sleep(3000);
            Console.WriteLine("我是同时开启的线程3,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));
        taskList.Add(Task.Run(() =>
        {
            Thread.Sleep(4000);
            Console.WriteLine("我是同时开启的线程4,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }));

        //阻塞当前线程,等待全部线程执行完成
        Task.WaitAll(taskList.ToArray());


        Console.WriteLine("主线程执行完毕");

        return "value";
    }

image.png 如图,主线程会等待所有子线程执行完毕之后,在执行主线程代码。

5、控制线程并发数

在工作中,经常会遇到控制并发线程数量在多少以内,防止太多线程紊乱问题。

我们在这里建设5个并发数。

    /// <summary>
    /// 控制线程并发数量
    /// 假设5个
    /// </summary>
    /// <returns></returns>
    [HttpGet("ControlTaskCount")]
    public string ControlTaskCount()
    {
        List<Task> taskList = new List<Task>();
        for (int i = 0; i < 100; i++)
        {
            //临时变量问题,线程是非阻塞的,延迟启动的;线程执行的时候,i已经是5了
            //k是闭包里面的变量,每次循环都有一个独立的k
            //5个k变量  1个i变量
            int k = i;
            //在集合中找未执行成功的线程个数
            if (taskList.Count(t => t.Status != TaskStatus.RanToCompletion) >= 5)
            {
                //等待这五个线程中其中一个完成
                Task.WaitAny(taskList.ToArray());
                Console.WriteLine();
                //筛除已经执行成功的线程
                taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList();
            }
            //向list线程列表添加新的线程
            taskList.Add(Task.Run(() =>
            {
                Console.WriteLine($"我是第{k}个线程, 线程id为:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
            }));
        }

       return "value";
    }

image.png 如图,可以看到他是按照我们需求5个线程并发数执行的。

6、多线程异常处理

工作中常规建议:多线程的委托里面不允许异常,包一层try-catch,然后记录下来异常信息,完成需要的操作

    /// <summary>
    /// 多线程异常处理
    /// 工作中常规建议:多线程的委托里面不允许异常,包一层try-catch,然后记录下来异常信息,完成需要的操作
    /// </summary>
    /// <returns></returns>
    [HttpGet("ExceptionTask")]
    public string ExceptionTask()
    {
        Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));

        try
        {
            List<Task> taskList = new List<Task>();
            for (int i = 0; i < 20; i++)
            {
                int k = i;
                taskList.Add(Task.Run(() =>
                {
                    if (k == 10 || k == 11)
                    {
                        throw new Exception("代码异常");
                    }
                    Console.WriteLine($"我是子线程{k},线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
                }));
            }
            //多线程里面抛出的异常,会终结当前线程;但是不会影响别的线程;
            //捕获所有线程中报错的线程,到catch中打印
            Task.WaitAll(taskList.ToArray());
        }
        catch (AggregateException aex)//Task.WaitAll捕获发生的异常
        {
            foreach (var exception in aex.InnerExceptions)//当前异常的所有实例
            {
                Console.WriteLine(exception.Message);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("代码异常:" + ex.Message);
        }

        Console.WriteLine("主线程执行完毕");
        return "value";
    }

image.png 如图,多线程里面抛出的异常,会终结当前线程,但是不会影响别的线程,并没有输出我们的线程id,并且我们也成功的通过/Task.WaitAll捕获到我们假设的10和11的异常线程,并通过catch处理打印了出来。

7、线程取消

例:一共十个线程,运行到第三个报错了,需要通知其余七个停止运行

    /// <summary>
    /// 线程取消
    /// 例:一共十个线程,运行到第三个报错了,需要通知其余七个停止运行
    /// </summary>
    /// <returns></returns>
    [HttpGet("CancelTask")]
    public string CancelTask()
    {
        Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
        //解决方法:
        //1、 Thread.Abort--终止线程;向当前线程抛一个异常然后终结任务;线程属于OS资源,可能不会立即停下来
        //2、 Task不能外部终止任务,只能自己终止自己(上帝才能打败自己)
        //3、cts有个bool属性IsCancellationRequested 初始化是false,调用Cancel方法后变成true(不能再变回去),可以重复cancel
        try
        {
            //代表线程被取消的实例
            CancellationTokenSource cts = new CancellationTokenSource();
            List<Task> taskList = new List<Task>();
            for (int i = 0; i < 20; i++)
            {
                try
                {
                    int k = i;
                    taskList.Add(Task.Run(() =>
                    {
                        //验证是否设置取消了
                        if (!cts.IsCancellationRequested)
                        {
                            //假设遇到10  11 会代码异常 则给他设置cts.Cancel(); 取消
                            if (k == 10)
                            {
                                throw new Exception("代码异常");
                            }
                            if (k == 11)
                            {
                                cts.Cancel();
                            }
                            Console.WriteLine($"我是子线程{k},线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
                        }
                        else
                        {
                            Console.WriteLine($"线程已经被取消");
                        }
                    }, cts.Token));
                }
                catch (Exception)
                {
                    //代码发生异常
                    //设置取消线程
                    cts.Cancel();
                }
            }
            //1 准备cts  2 try-catch-cancel  3 Action要随时判断IsCancellationRequested
            //尽快停止,肯定有延迟,在判断环节才会结束

            Task.WaitAll(taskList.ToArray());
            //如果线程还没启动,能不能就别启动了?
            //1 启动线程传递Token  2 异常抓取  
            //在Cancel时还没有启动的任务,就不启动了;也是抛异常,cts.Token.ThrowIfCancellationRequested
        }
        catch (AggregateException aex)
        {
            foreach (var exception in aex.InnerExceptions)
            {
                Console.WriteLine("取消线程:" + exception.Message);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        Console.WriteLine("主线程执行完毕");
        return "value";
    }

注意: 在开启线程的时候Run,要把cts.Token线程取消器的Token传递进去,把线程绑定到取消器上,这样如果取消器设置取消,那么绑定的所有线程都会被取消。

image.png 如图,我们通过CancellationTokenSource线程取消器的Cancel事件设置取消状态,使用IsCancellationRequested来判断是否取消并打印我们取消信息。

8、取消调用回调函数 TaskContinuationOptions.OnlyOnRanToCompletion

在项目中,经常会遇到用一个线程去执行业务A,然后在这个线程执行完之后调用回调函数,执行另一个业务B,那么如果业务A在执行过程中发生异常报错,就不应再在执行业务B了,此时就需要用到 TaskContinuationOptions.OnlyOnRanToCompletion,验证相对回调函数的主线程是否发生异常。

    /// <summary>
    /// 判断多线程报错,不执行回调函数
    /// </summary>
    /// <returns></returns>
    [HttpGet("ExceptionContinueWithTask")]
    public string ExceptionContinueWithTask()
    {
        Task.Run(() =>
        {
            Student student = null;
            if (student.Id == "1")
            {
                Console.WriteLine("123312");
            }
        }).ContinueWith(s=>
        {
            if (s.Exception == null || s.Exception.InnerExceptions.Count == 0)
            {
                Console.WriteLine(1312);
            }
        }, TaskContinuationOptions.OnlyOnRanToCompletion);
        return "value";
    }

image.png 如图,发生代码异常的输出中没有输出回调函数输出内容,而不会发生代码异常的片段中输出了回调函数内容,达到了我们预期的效果。

今天的异步多线程Task的分享就到这里,大家还有更换的管用多线程的使用,欢迎到评论区讨论!!!