小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

淺談Excel開發(fā):十 Excel 開發(fā)中與線程相關(guān)的若干問題

 法效天地 2014-01-29

    采用VSTO或者Shared Add-in等技術(shù)開發(fā)Excel插件,其實(shí)是在與Excel提供的API在打交道,Excel本身的組件大多數(shù)都是COM組件,也就是說通過Excel PIA來與COM進(jìn)行交互。這其中會(huì)存在一些問題,這些問題如果處理不好,通常會(huì)導(dǎo)致在運(yùn)行的時(shí)候會(huì)拋出難以調(diào)試的COM異常,從而導(dǎo)致我們開發(fā)出的Excel插件的不穩(wěn)定。

    和普通的WinForm程序一樣,Excel也是一種STA(Single Thread Apartment)線程的應(yīng)用程序,Excel插件是寄宿在Excel中運(yùn)行的,這也就意味著插件也是一種STA線程的應(yīng)用程序。插件在操作Excel的時(shí)候,如果是在Excel的主線程中,可以直接獲取Excel對象進(jìn)行操作,比如寫入單元格值,對單元格進(jìn)行格式化等操作。但是通常,我們會(huì)在多線程或者后臺(tái)工作線程中去處理一系列復(fù)雜的數(shù)據(jù)或者邏輯,待處理完成獲得結(jié)果之后,再像WinForm那樣,回到UI線程中,去更新界面信息,對于Excel插件來說,就是回到Excel的主線程上來,然后再更新界面。但是Excel又是一種不同于一般Winform 類型的STA,它是COM并且Excel插件是寄宿在其上的,所以還有一些需要注意的地方。

    本文首先介紹什么是STA應(yīng)用程序及其工作原理,然后介紹一般的Winform程序的界面刷新邏輯,以及在這其中非常重要的一個(gè)名為SynchronizationContext對象,最后介紹在Excel插件中如何獲取Excel主線程,以及這其中需要注意的地方。

    Excel插件的最難處理的地方在于其應(yīng)用程序的穩(wěn)定性,了解了Excel中的線程以及其機(jī)制對增強(qiáng)系統(tǒng)的穩(wěn)定性會(huì)有很大的幫助。

 

1. STA(Single Thread Apartment)

    COM組件的線程模型被稱之為Apartment模型,即COM對象初始化時(shí)其執(zhí)行上下文(Execution Context),他要么和單個(gè)線程關(guān)聯(lián)STA(Single Thread Apartment ) 要么和多個(gè)線程關(guān)聯(lián)MTA(Multi Thread Apartment)。

    通常COM對象為了保護(hù)其自身維護(hù)的數(shù)據(jù)不被破壞,需要運(yùn)行時(shí)來保證其不被多個(gè)線程同時(shí)調(diào)用;另外也需要運(yùn)行時(shí)來保證對COM對象的調(diào)用不會(huì)阻塞UI線程。Apartment 就是COM對象生存的地方,一個(gè)Apartment可以包含一個(gè)或者多個(gè)線程。對一個(gè)COM對象的調(diào)用可以由該COM生存的Apartment中的任何一個(gè)線程接受和處理。如果一個(gè)Apartment中只有一個(gè)線程,那么就是STA線程,否則就是MTA,這個(gè)是在程序初始化COM組件的時(shí)候即確定下來的。一個(gè)進(jìn)程可以包含多個(gè)STA,但是只有一個(gè)MTA。

    STA模型是COM對象使用的一種非線程安全的模型,這意味著他不能處理自己的線程同步,通常在UI組件中使用這種模型。因此,如果其他線程需要和UI對象進(jìn)行交互,需要將消息封送(marshall)到STA線程中。在Windows 窗體應(yīng)用程序中,這一過程是通過窗口消息隊(duì)列 (message pumping system)來實(shí)現(xiàn)的。當(dāng)客戶線程以STA 模式啟動(dòng)時(shí),系統(tǒng)將為STA創(chuàng)建一個(gè)隱藏窗口類,所有的對COM對象的調(diào)用都會(huì)放到這個(gè)隱藏窗口的消息隊(duì)列中。

messagepump

    如果COM對象能夠處理其本身的同步邏輯,那么就是MTA模型了,他似的多個(gè)線程能夠同時(shí)和對象進(jìn)行交互,而不需要進(jìn)行消息調(diào)用的封送。

    COM組件在創(chuàng)建的時(shí)候采用哪種模型,可以在注冊表項(xiàng)的ThreadingModel值中指定:

    COM組件在注冊表項(xiàng)中的ThreadingModel屬性中會(huì)有一下四個(gè)屬性:

ThreadingModel

  • Main thread. COM對象創(chuàng)建 在宿主程序的主UI線程上,所有的調(diào)用必須封送到 主UI線程上 .
  • Apartment. 表示該COM對象能夠運(yùn)行在任何但單線程模型的線程上,如果該線程是STA線程創(chuàng)建的,則對象運(yùn)行在該STA線程上,否則該對象運(yùn)行在主STA線程上,如果主STA線程不存在,系統(tǒng)則會(huì)自動(dòng)創(chuàng)建一個(gè)。
  • Free. 表示該COM對象運(yùn)行在MTA上。
  • Both. 表示該COM對象在那個(gè)模型上取決于創(chuàng)建Apartment的類型。

STAThread

    對于.NET Framework來說,通常在任何創(chuàng)建UI的線程上使用[STAThread]自定義屬性來標(biāo)識(shí)其為STA線程。工作線程通常使用MTA模型,但是如果該工作線程需要與表示為Apartment的COM組件一起使用,那就需要標(biāo)識(shí)為STAThread。

    我們可以給Thread對象的ApartmentState屬性指定ApartmentState枚舉類型來給定該Thread屬于那種類型的線程。

    那么如何在其他線程中往STA線程中封送消息呢?這個(gè)就要使用SynchronizationContext對象了。

 

2. SynchronizationContext

    關(guān)于SynchronizationContext類,Understanding SynchronizationContext (Part I) 這篇文章講解的比較好,建議直接閱讀原文。這里簡要說一下,為后面講解做鋪墊。 SynchronizationContext類主要是用來進(jìn)行線程間進(jìn)行通訊的, 比如我有Thread1和Thread2,Thread1在做一些事情,完了之后,Thread1希望將結(jié)果傳遞給Thread2,希望在Thread2上執(zhí)行操作。一種可行的方式是獲取Thread2的SynchronizationContext對象,然后在Thread1中調(diào)用SynchronizationContext的Send或者Post方法,這樣需要做的操作就會(huì)在Thread2上執(zhí)行的。需要注意的是,并不是所有的線程都有一個(gè)SynchronizationContext與之聯(lián)系,只有UI線程上才有SynchronizationContext,通常是在線程中,第一次創(chuàng)建UI控件的時(shí)候,就會(huì)將SynchronizationContext對象附加到當(dāng)前的線程中。

    在進(jìn)行Winform開發(fā)的時(shí)候,我們知道不應(yīng)該在UI線程上執(zhí)行耗時(shí)的操作,因?yàn)閁I線程是一種STA線程,是通過消息隊(duì)列來實(shí)現(xiàn)的,如果某一操作耗時(shí)的話會(huì)阻塞其他的消息處理,影像用戶交互。所以我們一般需要將一些耗時(shí)操縱放到后臺(tái)線程中去處理,完了之后將結(jié)果Post回UI線程來進(jìn)行界面刷新,我們常在非UI線程中使用Control的Invoke和BeginInvoke來實(shí)現(xiàn)UI界面的刷新。而Invoke和BeginInvoke在內(nèi)部其實(shí)是通過繼承自SynchronizationContext的對象來發(fā)送消息實(shí)現(xiàn)的。

    通常,可以通過SynchronizationContext.Current的靜態(tài)屬性來獲取當(dāng)前線程的SynchronizationContext對象

     有了UI線程的SynchronizationContext對象我們就可以在其他線程上通過該對象將我們需要在UI線程上進(jìn)進(jìn)行的操作Post到UI所在的線程上的消息隊(duì)列中了。

    下面的代碼中我們在button2中新建了一個(gè)新的進(jìn)程,然后在該進(jìn)行的方法中傳入了當(dāng)前UI線程的SynchronizationCotext對象, 然后在工作線程中通過該SynchronizationContext對象的Post方法更新UI界面上的Combox對象:

private void button2_Click(object sender, EventArgs e)
{
    // let's see the thread id
    int id = Thread.CurrentThread.ManagedThreadId;
    Trace.WriteLine("Button click thread: " + id);

    // grab the sync context associated to this
    // thread (the UI thread), and save it in uiContext
    // note that this context is set by the UI thread
    // during Form creation (outside of your control)
    // also note, that not every thread has a sync context attached to it.
    SynchronizationContext uiContext = SynchronizationContext.Current;

    // create a thread and associate it to the run method
    Thread thread = new Thread(Run);

    // start the thread, and pass it the UI context,
    // so this thread will be able to update the UI
    // from within the thread
    thread.Start(uiContext);
}


private void Run(object state)
{
    // lets see the thread id
    int id = Thread.CurrentThread.ManagedThreadId;
    Trace.WriteLine("Run thread: " + id);

    // grab the context from the state
    SynchronizationContext uiContext = state as SynchronizationContext;

    for (int i = 0; i < 10; i++)
    {
        // normally you would do some code here
        // to grab items from the database. or some long
        // computation
        Thread.Sleep(10);

        // use the ui context to execute the UpdateUI method,
        // this insure that the UpdateUI method will run on the UI thread.

        uiContext.Post(UpdateUI, "line " + i.ToString());
    }
}

/// <summary>
/// This method is executed on the main UI thread.
/// </summary>
private void UpdateUI(object state)
{
    int id = Thread.CurrentThread.ManagedThreadId;
    Trace.WriteLine("UpdateUI thread:" + id);
    string text = state as string;
    comboBox1.Items.Add(text);
}

  運(yùn)行結(jié)果如下,我們可以看到Button以及UpdateUI的方法都是在UI線程上運(yùn)行的,他們具有相同的線程ID 9,而我們新建的工作線程ID為10。

Button click thread: 9
Run thread: 10
UpdateUI thread:9
UpdateUI thread:9
UpdateUI thread:9
UpdateUI thread:9
UpdateUI thread:9
UpdateUI thread:9
UpdateUI thread:9
UpdateUI thread:9
UpdateUI thread:9
UpdateUI thread:9

   SynchronizationContext對象有Send和Post兩個(gè)方法可以被我們調(diào)用。Send方法是同步的,他會(huì)等待Send進(jìn)去的代理方法執(zhí)行完成之后,再執(zhí)行后面的代碼,而Post方法則是異步的,Post之后會(huì)繼續(xù)執(zhí)行后續(xù)的代碼,Post和Send方法會(huì)異常的捕獲。其內(nèi)部的實(shí)現(xiàn)大致如此:

public virtual void Send(SendOrPostCallback d, Object state)
{
    d(state);
}

public virtual void Post(SendOrPostCallback d, Object state)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
}

實(shí)際上在Winform以及WPF中,我們獲取到的是繼承自SynchronizationContext的對象,在Winform中是System.Windows.Forms.WindowsFormsSynchronizationContext在WPF中則是 System.Windows.Threading.DispatcherSynchronizationContext。比如在Winform中是Control.BeginInvoke,在WPF或者Silverlight中是Dispatcher.BeginInvoke,這些類都重寫了Post和Send方法。并提供了各自的“消息隊(duì)列”(message pump)機(jī)制比如Windows API中的SendMessage 和PostMessage方法來實(shí)現(xiàn)各自的消息分發(fā)和處理。我們在上面代碼中通過SynchronizationContext的Current獲取到的實(shí)際上是一個(gè)WindowsFormsSynchronizationContext對象。真實(shí)的SynchronizationContext類不做任何實(shí)現(xiàn),他更應(yīng)該是一個(gè)虛類。所以通過手動(dòng)new一個(gè)SynchronizationContext,然后賦予當(dāng)前的線程是沒有任何意義的。

 

3. Excel中的線程同步

    前面講過STA以及SynchronizationContext,這是因?yàn)镋xcel也是一種STA線程的應(yīng)用程序,寄宿在Excel之上的Automation程序也是STA的,了解這一點(diǎn)非常重要。

   通常在Excel的插件開發(fā)中,我們的業(yè)務(wù)邏輯可能比較復(fù)雜,這些復(fù)雜的計(jì)算一般不應(yīng)該放到Excel的主UI線程中,我們需要新建工作線程,然后在里面進(jìn)行計(jì)算。獲得了結(jié)果之后,我們應(yīng)該在回到Excel的UI線程中去更新界面。但是我們采用.NET技術(shù)開發(fā)Excel的Automation有一個(gè)特殊性在于,我們可以直接在非UI線程中去調(diào)用Excel的COM對象,在正常情況下,如果Excel比較空閑,沒有任何問題,但是如果Excel此時(shí)比較忙,就會(huì)拋出COM異常,這種異常難以捕捉。這也是導(dǎo)致插件不穩(wěn)定的一個(gè)非常重要的因素。這種情況通常出現(xiàn)在以下情形中:

  • 當(dāng)我們的插件在后臺(tái)線程中向服務(wù)端請求了大量數(shù)據(jù),進(jìn)行了一些處理(這種情況很常見)后,在Excel的Sheet頁中將數(shù)據(jù)填充到單元格中,然后對單元格進(jìn)行樣式,字體等格式化,這個(gè)過程需要與COM進(jìn)行交互,而且在某些情況下比較耗時(shí),如果在此過程中,用戶操作了Excel的單元格,比如鼠標(biāo)點(diǎn)擊填充過程中的單元格,這樣由于后臺(tái)線程通過COM對象對Excel的操作會(huì)遇到忙碌狀態(tài),就會(huì)拋出COM異常。
  • 在RTD 函數(shù)中,我們在某些情況下需要定時(shí)刷新單元格,比如在Excel中直播NBA比賽得分,使用實(shí)時(shí)的股票市場行情信息進(jìn)行建模。在RTD 中,我們可以直接調(diào)用UpdateNotify方法,通常該方法應(yīng)該在UI主線程上調(diào)用,這樣Excel就會(huì)將其放到消息隊(duì)列中,在某一時(shí)候觸發(fā)。但是在很多時(shí)候,我們獲取數(shù)據(jù)比如NBA實(shí)時(shí)比分,實(shí)時(shí)行情數(shù)據(jù),通常是在另外一根工作線程中進(jìn)行的,我們可以在工作線程中直接調(diào)用RTD的UpdateNofity方法,但是在Excel忙碌的時(shí)候就會(huì)拋出COM異常。

    Excel中運(yùn)行我們再工作線程中通過Excel 的Application對象來直接更新UI界面元素給了我們一個(gè)假象。原因在于這樣是很不穩(wěn)定的,非Excel主線程的每一次COM調(diào)用中都需要檢查是否拋出異常,在調(diào)用過程中Excel很可能處于忙碌狀態(tài),Excel也可能在任何情況下拒絕線程對COM調(diào)用的請求,尤其是在用戶正與Excel進(jìn)行交互的時(shí)候。通常我們至少要捕獲和處理一下三種COM異常:

  1. u const uint RPC_E_SERVERCALL_RETRYLATER = 0x8001010A;
  2. u const uint VBA_E_IGNORE = 0x800AC472;
  3. u const uint RPC_E_CALLREJECTED

     在其他線程中直接調(diào)用Excel對象不僅會(huì)導(dǎo)致性能損失,而且會(huì)增加插件的復(fù)雜性和不穩(wěn)定性。

    正確的做法是,在工作線程中獲取Excel主線程對象的SynchronizationContext,然后將待操作的步驟Post到Excel主線程的消息隊(duì)列中等待處理。但是作為一個(gè)Addin,在一般情況下如果直接獲取SynchronizationContext對象,該對象是為空的,只有在插件加載后,手動(dòng)創(chuàng)建一個(gè)Winform窗體或者控件才能夠獲取到主線程的SynchronizaitonContext對象。這個(gè)From窗體通常就是我們插件的登錄窗體。

    比如如果要在非Excel 主線程中調(diào)用RTD函數(shù)的UpdateNotify方法,我們可以首先定義一個(gè)SynchronizationContext用來保存Excel主線程的同步上下文。

private SynchronizationContext ExcelContext; 

    然后在RTD啟動(dòng)時(shí)獲取當(dāng)前Excel主線程的上下文。

public int ServerStart(IRTDUpdateEvent CallbackObject)
{
    this.ExcelContext = new SynchronizationContext();
    xlRTDUpdater = CallbackObject;
} 

   最后工作線程中,通過傳進(jìn)來的ExcelContext,然后將需要做的操作Send或者Post回Excel主線程中執(zhí)行。

ExcelContext.Post(delegate(object obj)
            {
                xlRTDUpdater.UpdateNotify();
            }, null);

  所以其他非UI線程中需要操作Excel COM對象的方法經(jīng)過如此封裝將需要做的操作以消息的形式封送到UI線程,這樣就可以解決之前調(diào)用COM組件可能出現(xiàn)的COM異常,能夠極大提高Excel插件的穩(wěn)定性。

    本文很多內(nèi)容涉及到COM組件的相關(guān)知識(shí),這里只是簡單的講解了一些與Excel插件開發(fā)中可能與之相關(guān)的一些問題,介紹了如何正確的在工作線程中更新Excel UI操作的一些正確做法,希望這些知識(shí)對您有所幫助。

 

參考資料

    本文參考了很多資料,如果您想深入了解,以下文章對您或許有幫助。

  1. Understanding The COM Single-Threaded Apartment
  2. Understanding SynchronizationContext
  3. Apartments and Pumping in the CLR
  4. What is a message pump?
  5. Excel interop COM exception while running in background
  6. Accessing Excel Application Object From An STA Thread (Refer to the post by Geoff Darst)
  7. ExecutionContext vs SynchronizationContext
  8. A CONCRETE EXAMPLE OF HOW CONTROL.INVOKE CAN CAUSE DEADLOCK

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多