Có sự thay thế dựa trên Tác vụ cho System.Threading.Timer không?


88

Tôi mới làm quen với Nhiệm vụ của .Net 4.0 và tôi không thể tìm thấy những gì tôi nghĩ sẽ là sự thay thế dựa trên Nhiệm vụ hoặc triển khai Bộ hẹn giờ, ví dụ: Nhiệm vụ định kỳ. Có một điều như vậy?

Cập nhật Tôi đã nghĩ ra một giải pháp cho nhu cầu của mình đó là gói chức năng "Hẹn giờ" bên trong một Nhiệm vụ với các Nhiệm vụ con, tất cả các Nhiệm vụ con đều tận dụng CancelToken và trả lại Nhiệm vụ để có thể tham gia vào các bước Nhiệm vụ tiếp theo.

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
Bạn nên sử dụng Bộ hẹn giờ bên trong Tác vụ thay vì sử dụng cơ chế Thread.Sleep. Nó hiệu quả hơn.
Yoann. B

Câu trả lời:


84

Nó phụ thuộc vào 4,5, nhưng điều này hoạt động.

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

Rõ ràng là bạn có thể thêm một phiên bản chung cũng có đối số. Điều này thực sự tương tự với các cách tiếp cận được đề xuất khác vì Task.Delay đang sử dụng thời gian hết hạn hẹn giờ làm nguồn hoàn thành nhiệm vụ.


1
Tôi đã chuyển sang cách tiếp cận này ngay bây giờ. Nhưng tôi có điều kiện gọi action()với một lần lặp lại !cancelToken.IsCancellationRequested. Điều đó tốt hơn, phải không?
HappyNomad

3
Cảm ơn cho điều này - chúng ta đang sử dụng giống nhau nhưng đã chuyển sự chậm trễ cho đến khi sau khi hành động (nó có ý nghĩa hơn đối với chúng ta khi chúng ta cần phải gọi hành động ngay lập tức sau đó lặp lại sau khi x)
Michael Parker

1
Cảm ơn vì điều đó. Nhưng mã này sẽ không chạy "mỗi X giờ" mà nó sẽ chạy "mỗi X giờ + thời gian actionthực hiện" đúng không?
Alex

Chính xác. Bạn sẽ cần một số phép toán nếu bạn muốn tính toán thời gian thực hiện. Tuy nhiên có thể có được khôn lanh, nếu thời gian thực hiện vượt quá thời gian của bạn, vv ...
Jeff

57

CẬP NHẬT Tôi đánh dấu câu trả lời dưới đây là "câu trả lời" vì câu trả lời này đã đủ cũ để chúng ta nên sử dụng mẫu async / await. Không cần phải phản đối điều này nữa. cười lớn


Như Amy đã trả lời, không có việc triển khai định kỳ / hẹn giờ dựa trên Nhiệm vụ. Tuy nhiên, dựa trên CẬP NHẬT ban đầu của tôi, chúng tôi đã phát triển điều này thành một thứ khá hữu ích và đã được thử nghiệm sản xuất. Tôi nghĩ rằng tôi sẽ chia sẻ:

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

Đầu ra:

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
Đây trông giống như một đoạn mã tuyệt vời, nhưng tôi đang tự hỏi liệu có cần thiết bây giờ không khi có các từ khóa async / await. Cách tiếp cận của bạn so với cách tiếp cận ở đây: stackoverflow.com/a/14297203/122781 như thế nào?
HappyNomad

1
@HappyNomad, có vẻ như lớp PeriodicTaskFactory có thể tận dụng async / await cho các ứng dụng nhắm mục tiêu .Net 4.5 nhưng đối với chúng tôi, chúng tôi chưa thể chuyển sang .Net 4.5. Ngoài ra, PeriodicTaskFactory cung cấp một số cơ chế kết thúc "hẹn giờ" bổ sung như số lần lặp tối đa và thời lượng tối đa cũng như cung cấp một cách để đảm bảo mỗi lần lặp có thể chờ vào lần lặp cuối cùng. Nhưng tôi sẽ tìm cách để thích ứng này để sử dụng async / chờ đợi khi chúng ta chuyển sang Net 4.5
Jim

4
+1 Tôi đang sử dụng lớp học của bạn bây giờ, cảm ơn. Tuy nhiên, để làm cho nó hoạt động tốt với chuỗi giao diện người dùng, tôi phải gọi TaskScheduler.FromCurrentSynchronizationContext()trước khi thiết lập mainAction. Sau đó, tôi chuyển trình lập lịch kết quả vào MainPeriodicTaskActionđể tạo subTaskvới.
HappyNomad

2
Tôi không chắc, đây là một ý tưởng hay để chặn một luồng, khi nó có thể hoạt động hữu ích. "Thread.Sleep (delayInMilliseconds)", "periodResetEvent.Wait (khoảngInMilliseconds, hủyToken)" ... Sau đó, bạn sử dụng Timer, bạn đợi trong phần cứng, vì vậy không có luồng nào được chi tiêu. Nhưng trong giải pháp của bạn, chủ đề được sử dụng để làm gì.
RollingStone

2
@rollingstone Tôi đồng ý. Tôi nghĩ rằng giải pháp này phần lớn đánh bại mục đích của hành vi giống như không đồng bộ. Tốt hơn nhiều nếu sử dụng bộ đếm thời gian và không lãng phí luồng. Điều này chỉ mang lại sự xuất hiện của async mà không có bất kỳ lợi ích nào.
Jeff

12

Nó không chính xác trong System.Threading.Tasks, nhưng Observable.Timer(hoặc đơn giản hơn Observable.Interval) từ thư viện Reactive Extensions có thể là thứ bạn đang tìm kiếm.


1
Ví dụ: Observable.Interval (TimeSpan.FromSeconds (1)). Subscribe (v => Debug.WriteLine (v));
Martin Capodici

1
Tốt, nhưng những cấu trúc Reactive đó có thể xếp được không?
Shmil The Cat

9

Cho đến bây giờ tôi đã sử dụng tác vụ LongRunning TPL cho công việc nền liên kết CPU theo chu kỳ thay vì bộ đếm thời gian phân luồng, bởi vì:

  • nhiệm vụ TPL hỗ trợ hủy bỏ
  • bộ đếm thời gian phân luồng có thể bắt đầu một luồng khác trong khi chương trình đang tắt gây ra sự cố có thể xảy ra với tài nguyên đã xử lý
  • cơ hội cho việc chạy quá mức: bộ đếm thời gian phân luồng có thể bắt đầu một chuỗi khác trong khi chuỗi trước đó vẫn đang được xử lý do công việc dài không mong muốn (tôi biết, có thể ngăn chặn bằng cách dừng và khởi động lại bộ hẹn giờ)

Tuy nhiên, giải pháp TPL luôn yêu cầu một luồng chuyên dụng không cần thiết trong khi chờ hành động tiếp theo (hầu hết thời gian). Tôi muốn sử dụng giải pháp được đề xuất của Jeff để thực hiện công việc tuần hoàn ràng buộc CPU trên nền bởi vì nó chỉ cần một luồng threadpool khi có việc phải làm, điều này tốt hơn cho khả năng mở rộng (đặc biệt khi khoảng thời gian lớn).

Để đạt được điều đó, tôi sẽ đề xuất 4 cách điều chỉnh:

  1. Thêm ConfigureAwait(false)vào Task.Delay()để thực hiện doWorkhành động trên luồng nhóm luồng, nếu không doWorksẽ được thực hiện trên luồng đang gọi mà không phải là ý tưởng của song song
  2. Bám sát mẫu hủy bằng cách ném TaskCanceledException (vẫn bắt buộc?)
  3. Chuyển tiếp CancelToken đến doWorkđể cho phép nó hủy tác vụ
  4. Thêm một tham số của đối tượng kiểu để cung cấp thông tin trạng thái tác vụ (như tác vụ TPL)

Về điểm 2, tôi không rõ, liệu async await vẫn yêu cầu TaskCanceledExecption hay chỉ là phương pháp hay nhất?

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

Hãy đóng góp ý kiến ​​của bạn cho giải pháp được đề xuất ...

Cập nhật 2016-8-30

Giải pháp trên không gọi ngay lập tức doWork()mà bắt đầu với await Task.Delay().ConfigureAwait(false)để đạt được chuyển đổi luồng cho doWork(). Giải pháp dưới đây khắc phục sự cố này bằng cách gói doWork()cuộc gọi đầu tiên trong một Task.Run()và chờ nó.

Dưới đây là thay thế async \ await được cải tiến để Threading.Timerthực hiện công việc theo chu kỳ có thể hủy bỏ và có thể mở rộng (so với giải pháp TPL) vì nó không chiếm bất kỳ luồng nào trong khi chờ hành động tiếp theo.

Lưu ý rằng ngược lại với Timer, thời gian chờ ( period) là không đổi và không phải là thời gian chu kỳ; thời gian chu kỳ là tổng của thời gian chờ đợi và thời gian doWork()có thể thay đổi.

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

Việc sử dụng ConfigureAwait(false)sẽ lập lịch trình tiếp tục của phương thức tới nhóm luồng, vì vậy nó không thực sự giải quyết được điểm thứ hai là bộ đếm thời gian phân luồng. Tôi cũng không nghĩ taskStatelà cần thiết; chụp biến lambda linh hoạt hơn và an toàn hơn.
Stephen Cleary

1
Những gì tôi thực sự muốn làm là trao đổi await Task.Delay()doWork()vì vậy doWork()sẽ ngay lập tức thực thi trong quá trình khởi động. Nhưng nếu không có một số thủ thuật doWork()sẽ thực thi trên chuỗi gọi lần đầu tiên và chặn nó. Stephen, bạn có giải pháp cho vấn đề đó không?
Erik Stroeken

1
Cách đơn giản nhất là chỉ cần gói toàn bộ trong một Task.Run.
Stephen Cleary,

Có, nhưng sau đó tôi chỉ có thể quay lại giải pháp TPL mà tôi sử dụng bây giờ yêu cầu một luồng miễn là vòng lặp đang chạy và do đó, giải pháp này ít khả năng mở rộng hơn.
Erik Stroeken

1

Tôi cần kích hoạt các tác vụ không đồng bộ định kỳ từ một phương pháp đồng bộ.

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

Đây là sự phỏng theo câu trả lời của Jeff. Nó được thay đổi để nhận một Func<Task> Nó cũng đảm bảo rằng khoảng thời gian là tần suất nó được chạy bằng cách trừ thời gian chạy của nhiệm vụ từ khoảng thời gian cho lần trì hoãn tiếp theo.

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

Tôi đã gặp phải sự cố tương tự và đã viết một TaskTimerlớp trả về một loạt các nhiệm vụ hoàn thành đúng thời gian: 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; }
}

Đơn giản...

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.