Kapan TaskCompletionSource <T> harus digunakan?


199

AFAIK, yang diketahuinya adalah bahwa pada titik tertentu, metode SetResultatau SetExceptiondipanggil untuk menyelesaikan Task<T>eksposur melalui Taskpropertinya.

Dengan kata lain, ia bertindak sebagai produsen untuk Task<TResult>dan penyelesaiannya.

Saya melihat contohnya di sini :

Jika saya memerlukan cara untuk menjalankan Func asynchronous dan memiliki Tugas untuk mewakili operasi itu.

public static Task<T> RunAsync<T>(Func<T> function) 
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ => 
    { 
        try 
        {  
            T result = function(); 
            tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
    }); 
    return tcs.Task; 
}

Yang dapat digunakan * jika saya tidak memiliki Task.Factory.StartNew- Tapi aku tidak memiliki Task.Factory.StartNew.

Pertanyaan:

Dapatkah seseorang tolong jelaskan dengan contoh skenario yang berhubungan langsung dengan TaskCompletionSource dan bukan dengan situasi hipotetis di mana saya tidak punya Task.Factory.StartNew?


5
TaskCompletionSource terutama digunakan untuk membungkus api async berdasarkan peristiwa dengan Tugas tanpa membuat Thread baru.
Arvis

Jawaban:


230

Saya kebanyakan menggunakannya ketika hanya API berbasis acara yang tersedia ( misalnya soket Windows Phone 8 ):

public Task<Args> SomeApiWrapper()
{
    TaskCompletionSource<Args> tcs = new TaskCompletionSource<Args>(); 

    var obj = new SomeApi();

    // will get raised, when the work is done
    obj.Done += (args) => 
    {
        // this will notify the caller 
        // of the SomeApiWrapper that 
        // the task just completed
        tcs.SetResult(args);
    }

    // start the work
    obj.Do();

    return tcs.Task;
}

Jadi ini sangat berguna ketika digunakan bersama dengan asynckata kunci C # 5 .


4
dapatkah kamu menulis dengan kata-kata apa yang kita lihat di sini? apakah itu seperti SomeApiWrappermenunggu di suatu tempat, sampai penerbit mengangkat acara yang menyebabkan tugas ini selesai?
Royi Namir

lihat tautan yang baru saya tambahkan
GameScripting

6
Hanya pembaruan, Microsoft telah merilis Microsoft.Bcl.Asyncpaket pada NuGet yang memungkinkan async/awaitkata kunci dalam proyek .NET 4.0 (VS2012 dan lebih tinggi direkomendasikan).
Erik

1
@ Fran_gg7 Anda dapat menggunakan CancellingToken, lihat msdn.microsoft.com/en-us/library/dd997396(v=vs.110).aspx atau sebagai pertanyaan baru di sini di stackoverflow
GameScripting

1
Masalah dengan implementasi ini adalah bahwa ini menghasilkan kebocoran memori karena acara tidak pernah dilepaskan dari obj. Selesai
Walter Vehoeven

78

Dalam pengalaman saya, TaskCompletionSourcesangat bagus untuk membungkus pola asinkron lama dengan pola modern async/await.

Contoh paling bermanfaat yang bisa saya pikirkan adalah ketika bekerja dengannya Socket. Ini memiliki pola APM dan EAP lama, tetapi bukan awaitable Taskmetode itu TcpListenerdan TcpClientmiliki.

Saya pribadi punya beberapa masalah dengan NetworkStreamkelas dan lebih suka yang mentah Socket. Karena saya juga menyukai async/awaitpolanya, saya membuat kelas ekstensi SocketExtenderyang menciptakan beberapa metode ekstensi untuk Socket.

Semua metode ini digunakan TaskCompletionSource<T>untuk membungkus panggilan asinkron seperti:

    public static Task<Socket> AcceptAsync(this Socket socket)
    {
        if (socket == null)
            throw new ArgumentNullException("socket");

        var tcs = new TaskCompletionSource<Socket>();

        socket.BeginAccept(asyncResult =>
        {
            try
            {
                var s = asyncResult.AsyncState as Socket;
                var client = s.EndAccept(asyncResult);

                tcs.SetResult(client);
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);
            }

        }, socket);

        return tcs.Task;
    }

Saya meneruskan socketke BeginAcceptmetode sehingga saya mendapatkan sedikit peningkatan kinerja dari kompiler tidak harus mengerek parameter lokal.

Maka keindahan dari semuanya:

 var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
 listener.Bind(new IPEndPoint(IPAddress.Loopback, 2610));
 listener.Listen(10);

 var client = await listener.AcceptAsync();

1
Mengapa Task.Factory.StartNew tidak bekerja di sini?
Tola Odejayi

23
@Tola Seperti itu akan menciptakan tugas baru yang berjalan di utas threadpool, tetapi kode di atas menggunakan utas penyelesaian i / o yang dimulai oleh BeginAccept, iow: ia tidak memulai utas baru.
Frans Bouma

4
Terima kasih, @ Frans-Bouma. Jadi TaskCompletionSource adalah cara praktis untuk mengubah kode yang menggunakan pernyataan Begin ... End ... menjadi tugas?
Tola Odejayi

3
@TolaOdejayi Sedikit dari jawaban yang terlambat, tapi ya itu adalah salah satu kasus penggunaan utama yang saya temukan untuk itu. Ia bekerja dengan sangat baik untuk transisi kode ini.
Erik

4
Lihatlah TaskFactory <TResult> .FromAsync untuk membungkus Begin.. End...pernyataan.
MicBig

37

Bagi saya, skenario klasik untuk digunakan TaskCompletionSourceadalah ketika mungkin bahwa metode saya tidak perlu harus melakukan operasi yang memakan waktu. Apa yang memungkinkan kami lakukan adalah memilih kasus tertentu di mana kami ingin menggunakan utas baru.

Contoh yang baik untuk ini adalah ketika Anda menggunakan cache. Anda dapat memiliki GetResourceAsyncmetode, yang terlihat di cache untuk sumber daya yang diminta dan kembali sekaligus (tanpa menggunakan utas baru, dengan menggunakan TaskCompletionSource) jika sumber itu ditemukan. Hanya jika sumber daya tidak ditemukan, kami ingin menggunakan utas baru dan mengambilnya menggunakan Task.Run().

Contoh kode dapat dilihat di sini: Cara menjalankan kode secara kondisional menggunakan tugas


Saya memang melihat pertanyaan Anda dan juga jawabannya. (lihat komentar saya untuk jawabannya) .... :-) dan memang itu adalah pertanyaan dan jawaban yang mendidik.
Royi Namir

11
Ini sebenarnya bukan situasi di mana TCS diperlukan. Anda cukup menggunakannya Task.FromResultuntuk melakukan ini. Tentu saja, jika Anda menggunakan 4.0 dan tidak memiliki Task.FromResultapa yang Anda gunakan untuk menggunakan TCS adalah menulis sendiri FromResult .
Servy

@Servy Task.FromResulthanya tersedia sejak .NET 4.5. Sebelum itu, itulah cara untuk mencapai perilaku ini.
Adi Lester

@AdiLester Anda menjawab merujuk Task.Run, yang menunjukkan 4.5+. Dan komentar saya sebelumnya secara khusus membahas .NET 4.0.
Servy

@Servy Tidak semua orang yang membaca jawaban ini menargetkan .NET 4.5+. Saya percaya ini adalah jawaban yang baik dan valid yang membantu orang mengajukan pertanyaan OP (yang omong-omong ditandai .NET-4.0). Either way, downvoting sepertinya agak banyak bagi saya, tetapi jika Anda benar-benar percaya itu layak downvote, silakan.
Adi Lester

25

Dalam posting blog ini , Levi Botelho menjelaskan cara menggunakan TaskCompletionSourceuntuk menulis bungkus asinkron untuk Proses sehingga Anda dapat meluncurkannya dan menunggu penghentiannya.

public static Task RunProcessAsync(string processPath)
{
    var tcs = new TaskCompletionSource<object>();
    var process = new Process
    {
        EnableRaisingEvents = true,
        StartInfo = new ProcessStartInfo(processPath)
        {
            RedirectStandardError = true,
            UseShellExecute = false
        }
    };
    process.Exited += (sender, args) =>
    {
        if (process.ExitCode != 0)
        {
            var errorMessage = process.StandardError.ReadToEnd();
            tcs.SetException(new InvalidOperationException("The process did not exit correctly. " +
                "The corresponding error message was: " + errorMessage));
        }
        else
        {
            tcs.SetResult(null);
        }
        process.Dispose();
    };
    process.Start();
    return tcs.Task;
}

dan penggunaannya

await RunProcessAsync("myexecutable.exe");

14

Sepertinya tidak ada yang disebutkan, tapi saya kira tes unit juga dapat dianggap cukup nyata .

Saya menemukan TaskCompletionSourceberguna ketika mengejek ketergantungan dengan metode async.

Dalam program aktual yang sedang diuji:

public interface IEntityFacade
{
  Task<Entity> GetByIdAsync(string id);
}

Dalam tes unit:

// set up mock dependency (here with NSubstitute)

TaskCompletionSource<Entity> queryTaskDriver = new TaskCompletionSource<Entity>();

IEntityFacade entityFacade = Substitute.For<IEntityFacade>();

entityFacade.GetByIdAsync(Arg.Any<string>()).Returns(queryTaskDriver.Task);

// later on, in the "Act" phase

private void When_Task_Completes_Successfully()
{
  queryTaskDriver.SetResult(someExpectedEntity);
  // ...
}

private void When_Task_Gives_Error()
{
  queryTaskDriver.SetException(someExpectedException);
  // ...
}

Lagi pula, penggunaan TaskCompletionSource ini tampaknya merupakan kasus lain dari "objek Tugas yang tidak mengeksekusi kode".


11

TaskCompletionSource digunakan untuk membuat objek Tugas yang tidak mengeksekusi kode. Dalam skenario dunia nyata, TaskCompletionSource sangat ideal untuk operasi terikat I / O. Dengan cara ini, Anda mendapatkan semua manfaat tugas (misalnya nilai pengembalian, kelanjutan, dll) tanpa memblokir utas selama operasi. Jika "fungsi" Anda adalah operasi terikat I / O, tidak disarankan untuk memblokir utas menggunakan Tugas baru . Alih-alih, menggunakan TaskCompletionSource , Anda dapat membuat tugas budak untuk menunjukkan kapan operasi terikat I / O Anda selesai atau ada kesalahan.


5

Ada contoh dunia nyata dengan penjelasan yang layak dalam posting ini dari blog "Pemrograman Paralel dengan .NET" . Anda benar-benar harus membacanya, tetapi inilah ringkasannya.

Posting blog menunjukkan dua implementasi untuk:

"metode pabrik untuk membuat tugas" tertunda ", tugas yang tidak benar-benar dijadwalkan sampai beberapa waktu habis yang disediakan pengguna terjadi."

Implementasi pertama yang ditunjukkan didasarkan pada Task<>dan memiliki dua kelemahan utama. Pos implementasi kedua berlanjut untuk mengurangi ini dengan menggunakan TaskCompletionSource<>.

Inilah implementasi kedua:

public static Task StartNewDelayed(int millisecondsDelay, Action action)
{
    // Validate arguments
    if (millisecondsDelay < 0)
        throw new ArgumentOutOfRangeException("millisecondsDelay");
    if (action == null) throw new ArgumentNullException("action");

    // Create a trigger used to start the task
    var tcs = new TaskCompletionSource<object>();

    // Start a timer that will trigger it
    var timer = new Timer(
        _ => tcs.SetResult(null), null, millisecondsDelay, Timeout.Infinite);

    // Create and return a task that will be scheduled when the trigger fires.
    return tcs.Task.ContinueWith(_ =>
    {
        timer.Dispose();
        action();
    });
}

akan lebih baik untuk menggunakan menunggu di tcs.Task dan kemudian gunakan tindakan () setelah
Royi Namir

5
Karena Anda kembali ke konteks di mana Anda pergi, di mana Continuewith tidak mempertahankan konteks. (tidak secara default) juga jika pernyataan berikutnya dalam tindakan () menyebabkan pengecualian, akan sulit untuk menangkapnya di mana menggunakan menunggu akan menunjukkan kepada Anda sebagai pengecualian biasa.
Royi Namir

3
Mengapa tidak adil await Task.Delay(millisecondsDelay); action(); return;atau (dalam. Net 4.0)return Task.Delay(millisecondsDelay).ContinueWith( _ => action() );
sgnsajgon

@sgnsajgon yang tentunya akan lebih mudah dibaca dan dikelola
JwJosefy

@JwJosefy Sebenarnya, metode Task.Delay dapat diimplementasikan dengan menggunakan TaskCompletionSource , mirip dengan kode di atas. Implementasi sesungguhnya ada di sini: Task.cs
sgnsajgon

4

Ini mungkin terlalu menyederhanakan hal-hal tetapi sumber TaskCompletion memungkinkan seseorang untuk menunggu acara. Karena tcs.SetResult hanya diatur setelah peristiwa terjadi, pemanggil dapat menunggu tugas.

Tonton video ini untuk wawasan lebih lanjut:

http://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Lucian03-TipsForAsyncThreadsAndDatabinding


1
Harap tempatkan kode atau dokumentasi yang relevan di sini karena tautan dapat berubah dari waktu ke waktu dan menjadikan jawaban ini tidak relevan.
rfornal

3

Skenario dunia nyata yang pernah saya gunakan TaskCompletionSourceadalah ketika mengimplementasikan antrian unduhan. Dalam kasus saya jika pengguna memulai 100 unduhan, saya tidak ingin memadamkan semuanya sekaligus dan alih-alih mengembalikan tugas yang sudah ditentukan, saya mengembalikan tugas yang dilampirkan TaskCompletionSource. Setelah unduhan selesai, utas yang berfungsi, antrian menyelesaikan tugas.

Konsep kuncinya di sini adalah bahwa saya melakukan decoupling ketika seorang klien meminta sebuah tugas dimulai dari saat ia benar-benar dimulai. Dalam hal ini karena saya tidak ingin klien harus berurusan dengan manajemen sumber daya.

perhatikan bahwa Anda dapat menggunakan async / menunggu di .net 4 selama Anda menggunakan kompiler C # 5 (VS 2012+) lihat di sini untuk lebih jelasnya.


0

Saya sudah terbiasa TaskCompletionSourcemenjalankan Tugas sampai dibatalkan. Dalam hal ini adalah pelanggan ServiceBus yang biasanya ingin saya jalankan selama aplikasi berjalan.

public async Task RunUntilCancellation(
    CancellationToken cancellationToken,
    Func<Task> onCancel)
{
    var doneReceiving = new TaskCompletionSource<bool>();

    cancellationToken.Register(
        async () =>
        {
            await onCancel();
            doneReceiving.SetResult(true); // Signal to quit message listener
        });

    await doneReceiving.Task.ConfigureAwait(false); // Listen until quit signal is received.
}

1
Tidak perlu menggunakan 'async' dengan 'TaskCompletionSource' karena telah membuat tugas
Mandeep Janjua

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.