Apakah ada pengganti berbasis tugas untuk System.Threading.Timer?

91

Saya baru mengenal .Net 4.0's Tasks dan saya tidak dapat menemukan apa yang saya pikir akan menjadi pengganti berbasis Tugas atau implementasi Timer, misalnya Tugas periodik. Apa ada yang seperti itu?

Pembaruan Saya datang dengan apa yang menurut saya merupakan solusi untuk kebutuhan saya yaitu membungkus fungsionalitas "Timer" di dalam Tugas dengan Tugas anak semua memanfaatkan CancellationToken dan mengembalikan Tugas untuk dapat berpartisipasi dalam langkah Tugas lebih lanjut.

public static Task StartPeriodicTask(Action action, int intervalInMilliseconds, int delayInMilliseconds, CancellationToken cancelToken)
{ 
    Action wrapperAction = () =>
    {
        if (cancelToken.IsCancellationRequested) { return; }

        action();
    };

    Action mainAction = () =>
    {
        TaskCreationOptions attachedToParent = TaskCreationOptions.AttachedToParent;

        if (cancelToken.IsCancellationRequested) { return; }

        if (delayInMilliseconds > 0)
            Thread.Sleep(delayInMilliseconds);

        while (true)
        {
            if (cancelToken.IsCancellationRequested) { break; }

            Task.Factory.StartNew(wrapperAction, cancelToken, attachedToParent, TaskScheduler.Current);

            if (cancelToken.IsCancellationRequested || intervalInMilliseconds == Timeout.Infinite) { break; }

            Thread.Sleep(intervalInMilliseconds);
        }
    };

    return Task.Factory.StartNew(mainAction, cancelToken);
}      
Jim
sumber
7
Anda harus menggunakan Timer di dalam Tugas daripada menggunakan mekanisme Thread.Sleep. Lebih efisien.
Yoann. B

Jawaban:

85

Itu tergantung pada 4,5, tetapi ini berhasil.

public class PeriodicTask
{
    public static async Task Run(Action action, TimeSpan period, CancellationToken cancellationToken)
    {
        while(!cancellationToken.IsCancellationRequested)
        {
            await Task.Delay(period, cancellationToken);

            if (!cancellationToken.IsCancellationRequested)
                action();
        }
     }

     public static Task Run(Action action, TimeSpan period)
     { 
         return Run(action, period, CancellationToken.None);
     }
}

Jelas Anda bisa menambahkan versi generik yang juga membutuhkan argumen. Ini sebenarnya mirip dengan pendekatan lain yang disarankan karena di bawah tenda Task. Penundaan menggunakan pengatur waktu kedaluwarsa sebagai sumber penyelesaian tugas.

Jeff
sumber
1
Saya beralih ke pendekatan ini sekarang. Tapi saya secara kondisional menelepon action()dengan pengulangan !cancelToken.IsCancellationRequested. Itu lebih baik, bukan?
HappyNomad
3
Terima kasih untuk ini - kami menggunakan yang sama tetapi telah memindahkan penundaan hingga setelah tindakan (ini lebih masuk akal bagi kami karena kami perlu segera memanggil tindakan kemudian ulangi setelah x)
Michael Parker
2
Terima kasih untuk ini. Tapi kode ini tidak akan berjalan "setiap X jam" itu akan berjalan "setiap X jam + waktu actioneksekusi" benar kan?
Alex
Benar. Anda akan membutuhkan beberapa matematika jika Anda ingin memperhitungkan waktu eksekusi. Namun itu bisa menjadi rumit jika waktu eksekusi melebihi periode Anda, dll ...
Jeff
57

UPDATE Saya menandai jawaban di bawah ini sebagai "jawaban" karena ini sudah cukup tua sekarang karena kita harus menggunakan pola async / await. Tidak perlu lagi meremehkan ini. LOL


Seperti yang dijawab Amy, belum ada implementasi periodik / timer berbasis Tugas. Namun, berdasarkan UPDATE asli saya, kami telah mengembangkan ini menjadi sesuatu yang sangat berguna dan produksi telah diuji. Pikir saya akan berbagi:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication7
{
    class Program
    {
        static void Main(string[] args)
        {
            Task perdiodicTask = PeriodicTaskFactory.Start(() =>
            {
                Console.WriteLine(DateTime.Now);
            }, intervalInMilliseconds: 2000, // fire every two seconds...
               maxIterations: 10);           // for a total of 10 iterations...

            perdiodicTask.ContinueWith(_ =>
            {
                Console.WriteLine("Finished!");
            }).Wait();
        }
    }

    /// <summary>
    /// Factory class to create a periodic Task to simulate a <see cref="System.Threading.Timer"/> using <see cref="Task">Tasks.</see>
    /// </summary>
    public static class PeriodicTaskFactory
    {
        /// <summary>
        /// Starts the periodic task.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds, i.e. how long it waits to kick off the timer.</param>
        /// <param name="duration">The duration.
        /// <example>If the duration is set to 10 seconds, the maximum time this task is allowed to run is 10 seconds.</example></param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create the task for executing the <see cref="Action"/>.</param>
        /// <returns>A <see cref="Task"/></returns>
        /// <remarks>
        /// Exceptions that occur in the <paramref name="action"/> need to be handled in the action itself. These exceptions will not be 
        /// bubbled up to the periodic task.
        /// </remarks>
        public static Task Start(Action action,
                                 int intervalInMilliseconds = Timeout.Infinite,
                                 int delayInMilliseconds = 0,
                                 int duration = Timeout.Infinite,
                                 int maxIterations = -1,
                                 bool synchronous = false,
                                 CancellationToken cancelToken = new CancellationToken(),
                                 TaskCreationOptions periodicTaskCreationOptions = TaskCreationOptions.None)
        {
            Stopwatch stopWatch = new Stopwatch();
            Action wrapperAction = () =>
            {
                CheckIfCancelled(cancelToken);
                action();
            };

            Action mainAction = () =>
            {
                MainPeriodicTaskAction(intervalInMilliseconds, delayInMilliseconds, duration, maxIterations, cancelToken, stopWatch, synchronous, wrapperAction, periodicTaskCreationOptions);
            };

            return Task.Factory.StartNew(mainAction, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        /// <summary>
        /// Mains the periodic task action.
        /// </summary>
        /// <param name="intervalInMilliseconds">The interval in milliseconds.</param>
        /// <param name="delayInMilliseconds">The delay in milliseconds.</param>
        /// <param name="duration">The duration.</param>
        /// <param name="maxIterations">The max iterations.</param>
        /// <param name="cancelToken">The cancel token.</param>
        /// <param name="stopWatch">The stop watch.</param>
        /// <param name="synchronous">if set to <c>true</c> executes each period in a blocking fashion and each periodic execution of the task
        /// is included in the total duration of the Task.</param>
        /// <param name="wrapperAction">The wrapper action.</param>
        /// <param name="periodicTaskCreationOptions"><see cref="TaskCreationOptions"/> used to create a sub task for executing the <see cref="Action"/>.</param>
        private static void MainPeriodicTaskAction(int intervalInMilliseconds,
                                                   int delayInMilliseconds,
                                                   int duration,
                                                   int maxIterations,
                                                   CancellationToken cancelToken,
                                                   Stopwatch stopWatch,
                                                   bool synchronous,
                                                   Action wrapperAction,
                                                   TaskCreationOptions periodicTaskCreationOptions)
        {
            TaskCreationOptions subTaskCreationOptions = TaskCreationOptions.AttachedToParent | periodicTaskCreationOptions;

            CheckIfCancelled(cancelToken);

            if (delayInMilliseconds > 0)
            {
                Thread.Sleep(delayInMilliseconds);
            }

            if (maxIterations == 0) { return; }

            int iteration = 0;

            ////////////////////////////////////////////////////////////////////////////
            // using a ManualResetEventSlim as it is more efficient in small intervals.
            // In the case where longer intervals are used, it will automatically use 
            // a standard WaitHandle....
            // see http://msdn.microsoft.com/en-us/library/vstudio/5hbefs30(v=vs.100).aspx
            using (ManualResetEventSlim periodResetEvent = new ManualResetEventSlim(false))
            {
                ////////////////////////////////////////////////////////////
                // Main periodic logic. Basically loop through this block
                // executing the action
                while (true)
                {
                    CheckIfCancelled(cancelToken);

                    Task subTask = Task.Factory.StartNew(wrapperAction, cancelToken, subTaskCreationOptions, TaskScheduler.Current);

                    if (synchronous)
                    {
                        stopWatch.Start();
                        try
                        {
                            subTask.Wait(cancelToken);
                        }
                        catch { /* do not let an errant subtask to kill the periodic task...*/ }
                        stopWatch.Stop();
                    }

                    // use the same Timeout setting as the System.Threading.Timer, infinite timeout will execute only one iteration.
                    if (intervalInMilliseconds == Timeout.Infinite) { break; }

                    iteration++;

                    if (maxIterations > 0 && iteration >= maxIterations) { break; }

                    try
                    {
                        stopWatch.Start();
                        periodResetEvent.Wait(intervalInMilliseconds, cancelToken);
                        stopWatch.Stop();
                    }
                    finally
                    {
                        periodResetEvent.Reset();
                    }

                    CheckIfCancelled(cancelToken);

                    if (duration > 0 && stopWatch.ElapsedMilliseconds >= duration) { break; }
                }
            }
        }

        /// <summary>
        /// Checks if cancelled.
        /// </summary>
        /// <param name="cancelToken">The cancel token.</param>
        private static void CheckIfCancelled(CancellationToken cancellationToken)
        {
            if (cancellationToken == null)
                throw new ArgumentNullException("cancellationToken");

            cancellationToken.ThrowIfCancellationRequested();
        }
    }
}

Keluaran:

2/18/2013 4:17:13 PM
2/18/2013 4:17:15 PM
2/18/2013 4:17:17 PM
2/18/2013 4:17:19 PM
2/18/2013 4:17:21 PM
2/18/2013 4:17:23 PM
2/18/2013 4:17:25 PM
2/18/2013 4:17:27 PM
2/18/2013 4:17:29 PM
2/18/2013 4:17:31 PM
Finished!
Press any key to continue . . .
Jim
sumber
1
Ini terlihat seperti kode yang bagus, tetapi saya bertanya-tanya apakah perlu sekarang karena ada kata kunci async / await. Bagaimana pendekatan Anda dibandingkan dengan yang di sini: stackoverflow.com/a/14297203/122781 ?
HappyNomad
1
@HappyNomad, sepertinya kelas PeriodicTaskFactory dapat memanfaatkan async / menunggu untuk aplikasi yang menargetkan .Net 4.5 tetapi bagi kami, kami belum dapat pindah ke .Net 4.5. Selain itu, PeriodicTaskFactory menyediakan beberapa mekanisme penghentian "pengatur waktu" tambahan seperti jumlah maksimum iterasi dan durasi maksimum serta menyediakan cara untuk memastikan setiap iterasi dapat menunggu pada iterasi terakhir. Tapi saya akan mencoba menyesuaikan ini untuk menggunakan async / menunggu ketika kita pindah ke .Net 4,5
Jim
4
+1 Saya menggunakan kelas Anda sekarang, terima kasih. Untuk membuatnya berfungsi baik dengan utas UI, saya harus memanggil TaskScheduler.FromCurrentSynchronizationContext()sebelum pengaturan mainAction. Saya kemudian meneruskan penjadwal yang dihasilkan ke MainPeriodicTaskActiondalamnya untuk membuat subTaskdengan.
HappyNomad
2
Saya tidak yakin, memblokir utas adalah ide yang bagus, jika dapat melakukan pekerjaan yang berguna. "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (intervalInMilliseconds, cancelToken)" ... Kemudian Anda menggunakan Timer, Anda menunggu di perangkat keras, jadi tidak ada thread yang dihabiskan. Tetapi dalam solusi Anda, utas dihabiskan untuk apa-apa.
RollingStone
2
@ Rollingstone saya setuju. Saya pikir solusi ini sebagian besar mengalahkan tujuan perilaku seperti asinkron. Jauh lebih baik menggunakan timer dan tidak menyia-nyiakan utas. Ini hanya memberikan tampilan asinkron tanpa manfaat apa pun.
Jeff
12

Ini tidak persis dalam System.Threading.Tasks, tetapi Observable.Timer(atau lebih sederhana Observable.Interval) dari perpustakaan Ekstensi Reaktif mungkin adalah apa yang Anda cari.

batu mstone
sumber
1
Misalnya Observable.Interval (TimeSpan.FromSeconds (1)). Subscribe (v => Debug.WriteLine (v));
Martin Capodici
1
Bagus, tetapi apakah konstruksi Reaktif itu dapat dimintai pendapat?
Shmil The Cat
9

Sampai sekarang saya menggunakan tugas LongRunning TPL untuk pekerjaan latar belakang terikat CPU siklik alih-alih pengatur waktu threading, karena:

  • tugas TPL mendukung pembatalan
  • pengatur waktu threading dapat memulai utas lain saat program dimatikan yang menyebabkan kemungkinan masalah dengan sumber daya yang dibuang
  • peluang untuk dibanjiri: pengatur waktu threading dapat memulai utas lain sementara yang sebelumnya masih diproses karena pekerjaan panjang yang tidak terduga (saya tahu, ini dapat dicegah dengan menghentikan dan memulai ulang pengatur waktu)

Namun, solusi TPL selalu mengklaim utas khusus yang tidak diperlukan sambil menunggu tindakan berikutnya (yang paling sering). Saya ingin menggunakan solusi yang diusulkan Jeff untuk melakukan pekerjaan siklik terikat CPU di latar belakang karena hanya memerlukan utas threadpool ketika ada pekerjaan yang harus dilakukan yang lebih baik untuk skalabilitas (terutama bila periode intervalnya besar).

Untuk mencapai itu, saya menyarankan 4 adaptasi:

  1. Tambahkan ConfigureAwait(false)ke Task.Delay()untuk mengeksekusi doWorktindakan pada utas kumpulan utas, jika tidak doWorkakan dilakukan pada utas pemanggil yang bukan merupakan gagasan paralelisme
  2. Tetap berpegang pada pola pembatalan dengan melemparkan TaskCanceledException (masih diperlukan?)
  3. Teruskan CancellationToken ke doWorkuntuk mengaktifkannya membatalkan tugas
  4. Tambahkan parameter tipe objek untuk memberikan informasi status tugas (seperti tugas TPL)

Tentang poin 2 Saya tidak yakin, apakah async menunggu masih memerlukan TaskCanceledExecption atau itu hanya praktik terbaik?

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }

Tolong berikan komentar Anda untuk solusi yang diusulkan ...

Perbarui 2016-8-30

Solusi di atas tidak segera memanggil doWork()tetapi dimulai dengan await Task.Delay().ConfigureAwait(false)untuk mencapai sakelar utas doWork(). Solusi di bawah ini mengatasi masalah ini dengan membungkus doWork()panggilan pertama dalam a Task.Run()dan menunggunya.

Di bawah ini adalah pengganti async \ await yang ditingkatkan untuk Threading.Timermenjalankan pekerjaan siklik yang dapat dibatalkan dan dapat diskalakan (dibandingkan dengan solusi TPL) karena tidak menempati utas apa pun saat menunggu tindakan berikutnya.

Perhatikan bahwa berlawanan dengan Timer, waktu tunggu ( period) adalah konstan dan bukan waktu siklus; waktu siklus adalah jumlah waktu tunggu dan durasinya doWork()dapat bervariasi.

    public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
    {
        await Task.Run(() => doWork(taskState, cancellationToken), cancellationToken).ConfigureAwait(false);
        do
        {
            await Task.Delay(period, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            doWork(taskState, cancellationToken);
        }
        while (true);
    }
Erik Stroeken
sumber
Penggunaan ConfigureAwait(false)akan menjadwalkan kelanjutan metode ke kumpulan utas, sehingga tidak benar-benar menyelesaikan poin kedua terkait pengatur waktu threading. Saya juga tidak berpikir taskStateperlu; tangkapan variabel lambda lebih fleksibel dan aman untuk jenis.
Stephen Cleary
1
Apa yang saya benar-benar ingin lakukan adalah untuk bertukar await Task.Delay()dan doWork()begitu doWork()akan segera mengeksekusi selama startup. Tetapi tanpa beberapa trik doWork()akan mengeksekusi thread pemanggil untuk pertama kalinya dan memblokirnya. Stephen, apa kamu punya solusi untuk masalah itu?
Erik Stroeken
1
Cara termudah adalah dengan membungkus semuanya dalam file Task.Run.
Stephen Cleary
Ya, tapi kemudian saya bisa kembali ke solusi TPL yang saya gunakan sekarang yang mengklaim utas selama loop berjalan dan dengan demikian kurang skalabel dari solusi ini.
Erik Stroeken
1

Saya perlu memicu tugas asinkron berulang dari metode sinkron.

public static class PeriodicTask
{
    public static async Task Run(
        Func<Task> action,
        TimeSpan period,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        while (!cancellationToken.IsCancellationRequested)
        {

            Stopwatch stopwatch = Stopwatch.StartNew();

            if (!cancellationToken.IsCancellationRequested)
                await action();

            stopwatch.Stop();

            await Task.Delay(period - stopwatch.Elapsed, cancellationToken);
        }
    }
}

Ini adalah adaptasi dari jawaban Jeff. Itu diubah untuk mengambil dalam Func<Task> Itu juga memastikan bahwa periode adalah seberapa sering itu dijalankan dengan mengurangi waktu berjalan tugas dari periode untuk penundaan berikutnya.

class Program
{
    static void Main(string[] args)
    {
        PeriodicTask
            .Run(GetSomething, TimeSpan.FromSeconds(3))
            .GetAwaiter()
            .GetResult();
    }

    static async Task GetSomething()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Console.WriteLine($"Hi {DateTime.UtcNow}");
    }
}
chris31389
sumber
0

Saya mengalami masalah serupa dan menulis TaskTimerkelas yang mengembalikan serangkaian tugas yang selesai pada timer: https://github.com/ikriv/tasktimer/ .

using (var timer = new TaskTimer(1000).Start())
{
    // Call DoStuff() every second
    foreach (var task in timer)
    {
        await task;
        DoStuff();
    }
}
Ivan Krivyakov
sumber
-1
static class Helper
{
    public async static Task ExecuteInterval(Action execute, int millisecond, IWorker worker)
    {
        while (worker.Worked)
        {
            execute();

            await Task.Delay(millisecond);
        }
    }
}


interface IWorker
{
    bool Worked { get; }
}

Sederhana...

nim
sumber