C#多線程開發:並行、並發與異步編程


概述

現代程序開發過程中不可避免會使用到多線程相關的技術,之所以要使用多線程,主要原因或目的大致有以下幾個:

1、 業務特性決定程序就是多任務的,比如,一邊采集數據、一邊分析數據、同時還要實時顯示數據;

2、 在執行一個較長時間的任務時,不能阻塞UI界面響應,必須通過后台線程處理;

3、 在執行批量計算密集型任務時,采用多線程技術可以提高運行效率。

傳統使用的多線程技術有:

  1. Thread & ThreadPool
  2. Timer
  3. BackgroundWorker

目前,這些技術都不再推薦使用了,目前推薦采用基於任務的異步編程模型,包括並行編程和Task的使用。

Concurrency並發和Multi-thread多線程不同

你在吃飯的時候,突然來了電話。

  1. 你吃完飯再打電話,這既不並發也不多線程
  2. 你吃一口飯,再打電話說一句話,然后再吃飯,再說一句話,這是並發,但不多線程。
  3. 你有2個嘴巴。一個嘴巴吃飯,一個嘴巴打電話。這就是多線程,也是並發。

並發:表示多個任務同時執行。但是有可能在內核是串行執行的。任務被分成了多個時間片,不斷切換上下文執行。

多線程:表示確實有多個處理內核,可同時處理多個任務。

 

一、並發編程:

  使用ThreadPool輪詢並發

方法是使用一個List(或其他容器)把所有的對象放進去,創建一個線程(為了防止UI假死,由於這個線程創建后會一直執行切運算密集,所以使用TheadPool和Thread差別不大),在這個線程中使用foreach(或for)循環依次對每個對象執行ReceiveData方法,每次執行的時候創建一個線程池線程來執行。代碼如下:

使用Task輪詢並發

方法與ThreadPool類似,只是每次創建線程池線程執行ReceiveData方法時是通過Task創建的線程。代碼如下所示:

 

 

二、並行編程:

private static bool IsPrimeNumber(int number)
        {
            if (number < 1)
            {
                return false;
            }

            if (number == 1 && number == 2)
            {
                return true;
            }

            for (int i = 2; i < number; i++)
            {
                if (number % i == 0)
                {
                    return false;
                }
            }

            return true;
        }

 如果不采用並行編程,常規實現方法:

  for (int i = 1; i <= 10000; i++)
            {
                bool b = IsPrimeNumber(i);             
                Console.WriteLine($"{i}:{b}");
            }

采用並行編程方法

 Parallel.For(1, 10000, x=> 
           {
                bool b = IsPrimeNumber(x);              
                Console.WriteLine($"{i}:{b}");
            })

Parallel類還有一個ForEach方法,使用和For類似。

三、 線程(或任務)同步

線程同步還有一個比較好的辦法就是采用ManualResetEvent 和AutoResetEvent :

public partial class Form1 : Form
    {  
        private ManualResetEvent manualResetEvent = new ManualResetEvent(false);

        public Form1()
        {
            InitializeComponent();
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
            this.btnStart.Enabled = false;
            this.btnStop.Enabled = true;

            manualResetEvent.Reset();
            Task.Run(() => WorkThread());
        }

        private void btnStop_Click(object sender, EventArgs e)
        {
            manualResetEvent.Set();
        }

        private void WorkThread()
        {
            for (int i = 0; i < 100; i++)
            {
                this.Invoke(new Action(() =>
                {
                    this.progressBar.Value = i;
                }));

               if(manualResetEvent.WaitOne(1000))
                {
                    break;
                }
            }

            this.Invoke(new Action(() =>
            {
                this.btnStart.Enabled = true;
                this.btnStop.Enabled = false;
            }));            
        }
    }

 

采用WaitOne來等待比通過Sleep進行延時要更好,因為當執行manualResetEvent.WaitOne(1000)時,如果manualResetEvent沒有調用Set,該方法在等待1000ms后返回false,如果期間調用了manualResetEvent的Set方法,該方法會立即返回true,不用等待剩下的時間。

采用這種同步方式優於采用通過內部字段變量進行同步的方式,另外盡量采用ManualResetEvent 而不是AutoResetEvent 

 

四、異步編程模型(await、async)

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

        private async void btnStart_ClickAsync(object sender, EventArgs e)
        {
            this.btnStart.Enabled = false;

            var result = await DoSomethingAsync();
            if(result)
            {
                this.picShow.BackColor = Color.Green;
            }
            else
            {
                this.picShow.BackColor = Color.Red;
            }

            await Task.Delay(1000);
            
            this.picShow.BackColor = Color.White;
            this.btnStart.Enabled = true;
        }

        private async Task<bool> DoSomethingAsync()
        {
            await Task.Run(() =>
            {
                Thread.Sleep(5000);                
            });
            return true;
        }
    }

 

五、 多線程環境下的數據安全

  目標:我們要向一個字典加入一些數據項,為了增加效率,我們使用了多個線程

  

private async static void Test1()
        {
            Task.Run(() => AddData());
            Task.Run(() => AddData());
            Task.Run(() => AddData());
            Task.Run(() => AddData()); 
        }

        private static void AddData()
        {
            for (int i = 0; i < 100; i++)
            {
                if(!Dic.ContainsKey(i))
                {
                    Dic.Add(i, i.ToString());
                }

                Thread.Sleep(50);
            }
        }

 

向字典重復加入同樣的關鍵字會引發異常,所以在增加數據前我們檢查一下是否已經包含該關鍵字。以上代碼看似沒有問題,但有時還是會引發異常:“已添加了具有相同鍵的項。”原因在於我們在檢查是否包含該Key時是不包含的,但在新增時其他線程加入了同樣的KEY,當前線程再增加就報錯了。

【注意:也許你多次運行上述程序都能順利執行,不報異常,但還是要清楚認識到上述代碼是有問題的!畢竟,程序在大部分情況下都運行正常,偶爾報一次故障才是最頭疼的事情。】

上述問題傳統的解決方案就是增加鎖機制。對於核心的修改代碼通過鎖來確保不會重入。

private object locker4Add=new object();
        private static void AddData()
        {
            for (int i = 0; i < 100; i++)
            {
                lock (locker4Add)
                {
                    if (!Dic.ContainsKey(i))
                    {
                        Dic.Add(i, i.ToString());
                    }
                }

                Thread.Sleep(50);
            }
        }

更好的方案是使用線程安全的容器:ConcurrentDictionary

private static ConcurrentDictionary<int, string> Dic = new ConcurrentDictionary<int, string>();
      
        private async static void Test1()
        {
            Task.Run(() => AddData());
            Task.Run(() => AddData());
            Task.Run(() => AddData());
            Task.Run(() => AddData());   
        }

        private static void AddData()
        {
            for (int i = 0; i < 100; i++)
            {
                Dic.TryAdd(i, i.ToString());
                Thread.Sleep(50);
            }
        }

  

 


免責聲明!

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



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