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()); //更新操作到同步的上下文中           
         
        }

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM