1. 程式人生 > >WPF下多執行緒的使用方法

WPF下多執行緒的使用方法

一、WPF的執行緒

對於初學wpf的人來說,一般會把所有的程式都在一個執行緒中執行,當資料量較大,需要頻繁重新整理介面時,介面會出現卡頓的情況。

 1、當我們開啟一個WPF應用程式即開啟了一個程序,該程序中都會載入兩個重要的執行緒:一個用於呈現使用者介面,另一個用於管理使用者介面。呈現執行緒是一個在後臺執行的隱藏執行緒,因此您通常面對的唯一執行緒 就是 UI執行緒。WPF 要求將其大多數物件與 UI 執行緒進行關聯,這稱之為執行緒關聯,意味著要使用一個 WPF 物件,只能在建立它的執行緒上使用,在其他執行緒上使用它會導致引發執行時異常。

  • 一個執行緒用於處理呈現:隱藏在後臺執行
  • 一個執行緒用於管理使用者介面:接收輸入、處理事件、繪製螢幕以及執行應用程式程式碼,即UI執行緒。
  • 不管是WinForm應用程式還是WPF應用程式,實際上都是一個程序,一個程序可以包含多個執行緒,其中有一個是主執行緒,其餘的是子執行緒。

2、在 WPF 中絕大部分控制元件都繼承自 DispatcherObject,甚至包括 Application。這些繼承自 DispatcherObject 的物件具有執行緒關聯特徵,也就意味著只有建立這些物件例項,且包含了 Dispatcher 的執行緒(通常指預設 UI 執行緒)才能直接對其進行更新操作。

在 WPF 中,DispatcherObject 只能通過與它關聯的 Dispatcher 進行訪問。 例如,後臺執行緒不能更新由 UI 執行緒建立的 Label的內容。

   二、 Dispatcher類

1、在UI執行緒中有一個Dispatcher物件,管理每一個需要執行的工作項。Dispatcher會根據每個工作項的優先順序排隊。向Dispatcher列隊中新增工作項時可指定10個不同的級別。那麼問題來了,如果遇到耗時操作的時候,該操作如果依舊發生在UI執行緒中,Dispatcher 列隊中其他的需要執行的工作項都要等待,從而造成介面假死的現象。為了加快響應速度,提高使用者體驗,我們應該儘量保證Dispatcher 列隊中工作項要。所以,對於耗時操作,我們應該開闢一個新的子執行緒去處理,在操作完成後,通過向UI執行緒的Dispatcher列隊註冊工作項,來通知UI執行緒更新結果。

 Dispatcher類詳細介紹

2、Dispatcher提供兩個註冊工作項的方法:Invoke 和 BeginInvoke。

這兩個方法均排程一個委託來執行。Invoke 是同步呼叫,也就是說,直到 UI 執行緒實際執行完該委託它才返回。BeginInvoke是非同步的,將立即返回。

  • Dispatcher實際上並不是多執行緒
  • 子執行緒不能直接修改UI執行緒,必須通過向UI執行緒中的Dispatcher註冊工作項來完成
  • Dispatcher 是單例模式,暴露了一個靜態的CurrentDispatcher方法用於獲得當前執行緒的Dispatcher
  • 每一個UI執行緒都至少有一個Dispatcher,一個Dispatcher只能在一個執行緒中執行工作。

3、UI執行緒中建立的物件,如何在非UI執行緒中更新

參考:https://www.cnblogs.com/chillsrc/p/4482691.html

(1)前臺,建立物件

<Window x:Class="WpfApp1.WindowThd"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        Title="WindowThd" Height="300" Width="400">

    <Grid>

        <StackPanel>

            <Label x:Name="lblHello">歡迎你光臨WPF的世界!</Label>

            <Button Name="btnThd" Click="btnThd_Click" >多執行緒同步呼叫</Button>

            <Button Name="btnAppBeginInvoke" Click="btnAppBeginInvoke_Click" >BeginInvoke 非同步呼叫</Button>

        </StackPanel>

    </Grid>
 
</Window>

(2)後臺建立非UI執行緒,若直接更新UI執行緒的物件就會報錯


namespace WpfApp1

{

    /// <summary>

    /// WindowThd.xaml 的互動邏輯

    /// </summary>

    public partial class WindowThd : Window

    {

        public WindowThd()

        {

            InitializeComponent();     

    }

    //更新UI執行緒建立的物件
    private void ModifyUI()

    {

        // 模擬一些工作正在進行

        Thread.Sleep(TimeSpan.FromSeconds(2));

        lblHello.Content = "歡迎你光臨WPF的世界,Dispatcher";

    }

  //開啟非UI執行緒
    private void btnThd_Click(object sender, RoutedEventArgs e)

    {

        Thread thread = new Thread(ModifyUI);

        thread.Start();

    }

    }

}

程式報錯:

正確方法:

修改上面的ModifyUI()函式

方法一:使用Invoke

private void ModifyUI()

    {

        // 模擬一些工作正在進行

        Thread.Sleep(TimeSpan.FromSeconds(2));

        //lblHello.Content = "歡迎你光臨WPF的世界,Dispatcher";

        this.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate()

        {

            lblHello.Content = "歡迎你光臨WPF的世界,Dispatche  同步方法 !!";

        });

}

方法二:使用 BeginInvoke

private void btnAppBeginInvoke_Click(object sender, RoutedEventArgs e)

    {

               new Thread(() =>

        {

            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,

                new Action(() =>

                {

                    Thread.Sleep(TimeSpan.FromSeconds(2));

                    this.lblHello.Content = "歡迎你光臨WPF的世界,Dispatche 非同步方法!!"+ DateTime.Now.ToString();

                }));

        }).Start();

    }

三、實驗測試

1、簡單測試

       做了一個上位機,接收光譜儀的資料,並將線陣CMOS的資料繪製成圖,一直存在介面卡頓的問題。

ShowCCD()函式用來將串列埠接收的資料繪製成圖。在串列埠接收處理事件中,接收完一幀資料後再呼叫ShowCCD()函式繪圖。

方式一:

直接呼叫:

   ShowCCD();

測試效果:

整個介面非常卡頓,CPU佔到了61%。

方法二:使用Dispatcher.Invoke

  this.Dispatcher.Invoke(ShowCCD);

 測試效果:

頁面相對較為卡頓,CPU佔用42%.

方法三:

使用Dispatcher.BeginInvoke

this.Dispatcher.BeginInvoke((Action)delegate(){ShowCCD();});

出現報錯。

降低傳送的幀數後,可以顯示。

通過以上分析程式卡頓的原因的我在UI執行緒上接收資料,並使用Dispatcher類顯示資料,實際上還是執行在UI執行緒上,造成卡頓。

四、優化方案

1、原來的主視窗還是用來進行資料的接收及資料的運算處理,即資料處理過程還是執行在UI執行緒。

2、新建一個wpf視窗,這個視窗使用zedgraph控制元件出圖,開啟一個Timer定時器執行緒定時重新整理介面,定時20ms,即重新整理頻率為50HZ,若重新整理的速率和接收資料的速率設定為一樣快,我認為也沒有必要,太快的重新整理速率人眼也分辨不出來。

WPF下使用ZedGraph控制元件

 private void Window_Loaded(object sender, RoutedEventArgs e)
        {           
          // savepicture_th = new Thread(new ThreadStart(savepicture));
          // savepicture_th.Start();         //啟動執行緒
          //  savepicture();
            //這種定時器不是工作在UI執行緒
            System.Timers.Timer timer = new System.Timers.Timer(20);//例項化Timer類,設定間隔時間為20毫秒;  
            timer.Elapsed += new System.Timers.ElapsedEventHandler(theout); //註冊中斷事件
            timer.Start();//啟動定時器

        }


        //時間中斷事件  以50Hz的頻率重新整理介面
        public void theout(object source, System.Timers.ElapsedEventArgs e)
        {
            showChart1(MainWindow.aveccdData0);
        }

        PointPairList list2 = new PointPairList();
        private void showChart1(double[] value)
        {
            list2.Clear();//清空陣列
            zedGraphControl.GraphPane.Title.Text = "";
            zedGraphControl.GraphPane.XAxis.Title.Text = "";
            zedGraphControl.GraphPane.YAxis.Title.Text = "";
            zedGraphControl.GraphPane.XAxis.Scale.Min = 0;        //X軸最小值0
            //zedGraphControl1.GraphPane.XAxis.Scale.MaxAuto = true;    //X軸最大30
            zedGraphControl.GraphPane.XAxis.Scale.Max = 520;
            zedGraphControl.GraphPane.XAxis.Scale.MinorStep = 10;
            zedGraphControl.GraphPane.XAxis.Scale.MajorStep = 100;
            zedGraphControl.GraphPane.YAxis.Scale.Min = 0;
            zedGraphControl.GraphPane.YAxis.Scale.Max = 2000;
            zedGraphControl.GraphPane.YAxis.Scale.MinorStep = 20;
            zedGraphControl.GraphPane.YAxis.Scale.MajorStep = 500;
            zedGraphControl.AxisChange();

            for (int j = 0; j < value.Length; j++)
                list2.Add(j, value[j]);

           this.Dispatcher.Invoke(RefreshInterface);//切換到UI執行緒更新介面
           
        }

        //重新整理介面
        private void RefreshInterface()
        {
          
            zedGraphControl.GraphPane.CurveList.Clear();
            zedGraphControl.GraphPane.AddCurve("", list2, System.Drawing.Color.Red, SymbolType.None);//繪製圖表
           // zedGraphControl.AxisChange();//重新整理介面
            zedGraphControl.Refresh();
        }

 Timer定時器執行緒裡重新整理介面時,必須使用Dispatcher類,若在Timer定時器執行緒裡直接重新整理介面就會報錯,這違反了WPF下用其他執行緒重新整理介面的規則。

如果使用定時器DispatcherTimer則不需要使用Dispatcher類,因為DispatcherTimer就是在UI執行緒中,僅是猜測未經實驗。

測試效果:介面完全不卡頓,CPU佔用率10%。