在MVVM中的多線程問題

最近在用MVVM模式寫應用的時候和線程打交道很多,也遇到一些問題,引起一些思考,這里和大家分享一下。

MVVM設計模式

**Model-View-ViewModel (MVVM) **在Windows平臺開發(fā)可以使用MVVM Light Toolkit進行,簡化開發(fā)過程,構建松耦合的應用程序。
MVVM Light通過:

  • 依賴注入(DI)/控制反轉(IOC)的方式創(chuàng)建ViewModel
  • 使用Messenger實現(xiàn)消息通知
  • 使用RelayCommand替代事件處理程序
    實現(xiàn)了View和ViewModel的分離關系。

客戶端上的多線程和調度

如今在移動客戶端上處理多線程以及線程間的通信顯得非常重要。在.Net平臺上構建應用程序和框架,多線程也是長談的話題,每個完整的應用都要啟動多個后臺線程并對他們進行管理,對于計算能力較低,資源受限的平臺,例如移動端設備和嵌入式設備,在這些平臺上處理好線程對UX的流暢顯得尤為重要。

Windows 10 Mobile平臺最早的版本W(wǎng)indows Phone7上,對于長列表的滾動流暢非常困難,但在其后的版本中,專門使用后臺線程處理,不在影響主線程,實現(xiàn)了滾動的流暢。本文中,我會回顧基于XAML編寫界面的程序中處理線程的一般方式。

線程

線程(Thread),在操作系統(tǒng)中被視為最小的執(zhí)行單位。每個程序至少都有一個主線程,新的線程可以在代碼中顯式啟動,多數(shù)情況下,新線程的啟動主要是為了執(zhí)行或者等待某個操作,也不會導致程序的其他部分被阻塞。耗時操作主要是計算密集型操作、磁盤I/O和網(wǎng)絡傳輸。由于這些操作在現(xiàn)代應用程序中非常常見,應用也日益多線程化。

同步和異步

在為Windows 10 UWP編寫的應用中,異步操作已經(jīng)是家常便飯了。例如,UWP中對文件的操作都是以異步的方式進行的。
下面是在WPF中同步讀取文件方式:

public string ReadFile(FileInfo file)
{
  using (var reader = new StreamReader(file.FullName))
  {
    return reader.ReadToEnd();
  }
}

而在UWP中,和上面效果等同的異步操作:

public async Task<string> ReadFile(IStorageFile file)
{
  var content = await FileIO.ReadTextAsync(file);
  return content;
}

我們注意到,這里多了兩個關鍵字awaitasync ,這里要感謝微軟爸爸對C#的不斷完善,這兩個關鍵字使新的程序代碼的可讀性提高,方便了程序員編寫異步方法。
同步方式和異步方式的主要區(qū)別在于,同步方法如果正在讀取的文件較大,可能會阻塞主線程,導致UX變得糟糕。而異步的方法將在后臺線程處理。

線程間通信

當一個線程要和另一個線程通行時,例如訪問另一個線程的對象,我們需要采取一些防范措施。看下面的代碼:

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            await Change();
        }
        private async Task Change()
        {
            await Task.Run(() =>
            {
                this.Count.Text=DateTime.Now.ToLocalTime().ToString();
            });
        }

使用Task.Run()方法新開一個線程,訪問一個XAML中的文本框控件的Text屬性。
如果真的用這段代碼運行程序,點擊按鈕時,應用程序會崩潰退出。
我們來分析一下。在創(chuàng)建對象時,這個操作發(fā)生在調用構造函數(shù)的線程上。對于UI控件,創(chuàng)建控件對象的操作發(fā)生在主線程上,也可以說是UI線程。因此,所有UI控件都是屬于主線程的,當我們新開一個線程去訪問UI線程擁有的對象時就會發(fā)生跨線程訪問問題。這個操作會引發(fā)一個異常:

跨線程訪問異常

注意到,異常信息提示“應用程序調用一個已為另一線程整理的接口”。

那么問題來了,難道我們真的不能在非UI線程去更新UI控件么?

線程調度

要使代碼按照我們設想的運行,我們需要讓新線程通過聯(lián)系主線程,通過主線程的調度程序更新UI控件。將上面的代碼修改為:

            await Task.Run(async () =>
            {
                await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                 {
                     this.Count.Text = DateTime.Now.ToLocalTime().ToString();
                 });
            });

注意到這里用到了Dispatcher屬性

Dispatcher屬性摘要
獲取此對象所關聯(lián)的 CoreDispatcher。 CoreDispatcher 表示即便代碼由非 UI 線程發(fā)起也可訪問 UI 線程上的 DependencyObject 的設備。

這個屬性提供對其所有者調度程序的訪問。所以所有擁有這個屬性的對象,理論上都能提供跨線程訪問的服務。
這個屬性源自Windows.UI.Xaml.DependencyObject對象,也就是說,所有有依賴屬性都繼承自這個對象,那么所有的UI控件都是可以通過Dispatcher實現(xiàn)跨線程訪問的。

繼承關系

MVVM中的跨線程調度

當我們在ViewModel中執(zhí)行后臺線程操作時,情況有所不同。通常,VM不會去繼承前面提到的DependencyObject,也就沒有Dispatcher屬性。
當我們把一個UI控件的屬性綁定到VM中的一個屬性,我們的程序通過更改VM的屬性來改變UI控件屬性。注意到,我們在使用MVVM Light的時候,每個VM都要繼承一個叫做ViewModelBase的對象,這個對象實現(xiàn)了INotifyPropertyChanged接口,這個接口的方法引發(fā)PropertyChanged事件,在這里實現(xiàn)通知UI屬性改變的效果。
當我們按先前的方法,新開一個線程,用這個線程去更改VM中綁定到UI控件屬性的屬性時,你會發(fā)現(xiàn)也引發(fā)了跨線程訪問異常,程序異常退出。
我們可以知道,即使我們通過數(shù)據(jù)綁定來實現(xiàn)這樣的目的也是不能實現(xiàn)的。
因此,我們需要一種方式來訪問主線程。在MVVM Light中,為我們實現(xiàn)了一種方式。當你的項目使用了MVVM Light之后,你嘗試在VM的類中輸入DispatcherHelper,再用一下智能感知,你會發(fā)現(xiàn)在MVVM Light中存在這個對象。這個類所做的就是將主線程的調度程序保存在靜態(tài)屬性中,公開了讓我們跨線程訪問等一些實用方法。為了正常使用這些功能,我們需要在主線程初始化這個類,最好是在應用程序初始化時進行。
我們能夠想到,App.xaml.cs這個文件的OnLaunched方法是在程序開始運行是執(zhí)行的,我們要把DispatcherHelper.Initialize();放在這個方法的最后以實現(xiàn)初始化,之后就能在VM中使用了。

            DispatcherHelper.CheckBeginInvokeOnUI(() =>
            {
                //訪問VM的屬性
            });

這里推薦使用他的CheckBeginInvokeOnUI方法去執(zhí)行而不是他的UIDispatcher屬性。這個方法首先執(zhí)行檢查,檢查調用方是否已經(jīng)在主線程上,如果在就直接執(zhí)行后面的委托,如果不是就去執(zhí)行調度。

那這篇文章就到這里啦,希望大家在MVVM的使用中一切順利~

本文參考MVVM : Multithreading and Dispatching in MVVM Applications

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容