System.Threading.Timer için Görev tabanlı bir yedek var mı?


91

.Net 4.0 Görevleri konusunda yeniyim ve bir Zamanlayıcının, örneğin periyodik bir Görev gibi, Görev tabanlı bir değiştirme veya uygulama olacağını düşündüğüm şeyi bulamadım. Böyle bir şey var mı?

Güncelleme İhtiyaçlarıma yönelik bir çözüm olduğunu düşündüğüm bir şey buldum, bu da "Zamanlayıcı" işlevselliğini tüm CancellationToken'dan yararlanarak bir Görev içinde alt Görevlerle sarmalamak ve Görev adımlarına katılabilmek için Görevi geri döndürmek.

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

7
Thread.Sleep mekanizmasını kullanmak yerine Task içinde bir Timer kullanmalısınız. Daha verimli.
Yoann. B

Yanıtlar:


87

4.5'e bağlıdır, ancak bu işe yarar.

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

Açıkçası, argümanlar da alan genel bir sürüm ekleyebilirsiniz. Task.Delay başlık altında görev tamamlama kaynağı olarak bir zamanlayıcı sona erme kullandığından bu aslında önerilen diğer yaklaşımlara benzer.


1
Az önce bu yaklaşıma geçtim. Ama şartlı olarak action()tekrar ediyorum !cancelToken.IsCancellationRequested. Bu daha iyi, değil mi?
HappyNomad

3
Bunun için teşekkürler - aynısını kullanıyoruz, ancak gecikmeyi eylem sonrasına taşıdık (eylemi hemen çağırmamız ve x'in ardından tekrar etmemiz gerektiğinden bize daha mantıklı geliyor)
Michael Parker

2
Bunun için teşekkürler. Ancak bu kod "her X saatte bir" çalışmayacak, "her X saatte + actionyürütme sırasında" çalışacak, değil mi?
Alex

Doğru. Yürütme zamanını hesaba katmak istiyorsanız biraz matematiğe ihtiyacınız olacaktır. Ancak, uygulama süresi sürenizi aşarsa, bu biraz yanıltıcı olabilir, vb ...
Jeff

57

GÜNCELLEME Ben am aşağıda cevabını işaretleme bu yaşlı yeterince biz zaman uyumsuz / bekliyoruz kalıbı kullanarak gerektiğini şimdi olduğundan "yanıt" olarak. Artık bunu olumsuz oylamaya gerek yok. LOL


Amy'nin yanıtladığı gibi, Görevli tabanlı periyodik / zamanlayıcı uygulaması yoktur. Ancak, orijinal GÜNCELLEME'ime dayanarak, bunu oldukça faydalı bir şeye dönüştürdük ve üretimde test ettik. Paylaşacağımı düşündüm:

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

Çıktı:

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 . . .

1
Bu harika bir kod gibi görünüyor, ancak async / await anahtar sözcükleri olduğu için gerekli olup olmadığını merak ediyorum. Yaklaşımınız buradaki yaklaşımla nasıl karşılaştırılır: stackoverflow.com/a/14297203/122781 ?
HappyNomad 13

1
@HappyNomad, PeriodicTaskFactory sınıfı .Net 4.5'i hedefleyen uygulamalar için zaman uyumsuz / bekleme avantajından yararlanabilecek gibi görünüyor, ancak bizim için henüz .Net 4.5'e geçemiyoruz. Ayrıca, PeriodicTaskFactory, maksimum yineleme sayısı ve maksimum süre gibi bazı ek "zamanlayıcı" sonlandırma mekanizmaları sağlar ve her yinelemenin son yinelemede beklemesini sağlamak için bir yol sağlar. Ama bunu .Net 4.5'e geçtiğimizde async / await'i kullanmak için uyarlamaya çalışacağım
Jim

4
+1 Şimdi sınıfınızı kullanıyorum, teşekkürler. UI iş parçacığı ile iyi oynamasını sağlamak için, TaskScheduler.FromCurrentSynchronizationContext()ayarlamadan önce aramam gerekiyor mainAction. Daha sonra ortaya çıkan zamanlayıcıyı MainPeriodicTaskAction, subTaskile oluşturması için içine iletiyorum .
HappyNomad 13

2
Emin değilim, bu bir iş parçacığını yararlı bir iş yapabildiğinde engellemek için iyi bir fikirdir. "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (intervalInMilliseconds, cancelToken)" ... Daha sonra bir Timer kullanırsınız, donanımda beklersiniz, böylece hiç iş parçacığı harcanmaz. Ancak çözümünüzde iş parçacıkları boşuna harcanır.
RollingStone

2
@rollingstone Kabul ediyorum. Bence bu çözüm, eşzamansız benzeri davranışın amacını büyük ölçüde ortadan kaldırıyor. Zamanlayıcı kullanmak ve ipliği boşa harcamamak çok daha iyi. Bu, hiçbir faydası olmadan zaman uyumsuz bir görünüm vermektir.
Jeff


9

Şimdiye kadar iş parçacığı zamanlayıcı yerine döngüsel CPU'ya bağlı arka plan çalışması için bir LongRunning TPL görevi kullandım, çünkü:

  • TPL görevi iptali destekler
  • iş parçacığı zamanlayıcısı, program kapanırken başka bir iş parçacığı başlatabilir ve atılan kaynaklarla olası sorunlara neden olabilir
  • taşma şansı: iş parçacığı zamanlayıcı, beklenmedik uzun çalışma nedeniyle önceki işlenirken başka bir iş parçacığı başlatabilir (biliyorum, zamanlayıcıyı durdurup yeniden başlatarak önlenebilir)

Bununla birlikte, TPL çözümü her zaman, bir sonraki eylemi beklerken (çoğu zaman bu) gerekli olmayan özel bir iş parçacığı talep eder. Arka planda CPU'ya bağlı döngüsel çalışma gerçekleştirmek için Jeff'in önerilen çözümünü kullanmak istiyorum, çünkü ölçeklenebilirlik için daha iyi olan, yapılacak iş olduğunda yalnızca bir iş parçacığı iş parçacığına ihtiyaç duyar (özellikle aralık dönemi büyük olduğunda).

Bunu başarmak için 4 uyarlama öneririm:

  1. Ekle ConfigureAwait(false)için Task.Delay()yürütülecek doWorkbir iş parçacığı havuzu iş parçacığı üzerinde eylem aksi doWorkparalellik fikri olmadığı çağıran iş parçacığı üzerinde yapılacak
  2. Bir TaskCanceledException oluşturarak iptal modeline bağlı kalın (yine de gerekli mi?)
  3. doWorkGörevi iptal etmesini sağlamak için CancellationToken'ı iletin
  4. Görev durumu bilgilerini sağlamak için nesne türünde bir parametre ekleyin (bir TPL görevi gibi)

2. nokta hakkında emin değilim, eşzamansız bekleme hala TaskCanceledExecption gerektiriyor mu yoksa sadece en iyi uygulama mı?

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

Lütfen önerilen çözüme yorumlarınızı yazın ...

2016-8-30 Güncellemesi

Yukarıdaki çözüm hemen aramaz, doWork()ancak await Task.Delay().ConfigureAwait(false)iş parçacığı anahtarını elde etmek için başlar doWork(). Aşağıdaki çözüm, ilk doWork()aramayı a karakterine sararak ve bekleyerek bu sorunun üstesinden Task.Run()gelir.

Aşağıda, Threading.Timeriptal edilebilir döngüsel çalışma gerçekleştiren ve ölçeklenebilir olan (TPL çözümüne kıyasla), bir sonraki eylemi beklerken herhangi bir iş parçacığı işgal etmediği için iyileştirilmiş zaman uyumsuz \ bekleme değiştirme yer almaktadır.

Zamanlayıcının aksine, bekleme süresinin ( period) sabit olduğunu ve döngü süresinin olmadığını unutmayın; döngü süresi, bekleme süresinin toplamıdır ve süresi doWork()değişebilir.

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

Kullanımı ConfigureAwait(false), yöntemin iş parçacığı havuzuna devam etmesini planlayacaktır, böylece iş parçacığı zamanlayıcı olan ikinci noktayı gerçekten çözmez. Ayrıca taskStategerekli olduğunu düşünmüyorum ; lambda değişken yakalama daha esnek ve tür güvenlidir.
Stephen Cleary

1
Gerçekten yapmak istediğim şey değiş tokuş yapmak await Task.Delay()ve doWork()bu yüzden doWork()başlangıç ​​sırasında hemen yürütülür. Ancak bazı hileler olmadan doWork()ilk kez çağıran iş parçacığı üzerinde çalıştırılır ve onu engeller. Stephen, bu soruna bir çözümün var mı?
Erik Stroeken

1
En kolay yol, her şeyi bir Task.Run.
Stephen Cleary

Evet, ama sonra şimdi kullandığım TPL çözümüne geri dönebilirim; bu, döngü çalıştığı sürece bir iş parçacığı olduğunu iddia eder ve bu nedenle bu çözümden daha az ölçeklenebilir.
Erik Stroeken

1

Yinelenen eşzamansız görevleri eşzamanlı bir yöntemden tetiklemem gerekiyordu.

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

Bu Jeff'in cevabının bir uyarlamasıdır. Ayrıca, Func<Task> görevin çalışma süresini bir sonraki gecikme süresinden çıkararak sürenin ne sıklıkla çalıştırıldığından emin olur.

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

0

Benzer bir sorunla karşılaştım ve TaskTimerzamanlayıcıda tamamlanan bir dizi görev döndüren bir sınıf yazdım : https://github.com/ikriv/tasktimer/ .

using (var timer = new TaskTimer(1000).Start())
{
    // Call DoStuff() every second
    foreach (var task in timer)
    {
        await task;
        DoStuff();
    }
}

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

Basit...

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.