Cara yang tepat untuk menerapkan tugas yang tidak pernah berakhir. (Pengatur Waktu vs Tugas)

92

Jadi, aplikasi saya perlu melakukan tindakan hampir terus menerus (dengan jeda 10 detik atau lebih di antara setiap proses) selama aplikasi berjalan atau diminta pembatalan. Pekerjaan yang perlu dilakukan memiliki kemungkinan memakan waktu hingga 30 detik.

Apakah lebih baik menggunakan System.Timers.Timer dan menggunakan AutoReset untuk memastikan tidak melakukan tindakan sebelum "centang" sebelumnya selesai.

Atau haruskah saya menggunakan Tugas umum dalam mode LongRunning dengan token pembatalan, dan memiliki loop sementara tak terbatas di dalamnya yang memanggil tindakan yang melakukan pekerjaan dengan Thread.Sleep 10 detik di antara panggilan? Adapun model async / await, saya tidak yakin itu akan sesuai di sini karena saya tidak memiliki nilai yang dikembalikan dari pekerjaan.

CancellationTokenSource wtoken;
Task task;

void StopWork()
{
    wtoken.Cancel();

    try 
    {
        task.Wait();
    } catch(AggregateException) { }
}

void StartWork()
{
    wtoken = new CancellationTokenSource();

    task = Task.Factory.StartNew(() =>
    {
        while (true)
        {
            wtoken.Token.ThrowIfCancellationRequested();
            DoWork();
            Thread.Sleep(10000);
        }
    }, wtoken, TaskCreationOptions.LongRunning);
}

void DoWork()
{
    // Some work that takes up to 30 seconds but isn't returning anything.
}

atau cukup gunakan timer sederhana saat menggunakan properti AutoReset, dan panggil .Stop () untuk membatalkannya?

Josh
sumber
Tugas sepertinya berlebihan mengingat apa yang ingin Anda capai. en.wikipedia.org/wiki/KISS_principle . Hentikan pengatur waktu di awal OnTick (), periksa bool untuk melihat apakah Anda harus melakukan sesuatu dengan tidak, lakukan pekerjaan, mulai ulang Pengatur waktu setelah selesai.
Mike Trusov

Jawaban:

94

Saya akan menggunakan TPL Dataflow untuk ini (karena Anda menggunakan .NET 4.5 dan digunakan secara Taskinternal). Anda dapat dengan mudah membuat ActionBlock<TInput>item yang memposting ke dirinya sendiri setelah diproses tindakannya dan menunggu waktu yang sesuai.

Pertama, buat pabrik yang akan membuat tugas tanpa akhir Anda:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Action<DateTimeOffset> action, CancellationToken cancellationToken)
{
    // Validate parameters.
    if (action == null) throw new ArgumentNullException("action");

    // Declare the block variable, it needs to be captured.
    ActionBlock<DateTimeOffset> block = null;

    // Create the block, it will call itself, so
    // you need to separate the declaration and
    // the assignment.
    // Async so you can wait easily when the
    // delay comes.
    block = new ActionBlock<DateTimeOffset>(async now => {
        // Perform the action.
        action(now);

        // Wait.
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).
            // Doing this here because synchronization context more than
            // likely *doesn't* need to be captured for the continuation
            // here.  As a matter of fact, that would be downright
            // dangerous.
            ConfigureAwait(false);

        // Post the action back to the block.
        block.Post(DateTimeOffset.Now);
    }, new ExecutionDataflowBlockOptions { 
        CancellationToken = cancellationToken
    });

    // Return the block.
    return block;
}

Saya telah memilih ActionBlock<TInput>untuk mengambil DateTimeOffsetstruktur ; Anda harus meneruskan parameter type, dan mungkin juga meneruskan beberapa status yang berguna (Anda dapat mengubah sifat status, jika Anda mau).

Selain itu, perhatikan bahwa ActionBlock<TInput>secara default hanya memproses satu item dalam satu waktu, jadi Anda dijamin bahwa hanya satu tindakan yang akan diproses (artinya, Anda tidak perlu berurusan dengan reentrancy saat memanggil kembali Postmetode ekstensi itu sendiri).

Saya juga telah melewati CancellationTokenstruktur ke konstruktor ActionBlock<TInput>dan ke pemanggilan Task.Delaymetode ; jika proses dibatalkan, pembatalan akan dilakukan pada kesempatan pertama yang memungkinkan.

Dari sana, pemfaktoran ulang kode Anda mudah dilakukan untuk menyimpan ITargetBlock<DateTimeoffset>antarmuka yang diterapkan oleh ActionBlock<TInput>(ini adalah abstraksi tingkat yang lebih tinggi yang mewakili blok yang merupakan konsumen, dan Anda ingin dapat memicu konsumsi melalui panggilan ke Postmetode ekstensi):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

StartWorkMetode Anda :

void StartWork()
{
    // Create the token source.
    wtoken = new CancellationTokenSource();

    // Set the task.
    task = CreateNeverEndingTask(now => DoWork(), wtoken.Token);

    // Start the task.  Post the time.
    task.Post(DateTimeOffset.Now);
}

Dan kemudian StopWorkmetode Anda :

void StopWork()
{
    // CancellationTokenSource implements IDisposable.
    using (wtoken)
    {
        // Cancel.  This will cancel the task.
        wtoken.Cancel();
    }

    // Set everything to null, since the references
    // are on the class level and keeping them around
    // is holding onto invalid state.
    wtoken = null;
    task = null;
}

Mengapa Anda ingin menggunakan TPL Dataflow di sini? Beberapa alasan:

Pemisahan kekhawatiran

The CreateNeverEndingTaskMetode sekarang menjadi pabrik yang menciptakan "layanan" Anda sehingga untuk berbicara. Anda mengontrol kapan itu mulai dan berhenti, dan itu benar-benar mandiri. Anda tidak perlu menjalin kontrol status pengatur waktu dengan aspek lain dari kode Anda. Anda cukup membuat blok, memulainya, dan menghentikannya setelah selesai.

Penggunaan utas / tugas / sumber daya yang lebih efisien

Penjadwal default untuk blok dalam aliran data TPL adalah sama untuk a Task, yang merupakan kumpulan utas. Dengan menggunakan ActionBlock<TInput>to memproses tindakan Anda, serta panggilan ke Task.Delay, Anda menghasilkan kendali atas utas yang Anda gunakan saat Anda tidak benar-benar melakukan apa pun. Memang, ini sebenarnya mengarah ke beberapa overhead saat Anda menelurkan yang baru Taskyang akan memproses kelanjutan, tetapi itu harus kecil, mengingat Anda tidak memproses ini dalam loop ketat (Anda menunggu sepuluh detik di antara pemanggilan).

Jika DoWorkfungsi benar-benar dapat dibuat menjadi menunggu (yaitu, dalam mengembalikan a Task), maka Anda (mungkin) dapat mengoptimalkan ini lebih banyak lagi dengan mengubah metode pabrik di atas untuk mengambil Func<DateTimeOffset, CancellationToken, Task>alih - alih Action<DateTimeOffset>, seperti:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Func<DateTimeOffset, CancellationToken, Task> action, 
    CancellationToken cancellationToken)
{
    // Validate parameters.
    if (action == null) throw new ArgumentNullException("action");

    // Declare the block variable, it needs to be captured.
    ActionBlock<DateTimeOffset> block = null;

    // Create the block, it will call itself, so
    // you need to separate the declaration and
    // the assignment.
    // Async so you can wait easily when the
    // delay comes.
    block = new ActionBlock<DateTimeOffset>(async now => {
        // Perform the action.  Wait on the result.
        await action(now, cancellationToken).
            // Doing this here because synchronization context more than
            // likely *doesn't* need to be captured for the continuation
            // here.  As a matter of fact, that would be downright
            // dangerous.
            ConfigureAwait(false);

        // Wait.
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken).
            // Same as above.
            ConfigureAwait(false);

        // Post the action back to the block.
        block.Post(DateTimeOffset.Now);
    }, new ExecutionDataflowBlockOptions { 
        CancellationToken = cancellationToken
    });

    // Return the block.
    return block;
}

Tentu saja, akan menjadi praktik yang baik untuk merangkai CancellationTokenmetode Anda (jika menerimanya), yang dilakukan di sini.

Itu berarti Anda kemudian akan memiliki DoWorkAsyncmetode dengan tanda tangan berikut:

Task DoWorkAsync(CancellationToken cancellationToken);

Anda harus mengubah (hanya sedikit, dan Anda tidak mengeluarkan pemisahan masalah di sini) StartWorkmetode untuk memperhitungkan tanda tangan baru yang diteruskan ke CreateNeverEndingTaskmetode, seperti:

void StartWork()
{
    // Create the token source.
    wtoken = new CancellationTokenSource();

    // Set the task.
    task = CreateNeverEndingTask((now, ct) => DoWorkAsync(ct), wtoken.Token);

    // Start the task.  Post the time.
    task.Post(DateTimeOffset.Now, wtoken.Token);
}
casperOne
sumber
Halo, saya mencoba penerapan ini tetapi saya menghadapi masalah. Jika DoWork saya tidak menerima argumen, task = CreateNeverEndingTask (now => DoWork (), wtoken.Token); memberi saya kesalahan versi (tipe tidak cocok). Di sisi lain, jika DoWork saya mengambil parameter DateTimeOffset, baris yang sama tersebut memberi saya kesalahan versi yang berbeda, yang memberi tahu saya bahwa tidak ada kelebihan beban untuk DoWork membutuhkan 0 argumen. Maukah Anda membantu saya mencari tahu yang ini?
Bovaz
1
Sebenarnya, saya menyelesaikan masalah saya dengan menambahkan cast ke baris tempat saya menetapkan tugas dan meneruskan parameter ke DoWork: task = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (now => DoWork (now), wtoken.Token);
Bovaz
Anda juga bisa mengubah jenis "ActionBlock <DateTimeOffset> task;" ke tugas ITargetBlock <DateTimeOffset>;
XOR
1
Saya yakin ini mungkin akan mengalokasikan memori selamanya, sehingga pada akhirnya menyebabkan overflow.
Nate Gardner
@NateGardner Di bagian mana?
casperOne
75

Saya menemukan antarmuka berbasis Tugas baru menjadi sangat sederhana untuk melakukan hal-hal seperti ini - bahkan lebih mudah daripada menggunakan kelas Timer.

Ada beberapa penyesuaian kecil yang dapat Anda lakukan untuk contoh Anda. Dari pada:

task = Task.Factory.StartNew(() =>
{
    while (true)
    {
        wtoken.Token.ThrowIfCancellationRequested();
        DoWork();
        Thread.Sleep(10000);
    }
}, wtoken, TaskCreationOptions.LongRunning);

Kamu bisa melakukan ini:

task = Task.Run(async () =>  // <- marked async
{
    while (true)
    {
        DoWork();
        await Task.Delay(10000, wtoken.Token); // <- await with cancellation
    }
}, wtoken.Token);

Dengan cara ini pembatalan akan terjadi seketika jika di dalam Task.Delay, daripada harus menunggu Thread.Sleepsampai selesai.

Selain itu, menggunakan Task.Delaylebih Thread.Sleepberarti Anda tidak mengikat utas tanpa melakukan apa pun selama tidur.

Jika Anda bisa, Anda juga dapat DoWork()menerima token pembatalan, dan pembatalan akan jauh lebih responsif.

porges
sumber
1
Whatch apa tugas yang akan Anda dapatkan jika Anda menggunakan lambda async sebagai parameter dari Task.Factory.StartNew - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx Ketika Anda melakukan task.Wait ( ); setelah pembatalan diminta, Anda akan menunggu tugas yang salah.
Lukas Pirkl
Ya, ini seharusnya menjadi Task. Jalankan sekarang, yang memiliki kelebihan beban yang benar.
Porges
Menurut http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx sepertinya Task.Runmenggunakan kumpulan utas, jadi contoh Anda yang menggunakan Task.Runalih-alih Task.Factory.StartNewwith TaskCreationOptions.LongRunningtidak melakukan hal yang persis sama - jika saya memerlukan tugas untuk menggunakan LongRunningopsi, apakah saya tidak dapat menggunakan Task.Runseperti yang Anda tunjukkan, atau saya kehilangan sesuatu?
Jeff
@Lumirris: Inti dari async / await adalah untuk menghindari mengikat utas selama utas dijalankan (di sini, selama panggilan Tunda, tugas tidak menggunakan utas). Jadi menggunakan LongRunningagak tidak sesuai dengan tujuan tidak mengikat utas. Jika Anda ingin menjamin berjalan pada utasnya sendiri, Anda dapat menggunakannya, tetapi di sini Anda akan memulai utas yang tertidur hampir sepanjang waktu. Apa kasus penggunaannya?
Porges
@Porges Point diambil. Kasus penggunaan saya akan menjadi tugas yang menjalankan loop tak terbatas, di mana setiap iterasi akan melakukan sebagian pekerjaan, dan 'bersantai' selama 2 detik sebelum melakukan chuck pekerjaan lain pada iterasi berikutnya. Ini berjalan selamanya, tetapi mengambil jeda 2 detik biasa. Komentar saya, bagaimanapun, lebih tentang apakah Anda dapat menentukannya LongRunningmenggunakan Task.Runsintaks. Dari dokumentasi, sepertinya Task.Runsintaks yang lebih bersih, selama Anda senang dengan pengaturan default yang digunakannya. Tampaknya tidak ada kelebihan beban yang membutuhkan TaskCreationOptionsargumen.
Jeff
4

Inilah yang saya dapatkan:

  • Mewarisi dari NeverEndingTaskdan mengganti ExecutionCoremetode dengan pekerjaan yang ingin Anda lakukan.
  • Mengubah ExecutionLoopDelayMsmemungkinkan Anda untuk mengatur waktu antara loop, misalnya jika Anda ingin menggunakan algoritma backoff.
  • Start/Stop menyediakan antarmuka sinkron untuk memulai / menghentikan tugas.
  • LongRunningberarti Anda akan mendapatkan satu utas khusus per NeverEndingTask.
  • Kelas ini tidak mengalokasikan memori dalam loop tidak seperti ActionBlocksolusi berbasis di atas.
  • Kode di bawah ini adalah sketsa, belum tentu kode produksi :)

:

public abstract class NeverEndingTask
{
    // Using a CTS allows NeverEndingTask to "cancel itself"
    private readonly CancellationTokenSource _cts = new CancellationTokenSource();

    protected NeverEndingTask()
    {
         TheNeverEndingTask = new Task(
            () =>
            {
                // Wait to see if we get cancelled...
                while (!_cts.Token.WaitHandle.WaitOne(ExecutionLoopDelayMs))
                {
                    // Otherwise execute our code...
                    ExecutionCore(_cts.Token);
                }
                // If we were cancelled, use the idiomatic way to terminate task
                _cts.Token.ThrowIfCancellationRequested();
            },
            _cts.Token,
            TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning);

        // Do not forget to observe faulted tasks - for NeverEndingTask faults are probably never desirable
        TheNeverEndingTask.ContinueWith(x =>
        {
            Trace.TraceError(x.Exception.InnerException.Message);
            // Log/Fire Events etc.
        }, TaskContinuationOptions.OnlyOnFaulted);

    }

    protected readonly int ExecutionLoopDelayMs = 0;
    protected Task TheNeverEndingTask;

    public void Start()
    {
       // Should throw if you try to start twice...
       TheNeverEndingTask.Start();
    }

    protected abstract void ExecutionCore(CancellationToken cancellationToken);

    public void Stop()
    {
        // This code should be reentrant...
        _cts.Cancel();
        TheNeverEndingTask.Wait();
    }
}
Jack Ukleja
sumber