Nhiệm vụ tiếp tục trên luồng UI


214

Có cách nào 'tiêu chuẩn' để xác định rằng việc tiếp tục tác vụ sẽ chạy trên luồng mà tác vụ ban đầu được tạo không?

Hiện tại tôi có mã bên dưới - nó đang hoạt động nhưng theo dõi người điều phối và tạo Hành động thứ hai có vẻ như không cần thiết.

dispatcher = Dispatcher.CurrentDispatcher;
Task task = Task.Factory.StartNew(() =>
{
    DoLongRunningWork();
});

Task UITask= task.ContinueWith(() =>
{
    dispatcher.Invoke(new Action(() =>
    {
        this.TextBlock1.Text = "Complete"; 
    }
});

Trong trường hợp ví dụ của bạn, bạn có thể sử dụng Control.Invoke(Action), tức là. TextBlock1.Invokethay vìdispatcher.Invoke
Đại tá Panic

2
Cảm ơn @ColonelPanic, nhưng tôi đã sử dụng WPF (như được gắn thẻ), không phải dạng winforms.
Greg Sansom

Câu trả lời:


352

Gọi tiếp tục với TaskScheduler.FromCurrentSynchronizationContext():

    Task UITask= task.ContinueWith(() =>
    {
     this.TextBlock1.Text = "Complete"; 
    }, TaskScheduler.FromCurrentSynchronizationContext());

Điều này chỉ phù hợp nếu bối cảnh thực hiện hiện tại nằm trên luồng UI.


39
Nó chỉ hợp lệ nếu bối cảnh thực hiện hiện tại nằm trên luồng UI. Nếu bạn đặt mã này bên trong một Tác vụ khác, thì bạn sẽ nhận được UnlimitedOperationException (xem phần Ngoại lệ )
stukselbax

3
Trong .NET 4.5 Câu trả lời của Johan Larsson nên được sử dụng như một cách tiêu chuẩn để tiếp tục nhiệm vụ trên luồng UI. Chỉ cần viết: đang chờ Nhiệm vụ.Run (DoLongRastyWork); this .extBlock1.Text = "Hoàn thành"; Xem thêm: blog.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Marcel W

1
Thx đã cứu mạng tôi. Tôi dành hàng giờ để tìm ra cách gọi những thứ chính trong vòng chờ / Tiếp tục. Đối với mọi người khác, cách sử dụng Google Firebase SDK cho Unity và vẫn có cùng một vấn đề, đây là một cách tiếp cận hiệu quả.
CHaP

2
@MarcelW - awaitlà một mẫu tốt - nhưng chỉ khi bạn ở trong một asyncbối cảnh (chẳng hạn như một phương thức được khai báo async). Nếu không, vẫn cần phải làm một cái gì đó như câu trả lời này.
ToolmakerSteve

33

Với async bạn chỉ cần làm:

await Task.Run(() => do some stuff);
// continue doing stuff on the same context as before.
// while it is the default it is nice to be explicit about it with:
await Task.Run(() => do some stuff).ConfigureAwait(true);

Tuy nhiên:

await Task.Run(() => do some stuff).ConfigureAwait(false);
// continue doing stuff on the same thread as the task finished on.

2
Các bình luận dưới falsephiên bản làm tôi bối rối. Tôi nghĩ falsecó nghĩa là nó có thể tiếp tục trên một chủ đề khác .
ToolmakerSteve

1
@ToolmakerSteve Phụ thuộc vào chủ đề mà bạn nghĩ đến. Chuỗi công nhân được sử dụng bởi Task.Run hoặc luồng người gọi? Hãy nhớ rằng, "cùng một luồng tác vụ đã hoàn thành" có nghĩa là luồng công nhân (tránh 'chuyển đổi' giữa các luồng). Ngoài ra, ConfigureAwait (true) không đảm bảo rằng điều khiển trả về cùng một luồng , chỉ cho cùng một bối cảnh (mặc dù sự khác biệt có thể không đáng kể).
Max Barraclough

@MaxBarraclough - Cảm ơn, tôi đã đọc sai "cùng chủ đề". tránh chuyển đổi giữa các luồng theo nghĩa tối đa hóa hiệu suất bằng cách sử dụng bất kỳ luồng nào đang chạy [để thực hiện tác vụ "làm một số thứ"], điều đó làm rõ cho tôi.
ToolmakerSteve

1
Câu hỏi không chỉ định nằm trong một asyncphương thức (cần thiết, để sử dụng await). Câu trả lời khi awaitkhông có sẵn là gì?
ToolmakerSteve

22

Nếu bạn có giá trị trả về, bạn cần gửi tới UI, bạn có thể sử dụng phiên bản chung như thế này:

Điều này đang được gọi từ một ViewModel MVVM trong trường hợp của tôi.

var updateManifest = Task<ShippingManifest>.Run(() =>
    {
        Thread.Sleep(5000);  // prove it's really working!

        // GenerateManifest calls service and returns 'ShippingManifest' object 
        return GenerateManifest();  
    })

    .ContinueWith(manifest =>
    {
        // MVVM property
        this.ShippingManifest = manifest.Result;

        // or if you are not using MVVM...
        // txtShippingManifest.Text = manifest.Result.ToString();    

        System.Diagnostics.Debug.WriteLine("UI manifest updated - " + DateTime.Now);

    }, TaskScheduler.FromCurrentSynchronizationContext());

Tôi đoán là = trước GenerateManifest là một lỗi đánh máy.
Sebastien F.

Vâng - Đã qua rồi! Cám ơn.
Simon_Weaver

11

Tôi chỉ muốn thêm phiên bản này vì đây là một chủ đề hữu ích và tôi nghĩ rằng đây là một triển khai rất đơn giản. Tôi đã sử dụng nhiều lần trong nhiều loại nếu ứng dụng đa luồng:

 Task.Factory.StartNew(() =>
      {
        DoLongRunningWork();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
              { txt.Text = "Complete"; }));
      });

2
Không bỏ phiếu vì đây là một giải pháp khả thi trong một số tình huống; tuy nhiên, câu trả lời được chấp nhận là cách tốt hơn. Nó không liên quan đến công nghệ ( TaskSchedulerlà một phần của BCL, Dispatcherkhông phải) và có thể được sử dụng để soạn các chuỗi nhiệm vụ phức tạp do không phải lo lắng về bất kỳ hoạt động không đồng bộ nào của lửa và quên (chẳng hạn như BeginInvoke).
Kirill Shlenskiy

@Kirill bạn có thể mở rộng một chút không, vì một số luồng SO đã nhất trí tuyên bố bộ điều phối là phương thức chính xác nếu sử dụng WPF của WinForms: Người ta có thể gọi một bản cập nhật GUI không đồng bộ (sử dụng BeginInvoke) hoặc đồng bộ hóa (Gọi) được sử dụng vì người ta không muốn chặn luồng nền chỉ để cập nhật GUI. Có phải FromCiverseSyn syncizationContext không đặt tác vụ tiếp tục vào hàng đợi thông báo luồng chính giống như cách của bộ điều phối không?
Trưởng khoa

1
Đúng, nhưng OP chắc chắn đang hỏi về WPF (và được gắn thẻ như vậy) và không muốn giữ một tham chiếu đến bất kỳ bộ điều phối nào (và tôi giả sử bất kỳ bối cảnh đồng bộ hóa nào - bạn chỉ có thể lấy thông tin này từ luồng chính và bạn phải lưu trữ một tài liệu tham khảo đến nó ở đâu đó). Đó là lý do tại sao tôi thích giải pháp mà tôi đã đăng: có một tham chiếu tĩnh an toàn luồng được xây dựng trong đó không yêu cầu điều này. Tôi nghĩ rằng điều này là vô cùng hữu ích trong bối cảnh WPF.
Trưởng khoa

3
Chỉ muốn củng cố nhận xét cuối cùng của tôi: Nhà phát triển không chỉ phải lưu trữ bối cảnh đồng bộ hóa, mà anh ấy / cô ấy phải biết rằng điều này chỉ có sẵn từ luồng chính; vấn đề này là nguyên nhân gây ra sự nhầm lẫn trong hàng tá câu hỏi SO: Mọi người luôn cố gắng để có được điều đó từ luồng công nhân. Nếu mã của họ đã được chuyển vào một luồng công nhân, thì nó không thành công vì vấn đề này. Vì vậy, vì sự phổ biến của WPF, điều này chắc chắn cần được làm rõ ở đây trong câu hỏi phổ biến này.
Trưởng khoa

1
... tuy nhiên, quan sát của Dean về [câu trả lời được chấp nhận] cần theo dõi bối cảnh đồng bộ nếu mã có thể không nằm trên luồng chính là điều quan trọng cần lưu ý và tránh đó là lợi ích của câu trả lời này.
ToolmakerSteve

1

Đến đây thông qua google bởi vì tôi đang tìm kiếm một cách tốt để thực hiện mọi thứ trên luồng ui sau khi ở trong một cuộc gọi Task.Run - Sử dụng mã sau đây bạn có thể sử dụng awaitđể quay lại UI Thread.

Tôi hi vọng điêu nay se giup được ai đo.

public static class UI
{
    public static DispatcherAwaiter Thread => new DispatcherAwaiter();
}

public struct DispatcherAwaiter : INotifyCompletion
{
    public bool IsCompleted => Application.Current.Dispatcher.CheckAccess();

    public void OnCompleted(Action continuation) => Application.Current.Dispatcher.Invoke(continuation);

    public void GetResult() { }

    public DispatcherAwaiter GetAwaiter()
    {
        return this;
    }
}

Sử dụng:

... code which is executed on the background thread...
await UI.Thread;
... code which will be run in the application dispatcher (ui thread) ...


Rất thông minh! Mặc dù không trực quan. Tôi đề nghị làm cho staticlớp học UI.
Theodor Zoulias
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.