簡體   English   中英

SynchronizationContext 有什么作用?

[英]What does SynchronizationContext do?

在 Programming C# 一書中,它有一些關於SynchronizationContext的示例代碼:

SynchronizationContext originalContext = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate {
    string text = File.ReadAllText(@"c:\temp\log.txt");
    originalContext.Post(delegate {
        myTextBox.Text = text;
    }, null);
});

我是線程初學者,所以請詳細回答。 首先,我不知道 context 是什么意思,程序在originalContext中保存了什么? 而當Post方法被觸發時,UI 線程會做什么?
如果我問一些愚蠢的事情,請糾正我,謝謝

編輯:例如,如果我只寫myTextBox.Text = text; 在方法上,有什么區別?

SynchronizationContext 有什么作用?

簡而言之, SynchronizationContext表示可能執行代碼的“位置”。 然后將在該位置調用傳遞給其SendPost方法的委托。 PostSend的非阻塞/異步版本。)

每個線程都可以有一個與之關聯的SynchronizationContext實例。 運行線程可以通過調用static SynchronizationContext.SetSynchronizationContext方法關聯一個同步上下文,通過SynchronizationContext.Current屬性可以查詢運行線程的當前上下文。

盡管我剛剛寫了(每個線程都有一個關聯的同步上下文),但SynchronizationContext並不一定代表一個特定的線程 它還可以將傳遞給它的委托的調用轉發給多個線程中的任何一個(例如,一個ThreadPool工作線程),或者(至少在理論上)一個特定的CPU 核心,甚至另一個網絡主機 您的代表最終運行的位置取決於所使用的SynchronizationContext的類型。

Windows Forms 將在創建第一個表單的線程上安裝WindowsFormsSynchronizationContext (這個線程通常被稱為“UI 線程”。)這種類型的同步上下文調用在那個線程上傳遞給它的委托。 這非常有用,因為 Windows Forms 與許多其他 UI 框架一樣,只允許在創建控件的同一線程上操作控件。

如果我只寫myTextBox.Text = text; 在方法上,有什么區別?

您傳遞給ThreadPool.QueueUserWorkItem的代碼將在線程池工作線程上運行。 也就是說,它不會在創建myTextBox的線程上執行,因此 Windows Forms 遲早會(尤其是在發布版本中)拋出異常,告訴您可能無法從另一個線程訪問myTextBox

這就是為什么您必須在特定分配之前以某種方式從工作線程“切換回”到“UI 線程”(創建myTextBox的地方)。 這是按如下方式完成的:

  1. 當您仍在 UI 線程上時,在此處捕獲 Windows F​​orms 的SynchronizationContext ,並將對它的引用存儲在變量 ( originalContext ) 中以供以后使用。 此時必須查詢SynchronizationContext.Current 如果您在傳遞給ThreadPool.QueueUserWorkItem的代碼中查詢它,您可能會獲得與線程池的工作線程相關聯的任何同步上下文。 一旦您存儲了對 Windows F​​orms 上下文的引用,您就可以隨時隨地使用它將代碼“發送”到 UI 線程。

  2. 每當您需要操作 UI 元素(但現在不在或可能不再在 UI 線程上)時,請通過originalContext訪問 Windows F​​orms 的同步上下文,並將操作 UI 的代碼交給SendPost


最后的評論和提示:

  • 同步上下文不會告訴您哪些代碼必須在特定位置/上下文中運行,哪些代碼可以正常執行,而無需將其傳遞給SynchronizationContext 為了做出決定,你必須知道你正在編程的框架的規則和要求——在這種情況下是 Windows Forms。

    所以請記住 Windows Forms 的這個簡單規則:不要從創建它們的線程之外的線程訪問控件或 forms。 如果您必須這樣做,請使用上述SynchronizationContext機制,或Control.BeginInvoke (這是 Windows F​​orms 特定的方式,可以執行完全相同的操作)。

  • 如果您正在針對 .NET 4.5 或更高版本進行編程,您可以通過將明確使用SynchronizationContextThreadPool.QueueUserWorkItemcontrol.BeginInvoke等的代碼轉換為新的async / await關鍵字任務並行庫來簡化您的工作(TPL) ,即圍繞TaskTask<TResult>類的 API。 這些將在很大程度上負責捕獲 UI 線程的同步上下文,啟動異步操作,然后返回 UI 線程,以便您可以處理操作的結果。

我想添加到其他答案, SynchronizationContext.Post只是將回調排隊以便稍后在目標線程上執行(通常在目標線程的消息循環的下一個循環期間),然后在調用線程上繼續執行。 另一方面, SynchronizationContext.Send嘗試立即在目標線程上執行回調,這會阻塞調用線程並可能導致死鎖。 在這兩種情況下,都存在代碼重入的可能性(在對同一方法的先前調用返回之前,在同一執行線程上輸入 class 方法)。

如果您熟悉 Win32 編程 model,一個非常相似的類比是PostMessageSendMessage API,您可以調用它們從與目標窗口線程不同的線程發送消息。

這是對什么是同步上下文的一個很好的解釋:這都是關於 SynchronizationContext 的

它存儲同步提供程序,即從 SynchronizationContext 派生的 class。 在這種情況下,這可能是 WindowsFormsSynchronizationContext 的一個實例。 class 使用 Control.Invoke() 和 Control.BeginInvoke() 方法來實現 Send() 和 Post() 方法。 或者它可以是 DispatcherSynchronizationContext,它使用 Dispatcher.Invoke() 和 BeginInvoke()。 在 Winforms 或 WPF 應用程序中,該提供程序會在您創建 window 后立即自動安裝。

當您在另一個線程上運行代碼時,例如片段中使用的線程池線程,那么您必須小心不要直接使用線程不安全的對象。 與任何用戶界面 object 一樣,您必須從創建 TextBox 的線程更新 TextBox.Text 屬性。 Post() 方法確保委托目標在該線程上運行。

請注意,此代碼段有點危險,它只有在您從 UI 線程調用時才能正常工作。 SynchronizationContext.Current 在不同的線程中具有不同的值。 只有 UI 線程具有可用值。 這就是代碼必須復制它的原因。 在 Winforms 應用程序中,一種更易讀、更安全的方法:

    ThreadPool.QueueUserWorkItem(delegate {
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.BeginInvoke(new Action(() => {
            myTextBox.Text = text;
        }));
    });

它的優點是它在從任何線程調用時都可以工作。 使用 SynchronizationContext.Current 的優點是,無論代碼用於 Winforms 還是 WPF,它仍然有效,這在庫中很重要。 這當然不是這樣的代碼的一個很好的例子,你總是知道你在這里有什么樣的文本框,所以你總是知道是使用 Control.BeginInvoke 還是 Dispatcher.BeginInvoke。 實際上使用 SynchronizationContext.Current 並不常見。

這本書試圖教你線程,所以使用這個有缺陷的例子是可以的。 在現實生活中,在您可能考慮使用 SynchronizationContext.Current 的少數情況下,您仍然可以將其留給 C# 的 async/await 關鍵字或 TaskScheduler.FromCurrentSynchronizationContext() 來為您完成。 但請注意,由於完全相同的原因,當您在錯誤的線程上使用它們時,它們的行為仍然與代碼片段的行為方式不同。 這里有一個非常常見的問題,額外的抽象級別很有用,但很難弄清楚為什么它們不能正常工作。 希望這本書也告訴你什么時候不使用它:)

這里同步上下文的目的是確保myTextbox.Text = text; 在主 UI 線程上調用。

Windows 要求 GUI 控件只能由創建它們的線程訪問。 如果您嘗試在沒有首先同步的情況下在后台線程中分配文本(通過多種方式中的任何一種,例如 this 或 Invoke 模式),則會引發異常。

這樣做是在創建后台線程之前保存同步上下文,然后后台線程使用 context.Post 方法執行 GUI 代碼。

是的,您顯示的代碼基本上沒用。 為什么要創建后台線程,只需要立即將 go 回主 UI 線程? 這只是一個例子。

到源頭

每個線程都有一個與之關聯的上下文——這也稱為“當前”上下文——並且這些上下文可以跨線程共享。 ExecutionContext 包含程序正在執行的當前環境或上下文的相關元數據。 SynchronizationContext 代表一種抽象——它表示應用程序代碼執行的位置。

SynchronizationContext 使您能夠將任務排隊到另一個上下文中。 請注意,每個線程都可以有自己的 SynchronizatonContext。

例如:假設您有兩個線程,Thread1 和 Thread2。 假設 Thread1 正在做一些工作,然后 Thread1 希望在 Thread2 上執行代碼。 一種可能的方法是向 Thread2 詢問其 SynchronizationContext object,將其提供給 Thread1,然后 Thread1 可以調用 SynchronizationContext.Send 來執行 Thread2 上的代碼。

SynchronizationContext基本上是回調委托執行的提供者。 它負責確保在程序中的特定代碼部分(封裝在.Net TPL 中的任務 object 中)完成執行后,委托在給定的執行上下文中運行。

從技術角度來看,SC 是一個簡單的 C# class,它旨在支持和提供專門用於任務並行庫對象的 function。

Every.Net 應用程序除了控制台應用程序之外,都有基於特定底層框架的 class 的定制實現,例如:WPF、WindowsForm、Asp Net、Silverlight 等。

此 object 的重要性與異步執行代碼返回的結果與等待異步工作結果的相關代碼執行之間的同步有關。

而“上下文”一詞代表執行上下文。 也就是說,等待代碼將被執行的當前執行上下文——即異步代碼與其等待代碼之間的同步發生在特定的執行上下文中。 因此這個 object 被命名為 SynchronizationContext。

它表示將關注異步代碼同步和等待代碼執行的執行上下文

SynchronizationContext 為我們提供了一種從不同線程更新 UI 的方法(通過 Send 方法同步或通過 Post 方法異步)。

看看下面的例子:

    private void SynchronizationContext SyncContext = SynchronizationContext.Current;
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Thread thread = new Thread(Work1);
        thread.Start(SyncContext);
    }

    private void Work1(object state)
    {
        SynchronizationContext syncContext = state as SynchronizationContext;
        syncContext.Post(UpdateTextBox, syncContext);
    }

    private void UpdateTextBox(object state)
    {
        Thread.Sleep(1000);
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.Text = text;
    }

SynchronizationContext.Current 將返回 UI 線程的同步上下文。 我怎么知道這個? 在每個表單或 WPF 應用程序的開頭,上下文將在 UI 線程上設置。 如果您創建一個 WPF 應用程序並運行我的示例,您會看到當您單擊按鈕時,它會休眠大約 1 秒鍾,然后它將顯示文件的內容。 您可能期望它不會,因為 UpdateTextBox 方法(即 Work1)的調用者是傳遞給線程的方法,因此它應該休眠該線程而不是主 UI 線程,NOPE,即使 Work1 方法被傳遞給線程。 請注意,它還接受一個 object,它是 SyncContext,如果您查看它。 您會看到 UpdateTextBox 方法是通過 syncContext.Post 方法而不是 Work1 方法執行的:看看下面的

private void Button_Click(object sender, RoutedEventArgs e) 
{
    Thread.Sleep(1000);
    string text = File.ReadAllText(@"c:\temp\log.txt");
    myTextBox.Text = text;
}

最后一個例子和這個例子執行相同。 兩者都不會在工作時阻止 UI。

總之,將 SynchronizationContext 視為一個線程。 它不是線程,它定義了一個線程(請注意,並非所有線程都有 SyncContext)。 每當我們在其上調用 Post 或 Send 方法來更新 UI 時,就像從主 UI 線程正常更新 UI 一樣。 如果由於某些原因,您需要從不同的線程更新 UI,請確保該線程具有主 UI 線程的 SyncContext,然后使用您要執行的方法調用其上的 Send 或 Post 方法,您就可以了放。

希望這對你有幫助,伙計

此示例來自 Joseph Albahari 的 Linqpad 示例,但它確實有助於理解同步上下文的作用。

void WaitForTwoSecondsAsync (Action continuation)
{
    continuation.Dump();
    var syncContext = AsyncOperationManager.SynchronizationContext;
    new Timer (_ => syncContext.Post (o => continuation(), _)).Change (2000, -1);
}

void Main()
{
    Util.CreateSynchronizationContext();
    ("Waiting on thread " + Thread.CurrentThread.ManagedThreadId).Dump();
    for (int i = 0; i < 10; i++)
        WaitForTwoSecondsAsync (() => ("Done on thread " + Thread.CurrentThread.ManagedThreadId).Dump());
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM