มีการแทนที่ตามงานสำหรับ System.Threading.Timer หรือไม่


91

ฉันเพิ่งเริ่มใช้ Tasks ของ. Net 4.0 และฉันไม่พบสิ่งที่คิดว่าจะเป็นการแทนที่งานตามงานหรือการใช้งานตัวจับเวลาเช่นงานตามระยะเวลา มีอย่างนั้นหรือ?

อัปเดต สิ่งที่ฉันคิดว่าเป็นวิธีแก้ปัญหาตามความต้องการของฉันซึ่งก็คือการรวมฟังก์ชัน "Timer" ไว้ในงานที่มี Tasks ย่อยโดยใช้ประโยชน์จาก Can CancelToken และส่งคืนงานเพื่อให้สามารถเข้าร่วมในขั้นตอนงานต่อไปได้

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
คุณควรใช้ Timer ภายในงานแทนการใช้กลไก Thread.Sleep มีประสิทธิภาพมากขึ้น
Yoann. B

คำตอบ:


87

ขึ้นอยู่กับ 4.5 แต่ใช้งานได้

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

เห็นได้ชัดว่าคุณสามารถเพิ่มเวอร์ชันทั่วไปที่รับข้อโต้แย้งได้เช่นกัน นี่เป็นวิธีที่คล้ายกับวิธีการอื่น ๆ ที่แนะนำเนื่องจากอยู่ภายใต้งานประทุน Delay ใช้การหมดอายุของตัวจับเวลาเป็นแหล่งที่มาของงาน


1
ตอนนี้ฉันเปลี่ยนมาใช้แนวทางนี้ แต่ผมมีเงื่อนไขเรียกด้วยซ้ำของaction() !cancelToken.IsCancellationRequestedดีกว่าใช่มั้ย?
HappyNomad

3
ขอบคุณสำหรับสิ่งนี้ - เราใช้แบบเดียวกัน แต่ได้เลื่อนการหน่วงเวลาไปจนถึงหลังการกระทำ (มันสมเหตุสมผลกว่าสำหรับเราเพราะเราต้องเรียกการกระทำทันทีแล้วทำซ้ำหลังจาก x)
Michael Parker

2
ขอบคุณสำหรับสิ่งนี้. แต่โค้ดนี้ไม่รัน "ทุก X ชั่วโมง" มันจะรัน "ทุก X ชั่วโมง + เวลาactionดำเนินการ" ใช่ไหม
Alex

แก้ไข. คุณจะต้องใช้คณิตศาสตร์หากคุณต้องการคำนวณเวลาดำเนินการ อย่างไรก็ตามอาจเป็นเรื่องยุ่งยากหากเวลาดำเนินการเกินระยะเวลาของคุณ ฯลฯ ...
เจฟฟ์

57

อัปเดต ฉันกำลังทำเครื่องหมายคำตอบด้านล่างเป็น "คำตอบ" เนื่องจากตอนนี้เก่าพอที่เราควรใช้รูปแบบ async / await ไม่จำเป็นต้องลดคะแนนอีกต่อไป ฮ่า ๆ


ตามที่ Amy ตอบไม่มีการใช้งานเป็นระยะ / จับเวลาตามภารกิจ อย่างไรก็ตามจาก UPDATE เดิมของฉันเราได้พัฒนาสิ่งนี้ให้เป็นสิ่งที่มีประโยชน์และผ่านการทดสอบการผลิตแล้ว คิดว่าฉันจะแบ่งปัน:

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

เอาท์พุต:

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
ดูเหมือนโค้ดที่ดี แต่ฉันสงสัยว่าตอนนี้จำเป็นหรือไม่ที่มีคีย์เวิร์ด async / await วิธีการของคุณเปรียบเทียบกับแนวทางที่นี่: stackoverflow.com/a/14297203/122781 ?
HappyNomad

1
@HappyNomad ดูเหมือนว่าคลาส PeriodicTaskFactory สามารถใช้ประโยชน์จาก async / await สำหรับแอปพลิเคชันที่กำหนดเป้าหมาย. Net 4.5 แต่สำหรับเราเรายังไม่สามารถย้ายไปที่. Net 4.5 ได้ นอกจากนี้ PeriodicTaskFactory ยังมีกลไกการยกเลิก "ตัวจับเวลา" เพิ่มเติมเช่นจำนวนการทำซ้ำสูงสุดและระยะเวลาสูงสุดตลอดจนวิธีการตรวจสอบให้แน่ใจว่าการทำซ้ำแต่ละครั้งสามารถรอการทำซ้ำครั้งสุดท้ายได้ แต่ฉันจะหาวิธีปรับให้ใช้ async / รอเมื่อเราย้ายไปที่. Net 4.5
Jim

4
+1 ฉันกำลังใช้ชั้นเรียนของคุณอยู่ขอบคุณ ที่จะได้ไปเล่นที่ดีกับหัวข้อ UI แม้ว่าฉันจะต้องโทรก่อนการตั้งค่าTaskScheduler.FromCurrentSynchronizationContext() mainActionจากนั้นฉันส่งตัวกำหนดตารางเวลาที่เป็นผลลัพธ์ไปMainPeriodicTaskActionเพื่อสร้างsubTaskด้วย.
HappyNomad

2
ฉันไม่แน่ใจว่านี่เป็นความคิดที่ดีที่จะบล็อกเธรดเมื่อมันสามารถทำงานได้อย่างมีประโยชน์ "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (intervalInMilliseconds, CancelToken)" ... จากนั้นคุณใช้ตัวจับเวลาคุณรอในฮาร์ดแวร์ดังนั้นจึงไม่มีเธรดที่ใช้แล้ว แต่ในการแก้ปัญหาของคุณเธรดจะถูกใช้ไปโดยเปล่าประโยชน์
RollingStone

2
@rollingstone ฉันเห็นด้วย ฉันคิดว่าวิธีแก้ปัญหานี้เอาชนะจุดประสงค์ของพฤติกรรมที่เหมือนแอซิงก์เป็นส่วนใหญ่ ดีกว่ามากในการใช้ตัวจับเวลาและไม่เปลืองด้าย นี่เป็นเพียงการแสดง async โดยไม่มีประโยชน์ใด ๆ
Jeff

12

มันไม่ได้อยู่ในอย่างแน่นอนSystem.Threading.Tasksแต่Observable.Timer(หรือง่ายกว่าObservable.Interval) จากไลบรารี Reactive Extensions อาจเป็นสิ่งที่คุณกำลังมองหา


1
เช่น Observable.Interval (TimeSpan.FromSeconds (1)) Subscribe (v => Debug.WriteLine (v));
Martin Capodici

1
ดี แต่โครงสร้างปฏิกิริยาเหล่านี้สามารถทำได้หรือไม่?
Shmil The Cat

9

จนถึงตอนนี้ฉันใช้งาน LongRunning TPL สำหรับการทำงานพื้นหลังที่ถูกผูกไว้กับซีพียูแบบวนรอบแทนการจับเวลาเธรดเนื่องจาก:

  • งาน TPL รองรับการยกเลิก
  • ตัวจับเวลาเธรดสามารถเริ่มเธรดอื่นได้ในขณะที่โปรแกรมกำลังปิดตัวลงทำให้เกิดปัญหากับทรัพยากรที่ถูกกำจัด
  • โอกาสในการโอเวอร์รัน: ตัวจับเวลาเธรดสามารถเริ่มเธรดอื่นได้ในขณะที่ก่อนหน้านี้ยังคงถูกประมวลผลเนื่องจากการทำงานที่ยาวนานโดยไม่คาดคิด (ฉันรู้ว่าสามารถป้องกันได้โดยการหยุดและรีสตาร์ทตัวจับเวลา)

อย่างไรก็ตามโซลูชัน TPL จะอ้างสิทธิ์เธรดเฉพาะเสมอซึ่งไม่จำเป็นในขณะที่รอการดำเนินการถัดไป (ซึ่งส่วนใหญ่แล้ว) ฉันต้องการใช้โซลูชันที่เสนอของเจฟฟ์เพื่อทำงานแบบวนรอบของ CPU บนพื้นหลังเพราะมันต้องการเธรดพูลเธรดเมื่อมีงานที่ต้องทำซึ่งดีกว่าสำหรับความสามารถในการปรับขนาดได้ (โดยเฉพาะอย่างยิ่งเมื่อช่วงเวลามีขนาดใหญ่)

เพื่อให้บรรลุเป้าหมายนี้ฉันขอแนะนำการดัดแปลง 4 ประการ:

  1. เพิ่มConfigureAwait(false)ลงในTask.Delay()การดำเนินdoWorkการกับเธรดพูลเธรดมิฉะนั้นdoWorkจะดำเนินการกับเธรดการเรียกซึ่งไม่ใช่แนวคิดของการขนานกัน
  2. ยึดติดกับรูปแบบการยกเลิกโดยการโยน TaskCanceledException (ยังจำเป็น?)
  3. ส่งต่อ Can CancelToken ไปที่doWorkเพื่อเปิดใช้งานเพื่อยกเลิกงาน
  4. เพิ่มพารามิเตอร์ของวัตถุชนิดเพื่อจัดหาข้อมูลสถานะงาน (เช่นงาน TPL)

เกี่ยวกับจุดที่ 2 ฉันไม่แน่ใจว่า async รอคอยยังคงต้องการ TaskCanceledExecption อยู่หรือไม่หรือเป็นเพียงแนวทางปฏิบัติที่ดีที่สุด

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

กรุณาแสดงความคิดเห็นของคุณเกี่ยวกับแนวทางแก้ไขที่เสนอ ...

อัปเดต 2016-8-30

การแก้ปัญหาดังกล่าวข้างต้นไม่ได้ทันทีโทรdoWork()แต่เริ่มต้นด้วยเพื่อให้เกิดการสลับด้ายสำหรับawait Task.Delay().ConfigureAwait(false) doWork()วิธีแก้ปัญหาด้านล่างนี้จะเอาชนะปัญหานี้ได้โดยการตัดdoWork()สายแรกเข้าTask.Run()และรอสาย

ด้านล่างนี้คือ async \ await ที่ได้รับการปรับปรุงใหม่สำหรับThreading.Timerการทำงานที่สามารถยกเลิกได้และสามารถปรับขนาดได้ (เมื่อเทียบกับโซลูชัน TPL) เนื่องจากไม่ได้ใช้เธรดใด ๆ ในขณะที่รอการดำเนินการถัดไป

โปรดทราบว่าในทางตรงกันข้ามกับ Timer เวลาที่รอ ( period) จะคงที่และไม่ใช่รอบเวลา รอบเวลาคือผลรวมของเวลารอคอยและระยะเวลาdoWork()ที่อาจแตกต่างกันไป

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

การใช้ConfigureAwait(false)จะกำหนดเวลาความต่อเนื่องของวิธีการไปยังเธรดพูลดังนั้นจึงไม่ได้แก้จุดที่สองของตัวจับเวลาเธรด ฉันก็ไม่คิดว่าtaskStateจำเป็น การจับตัวแปรแลมบ์ดามีความยืดหยุ่นและปลอดภัยกว่า
Stephen Cleary

1
สิ่งที่ผมต้องการจะทำคือการแลกเปลี่ยนawait Task.Delay()และdoWork()เพื่อให้doWork()ทันทีจะดำเนินการในช่วงเริ่มต้น แต่หากไม่มีเคล็ดลับบางอย่างdoWork()จะดำเนินการกับเธรดการโทรในครั้งแรกและบล็อก Stephen คุณมีทางออกสำหรับปัญหานั้นหรือไม่?
Erik Stroeken

1
วิธีที่ง่ายที่สุดคือห่อสิ่งทั้งหมดเป็นTask.Runไฟล์.
Stephen Cleary

ใช่ แต่ฉันสามารถกลับไปที่โซลูชัน TPL ที่ฉันใช้ตอนนี้ซึ่งอ้างสิทธิ์เธรดได้ตราบเท่าที่ลูปกำลังทำงานอยู่ดังนั้นจึงสามารถปรับขนาดได้น้อยกว่าโซลูชันนี้
Erik Stroeken

1

ฉันต้องการทริกเกอร์งานอะซิงโครนัสที่เกิดซ้ำจากวิธีซิงโครนัส

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

นี่เป็นการดัดแปลงคำตอบของเจฟฟ์ มันเปลี่ยนเป็นใช้เวลาFunc<Task> นอกจากนี้ยังตรวจสอบให้แน่ใจว่าช่วงเวลานั้นถูกเรียกใช้บ่อยเพียงใดโดยการหักเวลาทำงานของงานออกจากช่วงเวลาสำหรับการหน่วงเวลาถัดไป

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

ฉันวิ่งเข้าไปในปัญหาที่คล้ายกันและเขียนTaskTimerระดับที่ส่งกลับ seris ของงานที่เสร็จสมบูรณ์ในเวลา: 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; }
}

เรียบง่าย ...

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.