zoukankan      html  css  js  c++  java
  • 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
Copyright © 2011-2022 走看看