Pertanyaan Cara yang tepat untuk menerapkan tugas yang tidak pernah berakhir. (Timer vs Tugas)


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

Lebih baik menggunakan System.Timers.Timer dan gunakan AutoReset untuk memastikan tidak melakukan tindakan sebelum "tick" sebelumnya selesai.

Atau haruskah saya menggunakan Tugas umum dalam mode LongRunning dengan token pembatalan, dan memiliki rutinitas tak terbatas sementara loop di dalamnya memanggil tindakan melakukan pekerjaan dengan Thread 10 detik. Tidur antar panggilan? Adapun model async / wait, saya tidak yakin itu akan tepat di sini karena saya tidak memiliki nilai kembali 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 hanya menggunakan pengatur waktu sederhana saat menggunakan properti AutoReset, dan memanggil .Stop () untuk membatalkannya?


75
2017-12-04 03:12


asal


Jawaban:


Saya akan menggunakan TPL Dataflow untuk ini (karena Anda menggunakan .NET 4.5 dan ia menggunakan Task secara internal). Anda dapat dengan mudah membuat ActionBlock<TInput> yang mengeposkan item ke dirinya sendiri setelah diproses tindakannya dan menunggu dalam jumlah waktu yang sesuai.

Pertama, buat pabrik yang akan membuat tugas Anda yang tidak pernah berakhir:

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 DateTimeOffset struktur; Anda harus melewati parameter jenis, dan mungkin juga melewati beberapa keadaan yang berguna (Anda dapat mengubah sifat negara, jika Anda mau).

Juga, perhatikan bahwa ActionBlock<TInput> dengan proses default saja satu item sekaligus, jadi Anda dijamin hanya satu tindakan yang akan diproses (artinya, Anda tidak perlu berurusan dengan itu reentrancy ketika memanggil Post metode penyuluhan kembali pada dirinya sendiri).

Saya juga telah lulus CancellationToken struktur untuk kedua konstruktor dari ActionBlock<TInput> dan ke Task.Delay metode panggilan; jika proses dibatalkan, pembatalan akan dilakukan pada kesempatan pertama yang memungkinkan.

Dari sana, ini merupakan refactoring yang mudah dari kode Anda untuk menyimpan ITargetBlock<DateTimeoffset> antarmuka dilaksanakan oleh ActionBlock<TInput> (ini adalah abstraksi tingkat tinggi yang mewakili blok yang merupakan konsumen, dan Anda ingin dapat memicu konsumsi melalui panggilan ke Post metode penyuluhan):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

Anda StartWork metode:

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 Anda StopWork metode:

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

Itu CreateNeverEndingTask Metode sekarang adalah pabrik yang menciptakan "layanan" Anda sehingga dapat berbicara. Anda mengontrol kapan mulai dan berhenti, dan itu sepenuhnya mandiri. Anda tidak perlu menjalin kontrol negara dari timer dengan aspek lain dari kode Anda. Anda cukup membuat blok, memulainya, dan menghentikannya setelah selesai.

Penggunaan benang / tugas / sumber daya yang lebih efisien

Penjadwal default untuk blok dalam aliran data TPL adalah sama untuk a Task, yang merupakan kolam ulir. Dengan menggunakan ActionBlock<TInput> untuk memproses tindakan Anda, serta panggilan ke Task.Delay, Anda menghasilkan kontrol dari benang yang Anda gunakan ketika Anda tidak benar-benar melakukan apa-apa. Memang, ini benar-benar mengarah ke beberapa overhead ketika Anda menelurkan yang baru Taskyang akan memproses kelanjutan, tetapi itu harus kecil, mengingat Anda tidak memproses ini dalam lingkaran yang ketat (Anda menunggu sepuluh detik di antara panggilan).

Jika itu DoWork fungsi sebenarnya dapat dibuat menunggu (yaitu, dalam mengembalikannya a Task), maka Anda dapat (mungkin) mengoptimalkan ini bahkan lebih dengan mengutak-atik metode pabrik di atas untuk mengambil Func<DateTimeOffset, CancellationToken, Task> bukannya sebuah 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, itu akan menjadi latihan yang baik untuk menenun CancellationToken melalui metode Anda (jika menerima satu), yang dilakukan di sini.

Itu berarti Anda akan memiliki DoWorkAsync metode dengan tanda tangan berikut:

Task DoWorkAsync(CancellationToken cancellationToken);

Anda harus berubah (hanya sedikit, dan Anda tidak mengalami masalah di sini) StartWork metode untuk memperhitungkan tanda tangan baru yang diteruskan ke CreateNeverEndingTask metode, 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);
}

88
2017-12-04 21:54



Saya menemukan antarmuka berbasis Tugas yang 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 pada 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 secara instan jika di dalam Task.Delay, daripada harus menunggu Thread.Sleep menyelesaikan.

Juga, menggunakan Task.Delay lebih Thread.Sleep berarti Anda tidak mengikat benang yang tidak melakukan apa pun selama tidur.

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


63
2017-12-04 03:33



Inilah yang saya temukan:

  • Mewarisi dari NeverEndingTask dan menimpa ExecutionCore metode dengan pekerjaan yang ingin Anda lakukan.
  • Berubah ExecutionLoopDelayMs memungkinkan Anda menyesuaikan waktu antara loop misalnya jika Anda ingin menggunakan algoritma backoff.
  • Start/Stop menyediakan antarmuka sinkron untuk memulai / menghentikan tugas.
  • LongRunning berarti Anda akan mendapatkan satu thread khusus per NeverEndingTask.
  • Kelas ini tidak mengalokasikan memori dalam satu lingkaran tidak seperti ActionBlock solusi 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();
    }
}

3
2018-06-07 00:29