Kelanjutan tugas pada utas UI

214

Apakah ada cara 'standar' untuk menentukan bahwa kelanjutan tugas harus dijalankan pada utas dari mana tugas awal dibuat?

Saat ini saya memiliki kode di bawah ini - itu berfungsi tetapi melacak pengirim dan membuat Tindakan kedua sepertinya overhead yang tidak perlu.

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

Task UITask= task.ContinueWith(() =>
{
    dispatcher.Invoke(new Action(() =>
    {
        this.TextBlock1.Text = "Complete"; 
    }
});
Greg Sansom
sumber
Dalam kasus contoh Anda, Anda bisa menggunakan Control.Invoke(Action), yaitu. TextBlock1.Invokealih-alihdispatcher.Invoke
Kolonel Panic
2
Terima kasih @ ColonelPanic, tapi saya menggunakan WPF (seperti yang ditandai), bukan winforms.
Greg Sansom

Jawaban:

352

Sebut kelanjutan dengan TaskScheduler.FromCurrentSynchronizationContext():

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

Ini hanya cocok jika konteks eksekusi saat ini ada di utas UI.

Greg Sansom
sumber
39
Ini valid hanya jika konteks eksekusi saat ini ada pada utas UI. Jika Anda memasukkan kode ini ke dalam Tugas lain, maka Anda mendapatkan InvalidOperationException (lihat bagian Pengecualian )
stukselbax
3
Dalam .NET 4.5 jawaban Johan Larsson harus digunakan sebagai cara standar untuk kelanjutan tugas pada utas UI. Cukup tulis: tunggu Task.Run (DoLongRunningWork); this.TextBlock1.Text = "Lengkap"; Lihat juga: blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Marcel W
1
Terima kasih telah menyelamatkan hidup saya. Saya menghabiskan waktu berjam-jam untuk mencari tahu bagaimana memanggil hal-hal utas utama dalam menunggu / ContinueWith. Bagi semua orang bagaimana menggunakan Google Firebase SDK untuk Unity dan masih memiliki masalah yang sama, ini adalah pendekatan yang berfungsi.
CHaP
2
@MarcelW - awaitadalah pola yang baik - tetapi hanya jika Anda berada di dalam asynckonteks (seperti metode yang dideklarasikan async). Jika tidak, masih perlu melakukan sesuatu seperti jawaban ini.
ToolmakerSteve
33

Dengan async, Anda cukup melakukan:

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

Namun:

await Task.Run(() => do some stuff).ConfigureAwait(false);
// continue doing stuff on the same thread as the task finished on.
Johan Larsson
sumber
2
Komentar di bawah falseversi membingungkan saya. Saya pikir falseberarti itu dapat berlanjut pada utas yang berbeda .
ToolmakerSteve
1
@ToolmakerSteve Tergantung pada utas yang Anda pikirkan. Utas pekerja digunakan oleh Task.Run, atau utas penelepon? Ingat, "utas yang sama dengan tugas yang selesai" adalah utas pekerja (menghindari 'pergantian' antar utas). Selain itu, ConfigureAwait (true) tidak menjamin bahwa kontrol kembali ke utas yang sama , hanya untuk konteks yang sama (meskipun perbedaannya mungkin tidak signifikan).
Max Barraclough
@ MaxBarraclough - Terima kasih, saya salah membaca "thread yang sama" yang dimaksud. menghindari beralih di antara utas dalam arti memaksimalkan kinerja dengan menggunakan utas apa pun yang sedang berjalan [untuk melakukan tugas "lakukan beberapa hal"], yang menjelaskannya untuk saya.
ToolmakerSteve
1
Pertanyaannya tidak menentukan berada di dalam suatu asyncmetode (yang diperlukan, untuk digunakan await). Apa jawabannya ketika awaittidak tersedia?
ToolmakerSteve
22

Jika Anda memiliki nilai balik yang perlu Anda kirim ke UI, Anda dapat menggunakan versi generik seperti ini:

Ini dipanggil dari ViewModel MVVM dalam kasus saya.

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());
Simon_Weaver
sumber
Saya menduga = sebelum GenerateManifest adalah kesalahan ketik.
Sebastien F.
Ya - Hilang sekarang! Terima kasih.
Simon_Weaver
11

Saya hanya ingin menambahkan versi ini karena ini adalah utas yang sangat berguna dan saya pikir ini adalah implementasi yang sangat sederhana. Saya telah menggunakan ini berkali-kali dalam berbagai jenis jika aplikasi multithreaded:

 Task.Factory.StartNew(() =>
      {
        DoLongRunningWork();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
              { txt.Text = "Complete"; }));
      });
Dekan
sumber
2
Tidak downvoting karena ini merupakan solusi yang layak dalam beberapa skenario; Namun, jawaban yang diterima jauh lebih baik. Ini adalah teknologi-agnostik ( TaskSchedulermerupakan bagian dari BCL, Dispatcherbukan) dan dapat digunakan untuk menyusun rantai tugas yang kompleks karena tidak perlu khawatir tentang operasi async api-dan-lupa (seperti BeginInvoke).
Kirill Shlenskiy
@ Kirill dapatkah Anda sedikit memperluas, karena beberapa utas SO telah dengan suara bulat menyatakan dispatcher menjadi metode yang benar jika menggunakan WPF dari WinForms: Seseorang dapat meminta pembaruan GUI baik secara sinkron (menggunakan BeginInvoke) atau secara serempak (Invoke), meskipun biasanya async digunakan karena orang tidak ingin memblokir utas latar belakang hanya untuk pembaruan GUI. Apakah FromCurrentSynchronizationContext tidak memasukkan tugas lanjutan ke dalam antrian pesan utas dengan cara yang sama seperti dispatcher?
Dean
1
Benar, tetapi OP tentu bertanya tentang WPF (dan menandainya begitu), dan tidak ingin menyimpan referensi ke operator mana pun (dan saya menganggap konteks sinkronisasi apa pun juga - Anda hanya bisa mendapatkan ini dari utas utama dan Anda harus menyimpan referensi ke suatu tempat). Itulah sebabnya saya suka solusi yang saya posting: ada referensi statis aman thread yang dibangun di yang tidak memerlukan ini. Saya pikir ini sangat berguna dalam konteks WPF.
Dean
3
Hanya ingin memperkuat komentar terakhir saya: Pengembang tidak hanya harus menyimpan konteks sinkronisasi, tetapi ia juga harus tahu bahwa ini hanya tersedia dari utas utama; masalah ini telah menyebabkan kebingungan dalam lusinan pertanyaan SO: Orang-orang setiap saat mencoba mendapatkannya dari thread pekerja. Jika kode mereka sendiri telah dipindahkan ke utas pekerja, itu gagal karena masalah ini. Jadi karena prevalensi WPF, ini pasti harus diklarifikasi di sini dalam pertanyaan populer ini.
Dean
1
... namun demikian, pengamatan Dean tentang [jawaban yang diterima] perlu melacak konteks sinkronisasi jika kode mungkin tidak ada di utas utama penting untuk dicatat, dan menghindari itu adalah manfaat dari jawaban ini.
ToolmakerSteve
1

Sampai di sini melalui google karena saya sedang mencari cara yang baik untuk melakukan hal-hal di utas ui setelah berada di dalam panggilan Task.Run - Menggunakan kode berikut yang dapat Anda gunakan awaituntuk kembali ke UI Thread lagi.

Saya harap ini membantu seseorang.

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

Pemakaian:

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

Dbl
sumber
Sangat pintar! Cukup tidak intuitif. Saya sarankan membuat statickelas UI.
Theodor Zoulias