C#多线程和异步

本文详细介绍了C#中的多线程和异步编程,包括Task的创建和运行、阻塞方法、延续操作以及任务取消。重点讲解了Task如何解决线程池的控制问题,并对比了Thread的Join方法。此外,还深入探讨了C#5.0引入的async/await关键字,展示了如何简化异步编程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C#多线程和异步的示例项目工程下载

一、什么是异步

  同步和异步主要用于修饰方法。

①同步:当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;

②异步:当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。

  异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。

二、Task介绍

    Task是在ThreadPool的基础上推出的,我们简单了解下ThreadPool。ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少线程的创建,节省开销,看一下ThreadPool的示例吧。

        //线程池执行方法
        private static void ThreadPoolRunMethod()
        {
            //调用线程池的线程执行任务
            for (int i = 1; i <= 10; i++)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => {

                    Console.WriteLine($"第:{obj} 个执行任务");
                }), i);
            }
        }

使用线程池运行10个任务,结果为:

    ThreadPool相对于Thread来说可以减少线程的创建,有效减小系统开销;但是ThreadPool不能控制线程的执行顺序,我们也不能获取线程池内线程取消/异常/完成的通知,即我们不能有效监控和控制线程池中的线程。

2.1、 Task创建和运行

2.1.1、无返回值Task任务的创建

  我们知道了ThreadPool的弊端我们不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消/异常/完成的通知。net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的优点,同时也解决了使用线程池不易控制的弊端。

首先看一下怎么去创建并运行一个Task,Task的创建和执行方式有如下三种:

 #region   1-Task任务(无返回值)

        //创建任务方式1
        private static Task CreateTask_One()
        {
            //new 方式实例化一个Task,需要通过Start方法启动
            Task task = new Task(()=> {
                Thread.Sleep(2000);
                int taskId = Thread.CurrentThread.ManagedThreadId;

                Console.WriteLine($"Hello 现在执行创建任务1 TaskId:{taskId} 的方法:{DateTime.Now}");

            });

            task.Start();

            return task;
        }


        //创建任务方式2
        private static Task CreateTask_Two()
        {
            //Task.Factory.StartNew(Action action)方式创建和启动一个Task
            Task task = Task.Factory.StartNew(()=> {

                Thread.Sleep(1000);
                
                Console.WriteLine($"Hello 现在执行创建任务2 TaskId:" +
                    $"{Thread.CurrentThread.ManagedThreadId} 的方法:{DateTime.Now}");
            });

            return task;
        }

        //创建任务方式3
        private static Task CreateTask_Three()
        {
            //Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
            Task task = Task.Run(()=> {
                Thread.Sleep(10000);
                Console.WriteLine($"Hello 现在执行创建任务3 TaskId:" +
                    $"{Thread.CurrentThread.ManagedThreadId} 的方法:{DateTime.Now}");
            });

            return task;
        }

        #endregion



 static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");


            //Task创建的三种执行方法(无返回值)
            CreateTask_One();
            CreateTask_Two();
            CreateTask_Three();

            Console.WriteLine("执行主线程!!!!");
            Console.ReadLine();

        }//Class_end

执行结果如下:

 我们看到先打印"执行主线程",然后再打印各个任务,说明了Task不会阻塞主线程。上边的示例Task都没有返回值,我们也可以创建有返回值的Task<TResult>,用法和没有返回值的基本一致,我们简单修改一下上边的栗子,代码如下:

2.2.2、有返回值Task任务的创建

        #region   2-Task任务(有返回值)

        //创建任务方式1
        private static Task<string> CreateTask_OneHaveResult()
        {
            string result = null;

            //new 方式实例化一个Task,需要通过Start方法启动
            Task<string> task = new Task<string>(() => {
                Thread.Sleep(2000);
                int taskId = Thread.CurrentThread.ManagedThreadId;

                result = $"【有返回值】Hello 现在执行创建任务1 TaskId:" +
                $"{taskId} 的方法:{DateTime.Now}";
                return result;

            });

            task.Start();

            return task;
        }


        //创建任务方式2
        private static Task<string> CreateTask_TwoHaveResult()
        {
            string result = null;
            //Task.Factory.StartNew(Action action)方式创建和启动一个Task
            Task<string> task = Task.Factory.StartNew<string>(() => {

                Thread.Sleep(1000);

                result = $"【有返回值】Hello 现在执行创建任务2 TaskId:" +
                $"{Thread.CurrentThread.ManagedThreadId} 的方法:{DateTime.Now}";
                return result;
            });

            
            return task;
        }

        //创建任务方式3
        private static Task<string> CreateTask_ThreeHaveResult()
        {
            string result = null;
            //Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task
            Task<string> task = Task.Run<string>(() => {
                Thread.Sleep(10000);
                result = $"【有返回值】Hello 现在执行创建任务3 TaskId:" +
                $"{Thread.CurrentThread.ManagedThreadId} 的方法:{DateTime.Now}";

                return result;
            });

            return task;
           
        }

        #endregion 


        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            //Task创建的三种执行方法(有返回值)
            Task<string> task1 = CreateTask_OneHaveResult();
            Console.WriteLine(task1.Result);
            Task<string> task2 = CreateTask_TwoHaveResult();
            Console.WriteLine(task2.Result);
            Task<string> task3 = CreateTask_ThreeHaveResult();
            Console.WriteLine(task3.Result);

          
            Console.WriteLine("执行主线程!!!!");
            Console.ReadLine();

        }//Class_end

   注意:task.Resut获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码,程序运行结果如下:

    上边的所有示例中Task的执行都是异步的,不会阻塞主线程。有些场景下我们想让Task同步执行怎么办呢?Task提供了 task.RunSynchronously()用于同步执行Task任务,代码如下:


        static void Main(string[] args)
        {
            Task task = new Task(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine("执行Task结束!");
            });
            //同步执行,task会阻塞主线程
            task.RunSynchronously();
            Console.WriteLine("执行主线程结束!");
            Console.ReadKey();
        }

执行结果如下:

2.2、Task的阻塞方法(Wait/WaitAll/WaitAny)

2.2.1、Thread阻塞线程的方法

    使用Thread时,我们知道用thread.Join()方法即可阻塞主线程。看一个例子:

  #region   3-线程

        //线程1方法
        private static Thread ThreadMethod_One()
        {
            Thread thread = new Thread(() => {
                Thread.Sleep(5000);
                Console.WriteLine("线程1执行完毕!");
            });
            thread.Start();

            return thread;
        }

        //线程2方法
        private static Thread ThreadMethod_Two()
        {
            Thread thread = new Thread(() => {
                Thread.Sleep(2000);
                Console.WriteLine("线程2执行完毕!");
            });
            thread.Start();

            return thread;
        }

        #endregion


 static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            //线程执行(测试阻塞)
            Thread td1 = ThreadMethod_One();
            Thread td2 = ThreadMethod_Two();

            //阻塞线程
            td1.Join();
            td2.Join();

          
            Console.WriteLine("执行主线程!!!!");
            Console.ReadLine();

        }//Class_end

①如果注释掉两个Join,执行结果是:先打印【主线程执行完毕】,效果如下:

②而添加两个Join方法后执行结果如下,实现了线程阻塞,效果如下:

2.2.2、Task的Wait/WaitAny/WaitAll方法 

    Thread的Join方法可以阻塞调用线程,但是有一些弊端:

①如果我们要实现很多线程的阻塞时,每个线程都要调用一次Join方法;

②如果我们想让所有的线程执行完毕(或者任一线程执行完毕)时,立即解除阻塞,使用Join方法不容易实现。Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制线程阻塞。

    task.Wait() 表示等待task执行完毕,功能类似于thead.Join(); Task.WaitAll(Task[] tasks) 表示只有所有的task都执行完成了再解除阻塞; Task.WaitAny(Task[] tasks) 表示只要有一个task执行完毕就解除阻塞,看一个示例:

        #region   1-Task任务(无返回值)

        //创建任务方式1
        private static Task CreateTask_One()
        {
            //new 方式实例化一个Task,需要通过Start方法启动
            Task task = new Task(()=> {
                Thread.Sleep(2000);
                int taskId = Thread.CurrentThread.ManagedThreadId;

                Console.WriteLine($"Hello 现在执行创建任务1 TaskId:{taskId} 的方法:{DateTime.Now}");

            });

            task.Start();

            return task;
        }


        //创建任务方式2
        private static Task CreateTask_Two()
        {
            //Task.Factory.StartNew(Action action)方式创建和启动一个Task
            Task task = Task.Factory.StartNew(()=> {

                Thread.Sleep(1000);
                
                Console.WriteLine($"Hello 现在执行创建任务2 TaskId:" +
                    $"{Thread.CurrentThread.ManagedThreadId} 的方法:{DateTime.Now}");
            });

            return task;
        }

        //创建任务方式3
        private static Task CreateTask_Three()
        {
            Task task = Task.Run(()=> {
                Thread.Sleep(10000);
                Console.WriteLine($"Hello 现在执行创建任务3 TaskId:" +
                    $"{Thread.CurrentThread.ManagedThreadId} 的方法:{DateTime.Now}");
            });

            return task;
        }

        #endregion



static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            //Task创建的三种执行方法(无返回值)(测试任务阻塞)
            Task task11 = CreateTask_One();
            Task task21 = CreateTask_Two();
            Task task31 = CreateTask_Three();

            //所有任务都执行完成才解除阻塞
            Task.WaitAll(task11,task21,task31);
            ////任意一个任务都执行完成就解除阻塞
            //Task.WaitAny(task11, task21, task31);

           
            Console.WriteLine("执行主线程!!!!");
            Console.ReadLine();

        }//Class_end

 ①使用WaitAll,那么所有task执行完毕才会解除线程阻塞,效果如下:

②使用WaitAny,那么任一task执行完毕就会解除线程阻塞,效果如下:

2.2.3、 Task的延续操作(WhenAny/WhenAll/ContinueWith)

    上边的Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。我们现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?

    这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。 ①task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操作;②task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作。看如下示例:

#region   1-Task任务(无返回值)

        //创建任务方式1
        private static Task CreateTask_One()
        {
            //new 方式实例化一个Task,需要通过Start方法启动
            Task task = new Task(()=> {
                Thread.Sleep(2000);
                int taskId = Thread.CurrentThread.ManagedThreadId;

                Console.WriteLine($"Hello 现在执行创建任务1 TaskId:{taskId} 的方法:{DateTime.Now}");

            });

            task.Start();

            return task;
        }


        //创建任务方式2
        private static Task CreateTask_Two()
        {
            //Task.Factory.StartNew(Action action)方式创建和启动一个Task
            Task task = Task.Factory.StartNew(()=> {

                Thread.Sleep(1000);
                
                Console.WriteLine($"Hello 现在执行创建任务2 TaskId:" +
                    $"{Thread.CurrentThread.ManagedThreadId} 的方法:{DateTime.Now}");
            });

            return task;
        }

        //创建任务方式3
        private static Task CreateTask_Three()
        {
            Task task = Task.Run(()=> {
                Thread.Sleep(10000);
                Console.WriteLine($"Hello 现在执行创建任务3 TaskId:" +
                    $"{Thread.CurrentThread.ManagedThreadId} 的方法:{DateTime.Now}");
            });

            return task;
        }

        #endregion


  static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            //当任务完成后才执行后续操作
            Task task12 = CreateTask_One();
            Task task22 = CreateTask_Two();
            Task task32 = CreateTask_Three();

            //当所有任务完成后才执行后续操作
            Task.WhenAll(task12, task22, task32).ContinueWith((t) =>
            {
                Thread.Sleep(100);
                Console.WriteLine("执行后续操作完毕!");
            });

            //////当任意一个任务完成后就执行后续操作
            ////Task.WhenAny(task12, task22, task32).ContinueWith((t) => {
            ////    Thread.Sleep(100);
            ////    Console.WriteLine("执行后续操作完毕!");
            ////});

            //////当所有任务完成后才执行后续操作
            ////Task.Factory.ContinueWhenAll(new Task[] { task12, task22, task32 },(t) =>
            ////{
            ////    Thread.Sleep(100);
            ////    Console.WriteLine("执行后续操作完毕!");
            ////});

            ////当任意一个任务完成后就执行后续操作
            //Task.Factory.ContinueWhenAny(new Task[] { task12, task22, task32 }, (t) =>
            //{
            //    Thread.Sleep(100);
            //    Console.WriteLine("执行后续操作完毕!");
            //});

         
            Console.WriteLine("执行主线程!!!!");
            Console.ReadLine();

        }//Class_end

WhenAll/WhenAny方法不会阻塞主线程。 

①使用WhenAll方法时所有的task都执行完毕才会执行后续操作,效果如下:

②使用WhenAny方法时只要有一个线程执行完毕就会开始执行后续操作,效果如下:

    也可以通过 Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction) 和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 来实现.

①使用Task.Factory.ContinueWhenAll方法时所有的task都执行完毕才会执行后续操作,效果如下:

 ②使用Task.Factory.ContinueWhenAny方法时任意一个的task都执行完毕才会执行后续操作,效果如下:

2.2.4、Task的任务取消(CancellationTokenSource)

1、Thread取消任务执行

  在Task前我们执行任务采用的是Thread,Thread怎么取消任务呢?一般流程是:设置一个变量来控制任务是否停止,如设置一个变量isStop,然后线程轮询查看isStop,如果isStop为true就停止,代码如下:

        static void Main(string[] args)
        {
            bool isStop = false;
            int index = 0;
            //开启一个线程执行任务
            Thread th1 = new Thread(() =>
              {
                  while (!isStop)
                  {
                      Thread.Sleep(1000);
                      Console.WriteLine($"第{++index}次执行,线程运行中...");
                  }
              });
            th1.Start();
            //五秒后取消任务执行
            Thread.Sleep(5000);
            isStop = true;
            Console.ReadKey();
        }

2 Task取消任务执行

  Task中有一个专门的类 CancellationTokenSource 来取消任务执行,还是使用上边的例子,我们修改代码如下,程序运行的效果不变。

        #region   4-取消任务
        //创建任务方式1(支持取消)
        private static Dictionary<Task, CancellationTokenSource> CreateTask_OneHaveCancel()
        {
            Dictionary<Task,CancellationTokenSource> dic = new Dictionary<Task, CancellationTokenSource>();
            CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

            //注册任务取消的事件
            cancellationTokenSource.Token.Register(() =>
            {
                Console.WriteLine("这是任务被取消后执行的操作!");
            });

            //new 方式实例化一个Task,需要通过Start方法启动
            Task task = new Task(() => {
                while (!cancellationTokenSource.IsCancellationRequested)
                {
                    Thread.Sleep(2000);
                    int taskId = Thread.CurrentThread.ManagedThreadId;

                    Console.WriteLine($"Hello 现在执行创建任务1 TaskId:{taskId} 的方法:{DateTime.Now}");
                }
            });

            AddInfoToDic(dic,task,cancellationTokenSource);

            task.Start();

            return dic;
        }


        //添加信息到字典
        private static void AddInfoToDic(Dictionary<Task,CancellationTokenSource> dic,Task key, CancellationTokenSource value)
        {
            if (dic.ContainsKey(key))
            {
                dic[key] = value;
            }
            else
            {
                dic.Add(key,value);
            }
        }

        #endregion 


        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            //创建任务方式1(支持取消)
            Dictionary<Task, CancellationTokenSource> taskDic = CreateTask_OneHaveCancel();

            string InputKey = Console.ReadLine().ToUpper();
            bool marke = true;

            while (marke)
            {
                if (InputKey == "Q")
                {
                    if (taskDic != null && taskDic.Count > 0)
                    {

                        foreach (var item in taskDic)
                        {
                            //取消任务(立即取消)
                            taskDic[item.Key].Cancel();

                            ////延迟取消任务(8秒后取消)
                            //taskDic[item.Key].CancelAfter(8000);

                        }
                    }

                    marke = false;
                }
            }


            Console.WriteLine("执行主线程!!!!");
            Console.ReadLine();

        }//Class_end

 ①实现按下Q键则立即取消任务执行,效果如下:

②实现按下Q键后,等待8秒后取消任务执行,效果如下:

    最后看上一篇跨线程的栗子,点击按钮启动一个任务,给tetxtbox赋值,我们把Thread改成Task,代码如下:

 public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void mySetValueBtn_Click(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); };
                for (int i = 0; i < 1000000; i++)
                {
                    myTxtbox.Invoke(setValue,i);
                }
            });
        }
    }

运行界面如下,赋值的task不会阻塞UI线程:

三、异步方法(async/await)

    在C#5.0中出现的 async和await ,让异步编程变得更简单。我们看一个获取文件内容的示例:


    class Program
    {
        static void Main(string[] args)
        {
            string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result;
            //调用同步方法
            //string content = GetContent(Environment.CurrentDirectory + @"/test.txt");
            Console.WriteLine(content);
            Console.ReadKey();
        }
        //异步读取文件内容
        async static Task<string> GetContentAsync(string filename)
        {
            
            FileStream fs = new FileStream(filename, FileMode.Open);
            var bytes = new byte[fs.Length];
            //ReadAync方法异步读取内容,不阻塞线程
            Console.WriteLine("开始读取文件");
            int len = await fs.ReadAsync(bytes, 0, bytes.Length);
            string result = Encoding.UTF8.GetString(bytes);
            return result;
        }
        //同步读取文件内容
        static string GetContent(string filename)
        {
            FileStream fs = new FileStream(filename, FileMode.Open);
            var bytes = new byte[fs.Length];
            //Read方法同步读取内容,阻塞线程
            int len =  fs.Read(bytes, 0, bytes.Length);
            string result = Encoding.UTF8.GetString(bytes);
            return result;
        }
    }

 test.txt内容是【hello world!】执行结果为:

    上边的示例也写出了同步读取的方式,将main函数中的注释去掉即可同步读取文件内容。我们可以看到异步读取代码和同步读取代码基本一致。async/await让异步编码变得更简单,我们可以像写同步代码一样去写异步代码。

    注意一个小问题:异步方法中方法签名返回值为Task<T>,代码中的返回值为T。上边示例中GetContentAsync的签名返回值为Task<string>,而代码中返回值为string。牢记这一细节对我们分析异步代码很有帮助。

异步方法签名的返回值有以下三种:

    ① Task<T>:如果调用方法想通过调用异步方法获取一个T类型的返回值,那么签名必须为Task<TResult>;

    ② Task:如果调用方法不想通过异步方法获取一个值,仅仅想追踪异步方法的执行状态,那么我们可以设置异步方法签名的返回值为Task;

    ③ void:如果调用方法仅仅只是调用一下异步方法,不和异步方法做其他交互,我们可以设置异步方法签名的返回值为void,这种形式也叫做“调用并忘记”。

  小结:到这里Task,async/await的简单使用已经基本结束了,一些高级特性等到工作遇到了再去研究。

①通过上边的介绍,我们知道async/await是基于Task的,而Task是对ThreadPool的封装改进,主要是为了更有效的控制线程池中的线程(ThreadPool中的线程,我们很难通过代码控制其执行顺序,任务延续和取消等等);

②ThreadPool基于Thread的,主要目的是减少Thread创建数量和管理Thread的成本。

③async/await Task是C#中更先进的,也是微软大力推广的特性,我们在开发中可以尝试使用Task来替代Thread/ThreadPool,处理本地IO和网络IO任务是尽量使用async/await来提高任务执行效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值