C#--多线程--Task和各种任务阻塞、延续及其线程锁Lock和Task中的跨线程访问控件和UI耗时任务卡顿的解决方法

以下是学习笔记:

回顾:

Thread线程和ThreadPool线程池

Thread:我们可以开启一个线程。但是请大家记住:线程开启会在空间和时间上有不小的开销。所以,不能随便开。

ThreadPool:会根据你的CPU的核心数开启一个最合适的线程数量。如果你操作中,非常耗时,就不要用线程池,如果耗时十几分钟,那就不合适线程池了。

 Task=>Thread +  ThreadPool结合 ,使用多线程,尽量使用Task

 1,Task和各种任务阻塞、延续及其线程锁Lock

        #region Task使用【1】多线程任务的开启3种方式

        //【1】通过new的方式创建一个Task对象,并启动
        static void Method1_1()
        {
            Task task1 = new Task(() =>
            {
                //在这个地方编写我们需要的逻辑...

                Console.WriteLine($"new一个新的Task启动的子线程Id={Thread.CurrentThread.ManagedThreadId}");
            });
            task1.Start();
        }

        //【2】使用Task的Run()方法
        static void Method1_2()
        {
            Task task2 = Task.Run(() =>
              {
                  //在这个地方编写我们需要的逻辑...

                  Console.WriteLine($"使用Task的Run()方法开启的子线程Id={Thread.CurrentThread.ManagedThreadId}");
              });
        }
        //1和2对比
        //1,灵活开启线程,想什么时候开启就什么时候开启  
        //2, 马上开启线程

        //【3】使用TaskFactory启动(类似于ThreadPool)
        static void Method1_3()
        {
            Task task3 = Task.Factory.StartNew(() =>
            {
                //在这个地方编写我们需要的逻辑...

                Console.WriteLine($"使用TaskFactory开启的子线程Id={Thread.CurrentThread.ManagedThreadId}");
            });
        }

        #endregion

        #region Task使用【2】Task的阻塞方式和任务延续

        //【1】回顾之前使用Thread多个子线程执行时阻塞的方法
        static void Method2()
        {
            Thread thread1 = new Thread(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine("Child Thread (1)......");
            });
            Thread thread2 = new Thread(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine("Child Thread (2)......");
            });
            thread1.Start();
            thread2.Start();
            //...

            thread1.Join();//让调用线程阻塞
            thread2.Join();
            //如果有很多的thread,是不是也得有很多的Join?还有,我们只希望其中一个执行完以后,后面的其他线程就能执行,这个也做不了!

            Console.WriteLine("This is Main Thread!");
        }
        //【2】Task各种【阻塞】方式(3个)
        static void Method3()
        {
            Task task1 = new Task(() =>
             {
                 Thread.Sleep(1000);
                 Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
             });
            task1.Start();
            Task task2 = new Task(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
            });
            task2.Start();

            ////第1种方式:挨个等待和前面一样
            //task1.Wait();
            //task2.Wait();

            ////第2种方式:等待所有的任务完成    【推荐】
            Task.WaitAll(task1, task2);

            //第3种方式:等待任何一个完成即可  【推荐】
            //Task.WaitAny(task1, task2);

            Console.WriteLine("主线程开始运行!Time=" + DateTime.Now.ToLongTimeString());

            /*
            第2中方式结果:
            Task1子线程Id=4  21:46:58
            Task2子线程Id=3  21:46:59
            主线程开始运行!Time=21:46:59

            第3种方式结果
            Task1子线程Id = 3  21:41:34
            主线程开始运行!Time = 21:41:34
            Task2子线程Id = 4  21:41:35
            */
        }

        //Task任务的延续:WhenAll 希望前面所有任务执行完毕后,再继续执行后面的线程,和前面相比,既有阻塞,又有延续。
        static void Method4()
        {
            Task task1 = new Task(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
            });
            task1.Start();
            Task task2 = new Task(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
            });
            task2.Start();

            //线程的延续(主线程不等待,子线程依次执行,如果你需要主线程也按照子线程的顺序来,请你自己把主线程的任务放到延续任务中就可以)
            //线运行主线程,然后task1和task2都执行完,再执行task3
            Task.WhenAll(task1, task2).ContinueWith(task3 =>
             {
                 //在这里可以编写你需要的业务...

                 Console.WriteLine($"Task3子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
             });

            Console.WriteLine("主线程开始运行!Time=" + DateTime.Now.ToLongTimeString());

            /*
            主线程开始运行!Time = 21:44:46
            Task1子线程Id = 3  21:44:47
            Task2子线程Id = 4  21:44:48
            Task3子线程Id = 3  21:44:48
            */
        }

        //Task的延续:WhenAny
        static void Method5()
        {
            Task task1 = new Task(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
            });
            task1.Start();
            Task task2 = new Task(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
            });
            task2.Start();

            //线程的延续(主线程不等待,子线程任何一个执行完毕,就会执行后面的线程)
            Task.WhenAny(task1, task2).ContinueWith(task3 =>
            {
                //在这里可以编写你需要的业务...

                Console.WriteLine($"Task3子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
            });

            Console.WriteLine("主线程开始运行!Time=" + DateTime.Now.ToLongTimeString());

            /*
            主线程开始运行!Time=21:48:51
            Task1子线程Id=3  21:48:52
            Task3子线程Id=6  21:48:52
            Task2子线程Id=4  21:48:53
            */
        }

        #endregion

        #region Task使用【3】Task常见枚举 TaskCreationOptions(父子任务运行、长时间运行的任务处理)

        //请大家通过Task的构造方法,观察TaskCreationOptions这个枚举的类型,自己通过F12查看
        static void Method6()
        {
            Task parentTask = new Task(() =>
             {
                 Task task1 = new Task(() =>
                  {
                      Thread.Sleep(1000);
                      Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
                  }, TaskCreationOptions.AttachedToParent);

                 Task task2 = new Task(() =>
                 {
                     Thread.Sleep(3000);
                     Console.WriteLine($"Task2子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
                 }, TaskCreationOptions.AttachedToParent);
                 task1.Start();
                 task2.Start();
             });

            parentTask.Start();
            parentTask.Wait();//等待附加的子任务全部完成。相当于Task.WaitAll(taks1,task2);
            //TaskCreationOptions.AttachedToParent如果这个枚举参数不添加,主线程会直接运行,不等待
            Console.WriteLine("主线程开始执行!Time=  " + DateTime.Now.ToLongTimeString());

            /*
            Task1子线程Id=4  21:52:17
            Task2子线程Id=5  21:52:19
            主线程开始执行!Time=  21:52:19
             */
        }

        //长时间的任务运行,需要采取的方法
        static void Method7()
        {
            Task task1 = new Task(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine($"Task1子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
            }, TaskCreationOptions.LongRunning);

            //LongRunning:如果你明确知道这个任务是长时间运行的,建议你加上。
            //当然你使用Thread也是可以的。但是不要使用ThreadPool,因为长时间占用不归还线程,系统会强制开启新的线程,会一定程度影响性能
            task1.Start();
            task1.Wait();

            Console.WriteLine("主线程开始执行!Time=  " + DateTime.Now.ToLongTimeString());

            /*
             Task1子线程Id=3  21:57:42
            主线程开始执行!Time=  21:57:42
             */
        }
        #endregion

        #region  Task使用【4】Task中的取消功能:使用的是CacellationTokenSoure解决多任务中协作取消和超时取消方法

        //【1】Task任务的取消和判断
        static void Method8()
        {
            //创建取消信号源对象
            CancellationTokenSource cts = new CancellationTokenSource();
            Task task = Task.Factory.StartNew(() =>
            {
                int i = 0;
                while (!cts.IsCancellationRequested) //判断任务是否被取消
                {
                    Thread.Sleep(200);
                    i++;
                    Console.WriteLine(
                        $"执行次数:{i},子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
                }
            }, cts.Token);

            //我们在这个地方模拟一个事件产生,如果发生某个错误,就取消线程
            Thread.Sleep(2000);
            cts.Cancel(); //取消任务,只要传递这样一个信号就可以

            /*
            执行次数:1,子线程Id=3  22:06:18
            执行次数:2,子线程Id=3  22:06:18
            执行次数:3,子线程Id=3  22:06:18
            执行次数:4,子线程Id=3  22:06:18
            执行次数:5,子线程Id=3  22:06:19
            执行次数:6,子线程Id=3  22:06:19
            执行次数:7,子线程Id=3  22:06:19
            执行次数:8,子线程Id=3  22:06:19
            执行次数:9,子线程Id=3  22:06:19
            执行次数:10,子线程Id=3  22:06:20
            */
        }

        //【2】Task任务取消:同时我们也希望做一些清理的工作,也就是取消这个动作会触发一个任务。
        static void Method9()
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            Task task = Task.Factory.StartNew(() =>
            {
                while (!cts.IsCancellationRequested)
                {
                    Thread.Sleep(500);

                    Console.WriteLine($"子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
                }
            }, cts.Token);

            //注册一个委托:这个委托将在任务取消的时候调用
            cts.Token.Register(() =>
            {
                //在这个地方可以编写自己要处理的逻辑...
                Console.WriteLine($"任务取消,开始清理工作......{DateTime.Now.ToLongTimeString()}");
                Thread.Sleep(2000);
                Console.WriteLine($"任务取消,清理工作结束......{DateTime.Now.ToLongTimeString()}");
            });

            //这个地方肯定是有其他的逻辑来控制取消
            Thread.Sleep(3000);//模拟其他的耗时工作
            cts.Cancel();//取消任务

            /*
            子线程Id=3  22:12:52
            子线程Id=3  22:12:53
            子线程Id=3  22:12:53
            子线程Id=3  22:12:54
            子线程Id=3  22:12:54
            任务取消,开始清理工作......22:12:55
            子线程Id=3  22:12:55
            任务取消,清理工作结束......22:12:57
             */
        }

        //【3】Task任务延时自动取消:比如我们请求一个远程接口,如果长时间没有返回数据,我们可以做一个时间限制,超时可以取消任务(比如微信红包退回)
        static void Method10()
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            // CancellationTokenSource cts = new CancellationTokenSource(3000);
            Task task = Task.Factory.StartNew(() =>
            {
                while (!cts.IsCancellationRequested)
                {
                    Thread.Sleep(300);

                    Console.WriteLine($"子线程Id={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
                }
            }, cts.Token);

            //注册一个委托:这个委托将在任务取消的时候调用
            cts.Token.Register(() =>
            {
                //在这个地方可以编写自己要处理的逻辑...
                Console.WriteLine($"任务取消,开始清理工作......{DateTime.Now.ToLongTimeString()}");
                Thread.Sleep(2000);
                Console.WriteLine($"任务取消,清理工作结束......{DateTime.Now.ToLongTimeString()}");
            });

            cts.CancelAfter(3000); //3秒后自动取消

            /*
            子线程Id=3  22:16:49
            子线程Id=3  22:16:50
            子线程Id=3  22:16:50
            子线程Id=3  22:16:50
            子线程Id=3  22:16:50
            子线程Id=3  22:16:51
            子线程Id=3  22:16:51
            子线程Id=3  22:16:51
            子线程Id=3  22:16:52
            任务取消,开始清理工作......22:16:52
            子线程Id=3  22:16:52
            任务取消,清理工作结束......22:16:54
             */
        }

        #endregion

        #region Task使用【5】Task中专门的异常处理:AggregateException

        //AggregateException:是一个异常集合,因为Task中可能抛出异常,所以我们需要新的类型来收集异常对象
        static void Method11()
        {
            var task = Task.Factory.StartNew(() =>
            {
                var childTask1 = Task.Factory.StartNew(() =>
                {
                    //实际开发中这个地方写你处理的业务,可能会发生异常....

                    //自己模拟一个异常
                    throw new Exception("my god!Exception from childTask1 happend!");
                }, TaskCreationOptions.AttachedToParent);

                var childTask2 = Task.Factory.StartNew(() =>
                {
                    throw new Exception("my god!Exception from childTask2 happend!");
                }, TaskCreationOptions.AttachedToParent);
            });
            try
            {
                try
                {
                    task.Wait();   //1.异常抛出的时机(等待task执行完毕,这里是等到异常抛出)
                }
                catch (AggregateException ex)  //2.异常所在位置
                {
                    foreach (var item in ex.InnerExceptions)
                    {
                        Console.WriteLine(item.InnerException.Message + "     " + item.GetType().Name);
                    }

                    //3.异常集合,如果你想往上抛,需要使用Handle方法处理一下
                    ex.Handle(p =>
                    {
                        if (p.InnerException.Message == "my god!Exception from childTask1 happend!")
                            return true;//就结束了,不往上抛了
                        else
                            return false; //返回false表示往上继续抛出异常
                    });
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("-----------------------------------------------------");
                Console.WriteLine(ex.InnerException.InnerException.Message);
            }

            /*
            my god!Exception from childTask2 happend!     AggregateException
            my god!Exception from childTask1 happend!     AggregateException
            -----------------------------------------------------
            my god!Exception from childTask2 happend!
             */
        }

        #endregion

        #region 监视锁:Lock  限制线程个数的一把锁

        //为什么要用锁?在多线程中,尤其是静态资源的访问,必然会有竞争

        private static int nums = 0;
        private static object myLock = new object();
        static void Method12()
        {
            for (int i = 0; i < 5; i++)
            {
                //开启5线程调用一个nums
                Task.Factory.StartNew(() =>
                {
                    //TestMethod1();//不加锁的结果顺序是乱的,1,3,2,4,6,9,,,,500
                    TestMethod2();//加锁的结果顺序是对的,因为把资源给锁住了,1,2,3,4,5,6,,,,500
                });
            }
        }

        static void TestMethod1()
        {
            for (int i = 0; i < 100; i++)
            {
                nums++;
                Console.WriteLine(nums);
            }
        }

        static void TestMethod2()
        {
            for (int i = 0; i < 100; i++)
            {
                lock (myLock)
                {
                    nums++;
                    Console.WriteLine(nums);
                }
            }
        }

        //Lock是Monitor语法糖,本质是解决资源的锁定问题
        //我们锁住的资源一定是让线程可访问到的,所以不能是局部变量。
        //锁住的资源千万不要是值类型。
        //lock也不能锁住string类型。

    }
    #endregion

  

2,Task中的跨线程访问控件和UI耗时任务卡顿的解决方法

  //普通方法
        private void btnUpdate_Click(object sender, EventArgs e)
        {
            Task task = new Task(() =>
             {
                 this.lblInfo.Text = "来自Task的数据更新:我们正在学习多线程!";
             });
            //task.Start();  //这样使用会报错

            //使用下面的方式解决报错的问题
            task.Start(TaskScheduler.FromCurrentSynchronizationContext());//使用任务调度器

        }

        //针对UI耗时的情况,单独重载其实并不是很好
        private void btnUpdate_Click1(object sender, EventArgs e)
        {
            Task task = new Task(() =>
            {
                //模拟耗时(这个地方会卡主)
                Thread.Sleep(5000);//界面会卡5秒钟,多线程不是万能,多线程并不是解决卡界面的。
                this.lblInfo.Text = "来自Task的数据更新:我们正在学习多线程!";
            });
            //task.Start();  //这样使用会报错

            //使用下面的方式解决报错的问题
            task.Start(TaskScheduler.FromCurrentSynchronizationContext());
        }

        //以后耗时任务都可以用这个方法
        //针对耗时任务,我们可以使用新的方法
        private void btnUpdate_Click2(object sender, EventArgs e)
        {
            this.btnUpdate.Enabled = false;
            this.lblInfo.Text = "数据更新中,请等待......";
            Task task =Task.Factory.StartNew(() =>
            {            
                Thread.Sleep(5000); //有耗时的任务,我们可以放到ThreadPool中             
            });

            //在ContinueWith中更新我们的数据
            task.ContinueWith(t =>
            {
                this.lblInfo.Text = "来自Task的数据更新:我们正在学习多线程!";
                this.btnUpdate.Enabled = true;
            },TaskScheduler.FromCurrentSynchronizationContext()); //更新操作到同步的上下文中           
         
        }

  

原文地址:https://www.cnblogs.com/baozi789654/p/14660642.html