C#多線程彙總

一、Thread

            Console.WriteLine($"主線程{Thread.CurrentThread.ManagedThreadId}start");
            Thread thread1 = new Thread(() =>
            {
                Console.WriteLine($"線程{Thread.CurrentThread.ManagedThreadId}啓動");
                Thread.Sleep(5000);
                Console.WriteLine($"線程{Thread.CurrentThread.ManagedThreadId}休眠結束");
                /*
                 * 讓出當前線程時間片的剩餘部分,用於其他的線程,如果操作系統轉而執行了另一個線程,則返回true,否則false
                 * 該方法不是百分百成功的,和線程優先級,操作系統的線程調度,其他線程的情況有關
                 */
                Console.WriteLine($"讓出線程資源是否成功:{Thread.Yield()}");
                Console.WriteLine($"線程{Thread.CurrentThread.ManagedThreadId}Yield()結束");
            });
            Thread thread2 = new Thread(() =>
            {
                Console.WriteLine($"線程{Thread.CurrentThread.ManagedThreadId}啓動");
                Thread.Sleep(5000);
                Console.WriteLine($"線程{Thread.CurrentThread.ManagedThreadId}結束");
            })
            {
                /*
                 * 當被設置爲false時,線程不會隨着程序的運行結束而結束,而是等到自己處理的任務處理完成才結束,默認是true
                 * 當被設置爲true時,程序運行結束後,不管任務是否完成,該線程都會被結束
                 */
                IsBackground = true
            };
            Console.WriteLine($"線程thread2是否是線程池線程:{thread2.IsThreadPoolThread}");
            thread1.Start();
            /*
             * 線程不允許被重複啓動,否則會拋異常
             */
            //thread1.Start();
            thread2.Start();

            /*
             * 在調用此方法的線程上引發 ThreadAbortException,以開始終止此線程的過程。
             * 調用此方法通常會終止線程。
             * 注意:.net core不支持
             */
            //thread2.Abort();

            //阻塞當前線程,直到線程thread1執行結束,該方法要求thread1必須被啓動,否則拋異常
            thread1.Join();
            Console.WriteLine($"主線程{Thread.CurrentThread.ManagedThreadId}end");

以上代碼時創建一個線程常用的方法和要注意的地方,線程相關的方法還有很多。具體可以參考官方文檔:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.thread?view=netcore-3.1

二、線程池

直接new線程的方式,不是一個很好地利用線程的方式,對於一個較大的項目,如果多人合作,每個人都在自己的子模塊裏面隨意new新線程,就很容易導致線程氾濫,爲了統一地管理線程池,合理地使用線程資源,引入了線程池的概念。

在c#裏面線程池類是ThreadPool,下面是一個示例:

    class Program
    {
        /// <summary>
        /// 每個進程都有一個線程池。 從 .NET Framework 4 開始,進程的線程池的默認大小取決於若干因素,例如虛擬地址空間的大小。
        /// </summary>
        public static void Main()
        {
            /*
             * 獲取電腦的cpu線程數量,我的電腦是6核12線程的
             */
            int count = Environment.ProcessorCount;
            Console.WriteLine($"當前系統的處理器線程數:{count}");
            ThreadPool.GetMinThreads(out int a, out int b);
            Console.WriteLine($"線程池默認最小工作線程數:{a}");
            Console.WriteLine($"線程池默認最小i/o線程數:{b}");
            ThreadPool.GetMaxThreads(out a, out b);
            Console.WriteLine($"線程池默認最大工作線程數:{a}");
            Console.WriteLine($"線程池默認最大i/o線程數:{b}");
            /*
             * 設置線程池的最大線程數,如果設置的值小於cpu的線程數量,則返回false,設置無效
             * 這裏有兩個參數,第一個是workerThreads,第二個是completionPortThreads
             * workerThreads表示工作線程數,就是我們這個demo現在用的線程
             * completionPortThreads表示一步i/o線程數
             * 線程池是全局靜態的,所以更改線程池中線程的最大數量時,可能會對你使用的代碼庫產生影響,所以不要隨意修改
             * 將線程池大小設置得太大可能會導致性能問題。 如果同時執行的線程太多,任務切換開銷會成爲一個重要因素。
             */
            bool result = ThreadPool.SetMaxThreads(count, count);
            Console.WriteLine($"設置線程池最大線程數是否成功:{result}");
            /*
             * 進入線程池排隊,如果有空閒線程,則按順序執行
             * 進入排隊後無法取消
             */
            for (int i = 0; i < 20; i++)
            {
                
                ThreadPool.QueueUserWorkItem(ThreadProc,i);
            }
            Console.WriteLine("主線程開始等待");
            /*
             * 線程池裏的線程都是後臺線程,主線程退出後,會終止執行並退出
             * 該循環是等待線程池所有任務執行結束的一種方法
             * 由於線程池是全局的,所以線程池沒有等待所有線程結束的方法,
             * 因爲我們是無法確定在一個大的系統裏面是否有其他的模塊在使用線程池
             */           
            while (true)
            {
                ThreadPool.GetAvailableThreads(out int avail1, out int avail2);
                if (avail1 == count) break;
            }

            Console.WriteLine("主線程退出");
        }
        static void ThreadProc(Object stateInfo)
        {
            Console.WriteLine($"子線程執行,線程號:{stateInfo},線程id:{Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
        }
    }

注意:當線程池重用某個線程時,它不會清除線程本地存儲區中的數據或用 ThreadStaticAttribute 特性標記的字段中的數據。因此,當某個方法檢查線程本地存儲區或用 ThreadStaticAttribute 特性標記的字段時,它所找到的值可能會從先前使用線程池線程的過程中遺留。

參考下面的代碼:

    class Program
    {
        [ThreadStatic] static int flag = 0;
        public static void Main()
        {
            int count = Environment.ProcessorCount;
            bool result = ThreadPool.SetMaxThreads(count, count);
            
            for (int i = 0; i < 20; i++)
            {
                ThreadPool.QueueUserWorkItem(ThreadProc,i);
            }
            Console.WriteLine("主線程開始等待");        
            while (true)
            {
                ThreadPool.GetAvailableThreads(out int avail1, out int avail2);
                if (avail1 == count) break;
            }

            Console.WriteLine("主線程退出");
        }
        static void ThreadProc(Object stateInfo)
        {
            Console.WriteLine($"子線程執行,線程號:{stateInfo},flag:{++flag}");
            Thread.Sleep(1000);
        }
    }

這段代碼的輸出結果爲:

主線程開始等待
子線程執行,線程號:10,flag:1
子線程執行,線程號:9,flag:1
子線程執行,線程號:8,flag:1
子線程執行,線程號:7,flag:1
子線程執行,線程號:3,flag:1
子線程執行,線程號:1,flag:1
子線程執行,線程號:0,flag:1
子線程執行,線程號:11,flag:1
子線程執行,線程號:6,flag:1
子線程執行,線程號:5,flag:1
子線程執行,線程號:4,flag:1
子線程執行,線程號:2,flag:1
子線程執行,線程號:18,flag:2
子線程執行,線程號:15,flag:2
子線程執行,線程號:14,flag:2
子線程執行,線程號:19,flag:2
子線程執行,線程號:16,flag:2
子線程執行,線程號:13,flag:2
子線程執行,線程號:12,flag:2
子線程執行,線程號:17,flag:2
主線程退出

ThreadStatic標記的變量不會被線程共享,而是在每個線程裏面生成一個單獨的拷貝,所以第一批線程池啓動後,所有的輸出結果都是1,當第二批線程開始啓動後,由於重複使用的線程池不清楚此變量數據,導致第二批的值都是2。

三、Task

        static void Main()
        {
            static void action(object obj)
            {
                Console.WriteLine("Task={0}, obj={1}, Thread={2}",
                    Task.CurrentId, obj,
                    Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(2000);
                Console.WriteLine($"{obj}結束");
            }
            Console.WriteLine($"主線程id:{Thread.CurrentThread.ManagedThreadId}");
            /*
             * 創建一個task,但不啓動
             * 一般很少用構造方法創建task實例,都是使用 Task.Run 和 TaskFactory.StartNew
             * 它的唯一好處是啓動和創建分離
             */
            Task t1 = new Task(action, "t1");

            /*
             * 創建一個task,同時異步啓動
             */
            Task t2 = Task.Factory.StartNew(action, "t2");
            /*
             * 阻塞當前線程,直到t2結束
             */
            t2.Wait();

            t1.Start();
            t1.Wait();

            /*
             * 異步啓動一個task
             */
            Task t3 = Task.Run(() => {
                Console.WriteLine("Task={0}, obj={1}, Thread={2}",
                                  Task.CurrentId, "t3",
                                   Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(2000);
                Console.WriteLine("t3運行結束");
            });

            Task t4 = new Task(action, "t4");
            // 同步啓動一個task
            t4.RunSynchronously();
            /*
             * 這裏仍然是可以等待的,雖然t4運行結束後代碼纔會執行到這裏
             */
            t4.Wait();
            CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
            Task t5 = Task.Factory.StartNew(()=> {
                Console.WriteLine("t5開始");
                Thread.Sleep(2500);
                /*
                 * 線程是不可以被取消的,只是採取一種方式去中斷,
                 * 調用cancellationTokenSource.Cancel()並不會被取消,
                 * 只是把標記改了
                 */
                if (cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Token.ThrowIfCancellationRequested();
                Console.WriteLine($"t5被執行,線程id:{Thread.CurrentThread.ManagedThreadId}");
            }, cancellationTokenSource.Token);
            new Thread(()=> {
                Thread.Sleep(100);
                Console.WriteLine($"t5是否可以被取消:{cancellationTokenSource.Token.CanBeCanceled}");
                cancellationTokenSource.Cancel();
            }).Start();

            /*
             * 等待500ms內任一任務運行結束或者取消標記發出取消等待的操作
             */
            try
            {
                Task.WaitAny(new List<Task> { t5 }.ToArray(), 2000, cancellationTokenSource.Token);
            }
            catch {

            }
            /*
             * 等待所有的任務運行結束
             */
            Task.WaitAll(new List<Task> { t1, t2, t3, t4}.ToArray());
            Task t6 = Task.Factory.StartNew(async ()=> {
                try
                {
                    /*
                     * 下面的代碼不重要,所以讓出時間碎片,
                     * 讓下面的代碼重新在線程池排隊,等待線程池執行
                     * 使用await強制異步完成方法
                     */
                    await Task.Yield();
                    Thread.Sleep(2000);
                    //throw new Exception();
                }
                catch { throw; }
            });
            /*
             * 創建一個等待其他任務全部完成的任務
             */
            Task t = Task.WhenAll(new List<Task> { t6 }.ToArray());
            try
            {
                t.Wait();
            }
            catch {
            }

            if (t.Status == TaskStatus.RanToCompletion)
                Console.WriteLine("所有任務完成");
            //如果某個任務拋異常了,那麼下面的條件成立
            else if (t.Status == TaskStatus.Faulted)
                Console.WriteLine("有任務執行失敗了");

            Console.WriteLine("主線程結束");
        }
    }

task的ContinueWith:

            //等待任務結束後創建一個新的任務,因此這兩個任務不在一個線程
            Task<bool> task1 = Task.Factory.StartNew(()=> {
                Thread.Sleep(1000);
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                return true;
            });
            task1.ContinueWith(task=> {
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                Console.WriteLine(task.Result);
            });

task的本質:threadpool。看下面的代碼:

    class Program
    {
        static void Main()
        {
            List<Task> tasks = new List<Task>();
            Console.WriteLine("ThreadPool最小設置爲100");
            //ThreadPool.SetMinThreads(100, 100);
            for (int i = 0; i < 50; i++)
            {
                tasks.Add(Task.Factory.StartNew(() =>
                {
                    Console.WriteLine($"線程id:{Thread.CurrentThread.ManagedThreadId}");
                    Thread.Sleep(5000);
                }));
            }
            Task.WaitAll(tasks.ToArray());
        }
    }

如果設置了線程池的最小線程數爲100,那麼上面的50個task會立即全部被執行,如果不設置,則逐步執行。這說明task的本質就是利用默認線程池的,同時對線程的調度是逐步增加的,連續多次運行併發線程,會提高佔用的線程數,而等若干秒不運行,線程數又會降低。

四、await/async

        /// <summary>
        /// await/async 不創建新的線程,它更像是回調的語法糖
        /// 讓代碼讀起來更流暢,如果要開啓一個線程,仍然需要使用thread或者task等方法
        /// 因此這倆關鍵字通常和task結合使用,因爲task會開啓一個線程
        /// </summary>
        static async Task Main()
        {
            Console.WriteLine("start");
            Console.WriteLine("程序開始時的線程號:" + Thread.CurrentThread.ManagedThreadId);
            var b= TestAsync();
            Console.WriteLine("TestAsync()調用結束");
            /*
             * 程序一旦遇到await關鍵字,後續的代碼將在一個新線程裏面進行
             * 這個新線程是由這個TestAsync內部創建的
             * 這樣調用Main的地方就不需要等待了,可以在調用main之後繼續調用其他的方法,
             * 除非調用者在main方法調用前加了await
             */
            Console.WriteLine(await b);
            Console.WriteLine("程序退出時的線程號:"+Thread.CurrentThread.ManagedThreadId);
            /*
             * 使用Result和await是一樣的,都是等待task執行結束返回結果
             */
            //Console.WriteLine(b.Result);
        }
        private async static Task<bool> TestAsync()
        {
            Console.WriteLine("TestAsync方法內部開始時的線程號:" + Thread.CurrentThread.ManagedThreadId);
            //這裏纔會真正開啓線程
            await Task.Delay(5000);
            Console.WriteLine("TestAsync方法內部await之後的的線程號:" + Thread.CurrentThread.ManagedThreadId);
            return true;
        }

這段代碼的執行結果:

start
程序開始時的線程號:1
TestAsync方法內部開始時的線程號:1
TestAsync()調用結束
TestAsync方法內部await之後的的線程號:5
True
程序退出時的線程號:5

五、ConfigureAwait

在.net core裏面不在有用,但在.net framework裏面有用,可能會引起死鎖。

六、Parallel

        static void Main()
        {
            //設置系統最小線程池大小
            ThreadPool.SetMinThreads(100,100);
            /*
             * 這將導致系統同時開20個線程去調用test方法
             * 不包含i=20的情況
             * 該方法是基於線程池實現的,所以要設置線程池最小線程數,
             * 否則不會一次全部開始,而是逐步開始,像task對線程池的利用一樣
             * 該方法會阻塞當前線程,直到所有的方法執行完成
             * 該方法用於大數據的並行計算
             */
            var v= Parallel.For(0,20,test);
            Console.WriteLine("end");
            Console.ReadLine();
        }
        private static void test(int i)
        {
            Thread.Sleep(5000);
            Console.WriteLine($"根據i計算的值:{i*i},線程號:{Thread.CurrentThread.ManagedThreadId}");
        }

前面的內容是線程的一些基本知識,涉及到多線程的時候有兩個比較重要的問題,一個是線程安全,一個是線程同步/等待

1)線程安全

    class Program
    {
        private volatile static int volatileInt = 0;
        private static int commonInt1 = 0;
        private static int commontInt2 = 0;
        static int commontInt3 = 0;
        [ThreadStaticAttribute] static int threadStaticAttributeInt = 0;
        private static readonly object lockObj = new object();
        static void Main()
        {
            ThreadPool.SetMinThreads(100,100);
            List<Task> tasks = new List<Task>();
            for (int i = 0; i < 20; i++)
            {
                //ThreadPool.QueueUserWorkItem(test,null);
                tasks.Add(Task.Factory.StartNew(test));
            }
            //Task.WaitAll(tasks.ToArray());
            Thread.Sleep(5000);
            Console.WriteLine($"volatileInt:{volatileInt}\r\n" +
                $"commonInt1:{commonInt1}\r\n" +
                $"commontInt2:{commontInt2}\r\n" +
                $"commontInt3:{commontInt3}\r\n" +
                $"threadStaticAttributeInt:{threadStaticAttributeInt}");

        }
        private static void test()
        {
            Thread.Sleep(10);
            //線程內存可見,但不具備原子性,因爲++操作是非原子性操作,所以結果可能不是20
            volatileInt++;
            //線程不安全,最終結果不確定
            commonInt1++;
            //線程安全,最終結果是20
            lock (lockObj)
            {
                commontInt2++;
            }
            /*
             * 該類爲多個線程共享的變量提供原子操作
             * 內存可見,是線程安全的,最終結果是20
             * 該類提供大量的原子性操作內存的方法,比如可用於單例模式的Interlocked.Exchange
             * https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=netframework-4.8
             */
            Interlocked.Increment(ref commontInt3);
            //每個內存擁有自己的拷貝,所有的線程它的最終結果都是1
            threadStaticAttributeInt++;
            Thread.Sleep(2000);
        }
    }

另外還有下面幾個方法,都是值得關注的:

            Thread.VolatileWrite();
            Volatile.Write();

上面的方法的實現原理是使用了Interlocked.MemoryBarrier:

            new Thread(() =>{
                while (flag)
                {
                    //如果不加這句,該循環將會一直執行下去
                    /*
                     * Synchronizes memory access as follows:
                     * The processor that executes the current thread cannot reorder instructions
                     * in such a way that
                     * memory accesses before the call to MemoryBarrier()
                     * execute after
                     * memory accesses that follow the call to MemoryBarrier().
                     * 同步內存訪問按照下面的方式進行:
                     * 正在執行當前線程的處理器不可以用這樣的方式重排指令:
                     * 調用Interlocked.MemoryBarrier()方法之前的內存存取操作
                     * 放在
                     * 調用Interlocked.MemoryBarrier()之後的內存存取操作
                     * 後執行
                     */
                    Interlocked.MemoryBarrier();
                }
                Console.WriteLine("子線程結束");
            }).Start();
            Thread.Sleep(1000);
            new Thread(() => {
                flag = false;
            }).Start();
        }

Interlocked.MemoryBarrier的作用相當於主動刷新一下緩存

所謂內存可見是指當子線程更新了某個公共的變量,會立即更新主線程的內存,同時通知其他的子線程更新自己線程的臨時緩存。

所謂原子性就是對於一個公共變量的操作是線程安全的,比如a++,分成兩步:

a)讀取主內存a的值到緩存

b)對a的值+1

c)更新緩存。

d)如果加了volatile,則更新主內存,同時通知其他子線程,更新緩存。

以上步驟可能會因爲併發量很大導致寫入的數據和預期不一致,因爲a步驟到b步驟有時間間隔。

2)線程等待

如果你使用task方法,則不用擔心線程等待的問題,因爲task提供了await關鍵字用於線程等待。但是如果你自定義線程,那麼可能就無法使用await方法進行線程等待了。看下面的代碼:

        /*
         * 用於線程等待,調用WaitOne的代碼會阻塞,直到有線程調用了set方法
         * 如果傳入的是true,表示已經發過信號,那麼就不會阻塞了
         * 手動和自動的區別在於前者需要手動調用Reset方法來重置信號狀態
         */
        static AutoResetEvent autoResetEvent = new AutoResetEvent(false);
        static ManualResetEvent manualResetEvent = new ManualResetEvent(false);
        /*
         * 第一個參數表示初始可用的信號數量
         * 第二個參數表示最大可用信號數量
         * 如果第一個參數設置爲0,那麼這個信號量容器一開始就沒有可用的信號可用,
         * 必須釋放了,其他的線程才能拿到信號
         * 使用這個參數來控制其他線程啓動的時機
         */
        static Semaphore semaphore = new Semaphore(5, 5);
        
        static void Main()
        {
            
            Thread thread1 = new Thread(()=> {
                Console.WriteLine("thread1 開始");
                Thread.Sleep(5000);
                Console.WriteLine("thread1 結束");
                manualResetEvent.Set();
                
            });
            Thread thread2 = new Thread(()=> {
                manualResetEvent.WaitOne();
                Console.WriteLine("thread2 開始");
                Console.WriteLine("thread2 結束");
            });
            thread2.Start();
            thread1.Start();
            //阻塞當前線程,直到thread2執行結束
            thread2.Join();

            //重置信號狀態,否則線程4將不會等待
            var b= manualResetEvent.Reset();

            Thread thread3 = new Thread(() => {
                Console.WriteLine("thread3 開始");
                Thread.Sleep(5000);
                Console.WriteLine("thread3 結束");
                manualResetEvent.Set();

            });
            Thread thread4 = new Thread(() => {
                manualResetEvent.WaitOne();
                Console.WriteLine("thread4 開始");
                Console.WriteLine("thread4 結束");
            });
            thread4.Start();
            thread3.Start();
            //thread4.Join();

            
            for (int i = 0; i < 5; i++)
            {
                ThreadPool.QueueUserWorkItem(test,i);
            }
            Thread thread5 = new Thread(()=> {
                //如果想等五個線程都結束,那麼調用五次這個方法
                semaphore.WaitOne();
                semaphore.WaitOne();
                semaphore.WaitOne();
                semaphore.WaitOne();
                semaphore.WaitOne();
                Console.WriteLine("thread5啓動");
                Console.WriteLine("thread5結束");
                semaphore.Release(5);
            });
            Thread.Sleep(100);
            thread5.Start();
            thread5.Join();
            
        }

        private static void test(object state)
        {
            semaphore.WaitOne();

            Console.WriteLine($"線程池線程{state}啓動");
            Thread.Sleep(5000);
            Console.WriteLine($"線程池線程{state}結束");
            semaphore.Release();
        }

    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章