SyncizationContext làm gì?


134

Trong cuốn sách Lập trình C #, nó có một số mã mẫu về SynchronizationContext:

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

Tôi là người mới bắt đầu trong các chủ đề, vì vậy xin vui lòng trả lời chi tiết. Đầu tiên, tôi không biết bối cảnh nghĩa là gì, chương trình tiết kiệm trong cái originalContextgì? Và khi Postphương thức được kích hoạt, luồng UI sẽ làm gì?
Nếu tôi hỏi một số điều ngớ ngẩn, xin vui lòng sửa cho tôi, cảm ơn!

EDIT: Ví dụ, nếu tôi chỉ viết myTextBox.Text = text;trong phương thức, sự khác biệt là gì?



IMHO async đang chờ đợi điều này
Royi Namir

7
@RoyiNamir: Có, nhưng hãy đoán xem: async/ awaitdựa vào SynchronizationContextbên dưới.
stakx - không còn đóng góp vào

Câu trả lời:


169

SyncizationContext làm gì?

Nói một cách đơn giản, SynchronizationContextđại diện cho một vị trí "nơi" mã có thể được thực thi. Các đại biểu được truyền cho phương thứcSend hoặc Postphương thức của nó sau đó sẽ được gọi ở vị trí đó. ( Postlà phiên bản không chặn / không đồng bộ của Send.)

Mỗi chủ đề có thể có một SynchronizationContextví dụ liên quan đến nó. Chuỗi chạy có thể được liên kết với bối cảnh đồng bộ hóa bằng cách gọi phương thức tĩnhSynchronizationContext.SetSynchronizationContext và bối cảnh hiện tại của luồng đang chạy có thể được truy vấn thông qua thuộc SynchronizationContext.Currenttính .

Bất chấp những gì tôi vừa viết (mỗi luồng có bối cảnh đồng bộ hóa liên quan), một luồngSynchronizationContext không nhất thiết phải đại diện cho một luồng cụ thể ; nó cũng có thể chuyển tiếp lời mời của các đại biểu được truyền tới nó tới bất kỳ luồng nào (ví dụ như ThreadPoolluồng công nhân) hoặc (ít nhất là về lý thuyết) đến lõi CPU cụ thể hoặc thậm chí đến máy chủ mạng khác . Trường hợp đại biểu của bạn kết thúc hoạt động phụ thuộc vào loại SynchronizationContextđược sử dụng.

Windows Forms sẽ cài đặt một WindowsFormsSynchronizationContextchủ đề trên đó tạo mẫu đầu tiên. (Chuỗi này thường được gọi là "luồng UI".) Loại bối cảnh đồng bộ hóa này gọi các đại biểu được truyền cho nó trên chính xác luồng đó. Điều này rất hữu ích vì Windows Forms, giống như nhiều khung UI khác, chỉ cho phép thao tác các điều khiển trên cùng một luồng mà chúng được tạo.

Điều gì sẽ xảy ra nếu tôi chỉ viết myTextBox.Text = text;trong phương thức, sự khác biệt là gì?

Mã mà bạn đã chuyển đến ThreadPool.QueueUserWorkItemsẽ được chạy trên một luồng công nhân nhóm luồng. Đó là, nó sẽ không thực thi trên luồng mà bạn myTextBoxđã tạo, do đó, Windows Forms sẽ sớm hay muộn (đặc biệt là trong các bản dựng Phát hành) đưa ra một ngoại lệ, cho bạn biết rằng bạn không thể truy cập myTextBoxtừ một luồng khác.

Đây là lý do tại sao bạn phải "chuyển trở lại" từ luồng công nhân sang "luồng UI" (nơi myTextBoxđược tạo) trước khi gán cụ thể đó. Điều này được thực hiện như sau:

  1. Trong khi bạn vẫn đang sử dụng luồng UI, hãy chụp Windows Forms ' SynchronizationContextở đó và lưu trữ một tham chiếu đến nó trong một biến ( originalContext) để sử dụng sau. Bạn phải truy vấn SynchronizationContext.Currenttại thời điểm này; nếu bạn đã truy vấn nó bên trong mã được truyền tới ThreadPool.QueueUserWorkItem, bạn có thể nhận được bất kỳ bối cảnh đồng bộ hóa nào được liên kết với luồng công nhân của nhóm luồng. Khi bạn đã lưu trữ một tham chiếu đến ngữ cảnh của Windows Forms, bạn có thể sử dụng nó ở bất cứ đâu và bất cứ lúc nào để "gửi" mã đến luồng UI.

  2. Bất cứ khi nào bạn cần thao tác với một thành phần UI (nhưng không, hoặc có thể không, trên luồng UI nữa), hãy truy cập vào bối cảnh đồng bộ hóa của Windows Forms thông qua originalContextvà tắt mã sẽ thao tác UI với Sendhoặc Post.


Nhận xét và gợi ý cuối cùng:

  • Bối cảnh đồng bộ hóa nào sẽ không làm cho bạn là cho bạn biết mã nào phải chạy trong một vị trí / ngữ cảnh cụ thể và mã nào có thể được thực thi bình thường mà không chuyển mã sang a SynchronizationContext. Để quyết định điều đó, bạn phải biết các quy tắc và yêu cầu của khung bạn đang lập trình - Windows Forms trong trường hợp này.

    Vì vậy, hãy nhớ quy tắc đơn giản này cho Windows Forms: KHÔNG truy cập các điều khiển hoặc biểu mẫu từ một luồng khác với luồng đã tạo ra chúng. Nếu bạn phải làm điều này, hãy sử dụng SynchronizationContextcơ chế như được mô tả ở trên hoặc Control.BeginInvoke(đó là cách cụ thể của Windows Forms để thực hiện chính xác điều tương tự).

  • Nếu bạn đang lập trình với .NET 4.5 hoặc mới hơn, bạn có thể làm cho cuộc sống của bạn dễ dàng hơn nhiều bằng cách chuyển đổi mã của bạn một cách rõ ràng rằng sử dụng SynchronizationContext, ThreadPool.QueueUserWorkItem, control.BeginInvokevv giao cho mới async/ awaittừ khóa và các tác vụ song song Library (TPL) , tức là API xung quanh các TaskTask<TResult>các lớp học. Ở một mức độ rất cao, sẽ đảm nhận việc nắm bắt bối cảnh đồng bộ hóa của giao diện người dùng, bắt đầu một hoạt động không đồng bộ, sau đó quay lại vào giao diện người dùng để bạn có thể xử lý kết quả của hoạt động.


Bạn nói Windows Forms, giống như nhiều khung UI khác, chỉ cho phép thao tác điều khiển trên cùng một luồng nhưng tất cả các cửa sổ trong Windows phải được truy cập bởi cùng một luồng đã tạo ra nó.
dùng34660

4
@ user34660: Không, điều đó không chính xác. Bạn có thể có một số luồng tạo các điều khiển Windows Forms. Nhưng mỗi điều khiển được liên kết với một luồng đã tạo ra nó và chỉ được truy cập bởi một luồng đó. Các điều khiển từ các luồng UI khác nhau cũng rất hạn chế trong cách chúng tương tác với nhau: người ta không thể là cha / con của người kia, việc liên kết dữ liệu giữa họ không thể thực hiện được, v.v. Cuối cùng, mỗi luồng tạo điều khiển cần thông điệp riêng vòng lặp (được bắt đầu bởi Application.Run, IIRC). Đây là một chủ đề khá tiên tiến và không phải là một cái gì đó tình cờ được thực hiện.
stakx - không còn đóng góp vào

Nhận xét đầu tiên của tôi là do bạn nói "giống như nhiều khung UI khác" ngụ ý rằng một số cửa sổ cho phép "thao tác điều khiển" từ một luồng khác nhưng không có cửa sổ Windows nào làm được. Bạn không thể "có một số luồng tạo các điều khiển Windows Forms" cho cùng một cửa sổ và "phải được truy cập bởi cùng một luồng" và "chỉ được truy cập bởi một luồng đó" đang nói điều tương tự. Tôi nghi ngờ rằng có thể tạo "Điều khiển từ các luồng UI khác nhau" cho cùng một cửa sổ. Tất cả điều này không phải là nâng cao đối với những người trong chúng ta có kinh nghiệm với lập trình Windows trước .Net.
dùng34660

3
Tất cả những điều này nói về "windows" và "Windows windows" đang khiến tôi khá chóng mặt. Tôi đã đề cập đến bất kỳ "cửa sổ" nào? Tôi không nghĩ vậy ...
stakx - không còn đóng góp vào

1
@ibubi: Tôi không chắc là tôi hiểu câu hỏi của bạn. Bất kỳ bối cảnh đồng bộ hóa của luồng nào cũng không được đặt ( null) hoặc một thể hiện của SynchronizationContext(hoặc một lớp con của nó). Điểm của trích dẫn đó không phải là những gì bạn nhận được, mà là những gì bạn sẽ không nhận được: bối cảnh đồng bộ hóa của giao diện người dùng.
stakx - không còn đóng góp

24

Tôi muốn thêm vào các câu trả lời khác, SynchronizationContext.Postchỉ cần xếp hàng gọi lại để thực hiện sau trên luồng đích (thông thường trong chu kỳ tiếp theo của vòng lặp thông báo của luồng đích), và sau đó thực hiện tiếp tục trên luồng gọi. Mặt khác, SynchronizationContext.Sendcố gắng thực hiện cuộc gọi lại trên luồng đích ngay lập tức, điều này sẽ chặn luồng cuộc gọi và có thể dẫn đến bế tắc. Trong cả hai trường hợp, có khả năng reentrancy mã (nhập một phương thức lớp trên cùng một luồng thực hiện trước khi cuộc gọi trước đó đến cùng phương thức đã trả về).

Nếu bạn đã quen thuộc với mô hình lập trình Win32, một loại suy rất gần sẽ PostMessageSendMessageAPI, mà bạn có thể gọi để gửi một tin nhắn từ một khác nhau chủ đề từ một cửa sổ mục tiêu của.

Dưới đây là một lời giải thích rất hay về bối cảnh đồng bộ hóa là gì: Đó là tất cả về Đồng bộ hóa nội dung .


16

Nó lưu trữ nhà cung cấp đồng bộ hóa, một lớp có nguồn gốc từ SyncizationContext. Trong trường hợp này, đó có thể sẽ là một phiên bản của WindowsFormsSyn syncizationContext. Lớp đó sử dụng các phương thức Control.Invoke () và Control.BeginInvoke () để thực hiện các phương thức Send () và Post (). Hoặc nó có thể là DispatcherSyn syncizationContext, nó sử dụng Dispatcher.Invoke () và BeginInvoke (). Trong ứng dụng Winforms hoặc WPF, nhà cung cấp đó sẽ tự động cài đặt ngay khi bạn tạo một cửa sổ.

Khi bạn chạy mã trên một luồng khác, như luồng xử lý nhóm luồng được sử dụng trong đoạn mã, thì bạn phải cẩn thận rằng bạn không trực tiếp sử dụng các đối tượng không an toàn của luồng. Giống như bất kỳ đối tượng giao diện người dùng nào, bạn phải cập nhật thuộc tính TextBox.Text từ luồng đã tạo TextBox. Phương thức Post () đảm bảo rằng mục tiêu ủy nhiệm chạy trên luồng đó.

Coi chừng đoạn trích này hơi nguy hiểm, nó sẽ chỉ hoạt động chính xác khi bạn gọi nó từ luồng UI. Đồng bộ hóaContext.C hiện có các giá trị khác nhau trong các luồng khác nhau. Chỉ chủ đề UI có giá trị có thể sử dụng. Và là lý do mã phải sao chép nó. Một cách dễ đọc và an toàn hơn để làm điều đó, trong ứng dụng Winforms:

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

Mà có lợi thế là nó hoạt động khi được gọi từ bất kỳ chủ đề. Ưu điểm của việc sử dụng SyncizationContext.C hiện tại là nó vẫn hoạt động cho dù mã được sử dụng trong Winforms hay WPF, nó quan trọng trong thư viện. Đây chắc chắn không phải là một ví dụ hay về mã như vậy, bạn luôn biết loại TextBox nào bạn có ở đây để bạn luôn biết nên sử dụng Control.BeginInvoke hay Dispatcher.BeginInvoke. Trên thực tế, sử dụng SyncizationContext.C hiện tại không phổ biến.

Cuốn sách đang cố gắng dạy bạn về xâu chuỗi, vì vậy sử dụng ví dụ thiếu sót này là ổn. Trong cuộc sống thực, trong một số trường hợp bạn có thể cân nhắc sử dụng SyncizationContext.C Hiện tại, bạn vẫn để nó theo từ khóa không đồng bộ / chờ đợi của C # hoặc TaskScheduler.FromCienSyn syncizationContext () để làm điều đó cho bạn. Nhưng hãy lưu ý rằng họ vẫn xử lý sai cách đoạn trích khi bạn sử dụng chúng trên chuỗi sai, vì lý do chính xác tương tự. Một câu hỏi rất phổ biến ở đây, mức độ trừu tượng thêm rất hữu ích nhưng làm cho khó hiểu tại sao chúng không hoạt động chính xác. Hy vọng cuốn sách cũng cho bạn biết khi không sử dụng nó :)


Tôi xin lỗi, tại sao để xử lý chủ đề UI là an toàn cho chủ đề? tức là tôi nghĩ luồng UI có thể đang sử dụng myTextBox khi Post () được kích hoạt, điều đó có an toàn không?
cloudyFan

4
Tiếng Anh của bạn khó giải mã. Đoạn mã gốc của bạn chỉ hoạt động chính xác khi được gọi từ luồng UI. Đó là một trường hợp rất phổ biến. Chỉ sau đó nó sẽ đăng trở lại chủ đề UI. Nếu nó được gọi từ một luồng công nhân thì mục tiêu ủy nhiệm Post () sẽ chạy trên một luồng luồng. Kaboom. Đây là một cái gì đó bạn muốn thử cho chính mình. Bắt đầu một chủ đề và để cho chủ đề gọi mã này. Bạn đã làm đúng nếu mã gặp sự cố với NullReferenceException.
Hans Passant

5

Mục đích của bối cảnh đồng bộ hóa ở đây là để đảm bảo rằng nó myTextbox.Text = text;được gọi trên luồng UI chính.

Windows yêu cầu các điều khiển GUI chỉ được truy cập bởi luồng mà chúng được tạo. Nếu bạn thử gán văn bản trong một luồng nền mà không đồng bộ hóa trước (thông qua bất kỳ phương tiện nào, chẳng hạn như mẫu này hoặc mẫu Gọi) thì một ngoại lệ sẽ được đưa ra.

Điều này làm là lưu bối cảnh đồng bộ hóa trước khi tạo luồng nền, sau đó luồng nền sử dụng bối cảnh.Post thực thi mã GUI.

Có, mã bạn đã hiển thị về cơ bản là vô dụng. Tại sao tạo một luồng nền, chỉ cần ngay lập tức quay lại luồng UI chính? Đó chỉ là một ví dụ.


4
"Vâng, mã bạn đã hiển thị về cơ bản là vô dụng. Tại sao phải tạo một luồng nền, chỉ cần ngay lập tức quay lại luồng UI chính? Đó chỉ là một ví dụ." - Đọc từ tệp có thể là một nhiệm vụ dài nếu tệp lớn, thứ gì đó có thể chặn luồng UI và khiến nó không phản hồi
Yair Nevet

Tôi có một câu hỏi ngu ngốc. Mỗi luồng có một Id và tôi cho rằng luồng UI cũng có ID = 2 chẳng hạn. Sau đó, khi tôi đang xử lý chuỗi chủ đề, tôi có thể làm điều gì đó như thế không: var thread = GetThread (2); thread.Execute (() => textbox1.Text = "foo")?
Giăng

@ John - Không, tôi không nghĩ rằng nó hoạt động vì luồng đã được thực thi. Bạn không thể thực thi một luồng đã thực hiện. Thực thi chỉ hoạt động khi một luồng không chạy (IIRC)
Erik Funkenbusch

3

Về nguồn

Mỗi luồng có một bối cảnh liên quan đến nó - đây còn được gọi là bối cảnh "hiện tại" - và các bối cảnh này có thể được chia sẻ trên các luồng. ExecutContext chứa siêu dữ liệu có liên quan của môi trường hoặc bối cảnh hiện tại mà chương trình đang được thực thi. SyncizationContext đại diện cho một sự trừu tượng hóa - nó biểu thị vị trí nơi mã ứng dụng của bạn được thực thi.

SyncizationContext cho phép bạn xếp hàng một tác vụ vào một bối cảnh khác. Lưu ý rằng mỗi luồng có thể có SyncizatonContext riêng.

Ví dụ: Giả sử bạn có hai luồng, Thread1 và Thread2. Giả sử, Thread1 đang thực hiện một số công việc và sau đó Thread1 muốn thực thi mã trên Thread2. Một cách có thể làm là yêu cầu Thread2 cho đối tượng SyncizationContext của nó, đưa nó cho Thread1 và sau đó Thread1 có thể gọi SyncizationContext.Send để thực thi mã trên Thread2.


2
Một bối cảnh đồng bộ hóa không nhất thiết phải gắn với một chủ đề cụ thể. Nhiều luồng có thể xử lý các yêu cầu trong một bối cảnh đồng bộ hóa duy nhất và cho một luồng xử lý các yêu cầu cho nhiều bối cảnh đồng bộ hóa.
Phục vụ

3

SyncizationContext cung cấp cho chúng ta cách cập nhật giao diện người dùng từ một luồng khác (đồng bộ thông qua phương thức Gửi hoặc không đồng bộ qua phương thức Đăng).

Hãy xem ví dụ sau:

    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;
    }

SyncizationContext.C hiện tại sẽ trả về bối cảnh đồng bộ hóa của giao diện người dùng. Làm thế nào để tôi biết điều này? Khi bắt đầu mọi hình thức hoặc ứng dụng WPF, bối cảnh sẽ được đặt trên luồng UI. Nếu bạn tạo một ứng dụng WPF và chạy ví dụ của tôi, bạn sẽ thấy rằng khi bạn nhấp vào nút, nó sẽ ngủ trong khoảng 1 giây, sau đó nó sẽ hiển thị nội dung của tệp. Bạn có thể mong đợi nó sẽ không bởi vì người gọi phương thức UpdateTextBox (là Work1) là một phương thức được truyền cho một Chủ đề, do đó, nó sẽ ngủ chủ đề đó không phải là chủ đề UI chính, NOPE! Mặc dù phương thức Work1 được truyền đến một luồng, lưu ý rằng nó cũng chấp nhận một đối tượng là SyncContext. Nếu bạn nhìn vào nó, bạn sẽ thấy phương thức UpdateTextBox được thực thi thông qua phương thức syncContext.Post chứ không phải phương thức Work1. Hãy xem những điều sau đây:

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

Ví dụ cuối cùng và cái này thực hiện giống nhau. Cả hai đều không chặn UI trong khi nó hoạt động.

Để kết luận, hãy nghĩ về SyncizationContext như một luồng. Nó không phải là một luồng, nó xác định một luồng (Lưu ý rằng không phải tất cả các luồng đều có SyncContext). Bất cứ khi nào chúng tôi gọi phương thức Đăng hoặc Gửi trên đó để cập nhật giao diện người dùng, thì cũng giống như cập nhật giao diện người dùng thông thường từ luồng giao diện người dùng chính. Nếu, vì một số lý do, bạn cần cập nhật UI từ một luồng khác, hãy đảm bảo rằng luồng đó có SyncContext của luồng UI chính và chỉ gọi phương thức Gửi hoặc Đăng trên đó bằng phương thức mà bạn muốn thực thi và bạn là tất cả bộ.

Hy vọng điều này sẽ giúp bạn, bạn đời!


2

SyncizationContext về cơ bản là nhà cung cấp thực thi các đại biểu gọi lại chịu trách nhiệm chính để đảm bảo rằng các đại biểu được chạy trong bối cảnh thực thi nhất định sau khi một phần mã cụ thể (được bao gồm trong Nhiệm vụ của .Net TPL) của chương trình đã hoàn thành việc thực thi.

Từ quan điểm kỹ thuật, SC là một lớp C # đơn giản được định hướng để hỗ trợ và cung cấp chức năng của nó đặc biệt cho các đối tượng Thư viện song song.

Mỗi ứng dụng .Net, ngoại trừ các ứng dụng bảng điều khiển, có một triển khai cụ thể của lớp này dựa trên khung cơ bản cụ thể, ví dụ: WPF, WindowsForm, Asp Net, Silverlight, ecc ..

Tầm quan trọng của đối tượng này bị ràng buộc với sự đồng bộ hóa giữa các kết quả trả về từ việc thực thi mã không đồng bộ và thực thi mã phụ thuộc đang chờ kết quả từ công việc không đồng bộ đó.

Và từ "bối cảnh" là viết tắt của bối cảnh thực thi, đó là bối cảnh thực thi hiện tại nơi mã chờ đó sẽ được thực thi, cụ thể là quá trình đồng bộ hóa giữa mã async và mã chờ của nó xảy ra trong ngữ cảnh thực thi cụ thể, do đó, đối tượng này được đặt tên là SyncizationContext: nó đại diện cho bối cảnh thực thi sẽ chăm sóc quá trình đồng bộ hóa mã async và thực thi mã chờ .


1

Ví dụ này là từ các ví dụ Linqpad từ Joseph Albahari nhưng nó thực sự giúp hiểu được bối cảnh Đồng bộ hóa làm gì.

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());
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.