2011-02-03 56 views
70

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

Cập nhật tôi đến với những gì tôi nghĩ là một giải pháp cho nhu cầu của tôi mà là để quấn "Timer" chức năng bên trong một công tác với nhiệm vụ con tất cả lợi dụng việc CancellationToken và trả về nhiệm vụ được có thể tham gia 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. –

Trả lời

61

Nó phụ thuộc vào 4.5, nhưng công trình này.

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 bạn cũng có thể thêm phiên bản chung cũng có đối số. Điều này thực sự tương tự như các phương pháp tiếp cận được đề xuất khác kể từ dưới Task.Delay mui xe đang sử dụng hết hạn bộ hẹn giờ làm nguồn hoàn thành nhiệm vụ.

+1

Tôi đã chuyển sang phương pháp này ngay bây giờ. Nhưng tôi có điều kiện gọi 'action()' với lặp lại '! CancelToken.IsCancellationRequested'. Tốt hơn, phải không? – HappyNomad

+0

Yup, tôi đã thêm dấu kiểm, cảm ơn – Jeff

+3

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

12

Nó không phải 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 phản Extensions có lẽ là những gì bạn đang tìm kiếm.

+1

Ví dụ: Observable.Interval (TimeSpan.FromSeconds (1)) Đăng ký (v => Debug.WriteLine (v)); –

51

CẬP NHẬT Tôi marking the answer below là "câu trả lời" vì đây là đủ tuổi bây giờ mà chúng ta nên sử dụng async/chờ đợi mẫu. Không cần phải downvote nữa. LOL


Như Amy đã trả lời, không có công việc định kỳ dựa trên giờ/giờ. Tuy nhiên, dựa trên UPDATE ban đầu của tôi, chúng tôi đã phát triển điều này thành một cái gì đó khá hữu ích và sản xuất thử nghiệm. Nghĩ 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(); 
     } 
    } 
} 

Output:

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

Điều này trông giống như mã tuyệt vời, nhưng tôi tự hỏi nếu nó là cần thiết bây giờ mà có các từ khóa async/await. Cách tiếp cận của bạn so sánh với cách tiếp cận ở đây như thế nào: http://stackoverflow.com/a/14297203/122781? – HappyNomad

+1

@HappyNomad, có vẻ như lớp PeriodicTaskFactory có thể tận dụng lợi thế 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 không thể chuyển sang .Net 4.5. Ngoài ra, PeriodicTaskFactory cung cấp một số cơ chế chấm dứt "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 lại có thể đợi trong lần lặp cuối cùng. Nhưng tôi sẽ tìm cách để thích ứng với điều này để sử dụng async/await khi chúng tôi di chuyển đến .Net 4.5 – Jim

+3

+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ó trở nên tốt đẹp với luồng UI, 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 biểu kết quả vào 'MainPeriodicTaskAction' để nó tạo ra' subTask' với. – HappyNomad

8

Cho đến bây giờ tôi sử dụng một nhiệm vụ LongRunning TPL cho CPU cyclic ràng buộc công việc nền thay vì bộ đếm thời gian luồng, vì:

  • nhiệm vụ TPL hỗ trợ hủy
  • bộ đếm thời gian luồng có thể bắt đầu một chủ đề trong khi programm tắt nguồn gây ra các vấn đề có thể xảy ra với các tài nguyên được xử lý
  • cơ hội bị tràn: bộ đếm luồng có thể bắt đầu một luồng khác trong khi trước đó vẫn đang được xử lý do công việc lâu dài bất ngờ (tôi biết, nó có thể được 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 xác nhận một chuỗi chuyên dụng không cần thiết trong khi chờ hành động tiếp theo (phần lớn 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 cyclic CPU bị ràng buộc trên nền bởi vì nó chỉ cần một thread threadpool khi có công việc để làm đó là tốt hơn cho khả năng mở rộng (đặc biệt là khi khoảng thời gian là lớn).

Để đạt được điều đó, tôi sẽ đề nghị 4 adaptions:

  1. Thêm ConfigureAwait(false) đến Task.Delay() để thực hiện doWork hành động trên một sợi bơi thread, nếu không doWork sẽ được thực hiện trên các thread kêu gọi mà không phải là ý tưởng song song
  2. Stick với mẫu hủy bằng cách ném một TaskCanceledException (vẫn còn cần thiết?)
  3. Chuyển tiếp các CancellationToken để doWork để kích hoạt nó để hủy bỏ nhiệm vụ
  4. Thêm một tham số của loại đối tượng để cung cấp thông tin trạng thái công việc (như một nhiệm vụ TPL)

Về điểm 2 Tôi không chắc chắn, không async chờ đợi vẫn đòi hỏi sự TaskCanceledExecption hoặc là nó chỉ thực hành tốt 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); 
    } 

Xin vui lòng cho ý kiến ​​của bạn với các giải pháp đề xuất ...

Cập nhật 2016-8-30

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

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

Lưu ý rằng ngược lại với Bộ hẹn giờ, 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 thời gian chờ đợi và thời lượng 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); 
    } 
+0

Sử dụng 'ConfigureAwait (false)' sẽ lập lịch trình tiếp tục của phương thức vào nhóm luồng, vì vậy nó không thực sự giải quyết được điểm thứ hai . Tôi cũng không nghĩ rằng 'taskState' là cần thiết; lambda biến chụp là linh hoạt hơn và loại an toàn. –

+1

Điều tôi thực sự muốn làm là trao đổi 'await Task.Delay()' và 'doWork()' do đó 'doWork()' sẽ ngay lập tức thực hiện trong khi khởi động. Nhưng không có một số mẹo 'doWork()' sẽ thực hiện 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? –

+1

Cách dễ nhất là chỉ quấn toàn bộ thứ trong một 'Task.Run'. –

0

Tôi chạy vào một vấn đề tương tự và viết một lớp TaskTimer mà trả về một seris của nhiệm vụ mà đầy đủ về 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(); 
    } 
} 
Các vấn đề liên quan