2012-12-04 57 views
73

因此,只要應用程序正在運行或請求取消,我的應用程序需要幾乎不間斷地執行操作(每次運行之間暫停10秒左右)。它需要做的工作可能需要長達30秒。正確的方式來實現永不結束的任務。 (定時器vs任務)

是更好地使用System.Timers.Timer,並使用自動復位,以確保它不會將以前的「滴答」完成之前執行的操作。

或者我應該使用LongRunning模式一般任務與取消標記,並有定期無限while裏面調用動作做的工作與電話之間有10秒的Thread.Sleep循環?至於異步/等待模式,我不確定這是否合適,因爲我沒有任何工作返回值。

CancellationTokenSource wtoken; 
Task task; 

void StopWork() 
{ 
    wtoken.Cancel(); 

    try 
    { 
     task.Wait(); 
    } catch(AggregateException) { } 
} 

void StartWork() 
{ 
    wtoken = new CancellationTokenSource(); 

    task = Task.Factory.StartNew(() => 
    { 
     while (true) 
     { 
      wtoken.Token.ThrowIfCancellationRequested(); 
      DoWork(); 
      Thread.Sleep(10000); 
     } 
    }, wtoken, TaskCreationOptions.LongRunning); 
} 

void DoWork() 
{ 
    // Some work that takes up to 30 seconds but isn't returning anything. 
} 

或只使用一個簡單的定時器,而使用其自動復位性能,並調用.Stop()來取消它?

+0

任務似乎是一個矯枉過正考慮你想要達到的目標。 http://en.wikipedia.org/wiki/KISS_principle。在OnTick()開始時停止定時器,檢查一個bool,看看你是否應該做任何事情,做工作,當你完成後重新啓動Timer。 –

回答

87

(因爲你正在使用.NET 4.5,它使用Task內部)我會使用TPL Dataflow這一點。您可以輕鬆創建一個ActionBlock<TInput>,它在處理它的操作並等待適當的時間後將項目發佈到自身。

首先,建立一個工廠,將創建永無止境的任務:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Action<DateTimeOffset> action, CancellationToken cancellationToken) 
{ 
    // Validate parameters. 
    if (action == null) throw new ArgumentNullException("action"); 

    // Declare the block variable, it needs to be captured. 
    ActionBlock<DateTimeOffset> block = null; 

    // Create the block, it will call itself, so 
    // you need to separate the declaration and 
    // the assignment. 
    // Async so you can wait easily when the 
    // delay comes. 
    block = new ActionBlock<DateTimeOffset>(async now => { 
     // Perform the action. 
     action(now); 

     // Wait. 
     await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken). 
      // Doing this here because synchronization context more than 
      // likely *doesn't* need to be captured for the continuation 
      // here. As a matter of fact, that would be downright 
      // dangerous. 
      ConfigureAwait(false); 

     // Post the action back to the block. 
     block.Post(DateTimeOffset.Now); 
    }, new ExecutionDataflowBlockOptions { 
     CancellationToken = cancellationToken 
    }); 

    // Return the block. 
    return block; 
} 

我選擇了ActionBlock<TInput>採取DateTimeOffset structure;你必須傳遞一個類型參數,並且它可能會傳遞一些有用的狀態(如果你願意,你可以改變狀態的性質)。

而且,請注意,ActionBlock<TInput>在默認情況下只處理一個項目的時間,所以你保證只有一個動作將被處理(意思是,你將不必應付reentrancy當它調用Post extension method)。

我也已將CancellationToken structure傳遞給ActionBlock<TInput>的構造函數和Task.Delay method調用;如果過程取消,取消將在第一個可能的機會進行。

從那裏,它是你的代碼來存儲ActionBlock<TInput>實現(這是代表都是消費者塊的更高層次的抽象,並希望能夠通過對呼叫觸發消費ITargetBlock<DateTimeoffset> interface的簡單重構Post擴展方法):

CancellationTokenSource wtoken; 
ActionBlock<DateTimeOffset> task; 

StartWork方法:

void StartWork() 
{ 
    // Create the token source. 
    wtoken = new CancellationTokenSource(); 

    // Set the task. 
    task = CreateNeverEndingTask(now => DoWork(), wtoken.Token); 

    // Start the task. Post the time. 
    task.Post(DateTimeOffset.Now); 
} 

然後你StopWork方法:

void StopWork() 
{ 
    // CancellationTokenSource implements IDisposable. 
    using (wtoken) 
    { 
     // Cancel. This will cancel the task. 
     wtoken.Cancel(); 
    } 

    // Set everything to null, since the references 
    // are on the class level and keeping them around 
    // is holding onto invalid state. 
    wtoken = null; 
    task = null; 
} 

爲什麼要在這裏使用TPL Dataflow?有幾個原因:

關注

CreateNeverEndingTask方法分離現在是一個工廠,創建你的「服務」可以這麼說。您可以控制何時啓動和停止,並且它是完全獨立的。您不必將定時器的狀態控制與代碼的其他方面交織在一起。您只需創建塊,啓動它,並在完成後停止。

更有效地利用的線程/任務/資源

在TPL數據流中的塊的默認調度爲Task,這是線程池相同。通過使用ActionBlock<TInput>來處理您的操作以及致電Task.Delay,您可以控制在實際沒有做任何事情時使用的線程。當然,當你產生新的Task來處理延續時,這實際上會導致一些開銷,但是這應該很小,因爲你沒有在緊密的循環中處理這個(你在調用之間等待10秒)。

如果DoWork函數實際上可製成awaitable(即,在它返回一個Task),然後就可以(可能)優化此更加通過調整上述工廠方法採取Func<DateTimeOffset, CancellationToken, Task>代替Action<DateTimeOffset>,像所以:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Func<DateTimeOffset, CancellationToken, Task> action, 
    CancellationToken cancellationToken) 
{ 
    // Validate parameters. 
    if (action == null) throw new ArgumentNullException("action"); 

    // Declare the block variable, it needs to be captured. 
    ActionBlock<DateTimeOffset> block = null; 

    // Create the block, it will call itself, so 
    // you need to separate the declaration and 
    // the assignment. 
    // Async so you can wait easily when the 
    // delay comes. 
    block = new ActionBlock<DateTimeOffset>(async now => { 
     // Perform the action. Wait on the result. 
     await action(now, cancellationToken). 
      // Doing this here because synchronization context more than 
      // likely *doesn't* need to be captured for the continuation 
      // here. As a matter of fact, that would be downright 
      // dangerous. 
      ConfigureAwait(false); 

     // Wait. 
     await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken). 
      // Same as above. 
      ConfigureAwait(false); 

     // Post the action back to the block. 
     block.Post(DateTimeOffset.Now); 
    }, new ExecutionDataflowBlockOptions { 
     CancellationToken = cancellationToken 
    }); 

    // Return the block. 
    return block; 
} 

當然,這將是很好的做法,編織CancellationToken通過你的方法(如果它接受一個),這是在這裏完成。

這意味着,你將有一個DoWorkAsync方法具有以下簽名:

Task DoWorkAsync(CancellationToken cancellationToken); 

你不得不改變(僅略,你也不會在這裏滲出關注點分離)的StartWork方法考慮到傳遞給CreateNeverEndingTask方法的新簽名,就像這樣:

void StartWork() 
{ 
    // Create the token source. 
    wtoken = new CancellationTokenSource(); 

    // Set the task. 
    task = CreateNeverEndingTask((now, ct) => DoWorkAsync(ct), wtoken.Token); 

    // Start the task. Post the time. 
    task.Post(DateTimeOffset.Now, wtoken.Token); 
} 
+0

你好,我正在嘗試這個實現,但我面臨的問題。如果我的DoWork不帶任何參數,task = CreateNeverEndingTask(now => DoWork(),wtoken.Token);給我一個構建錯誤(類型不匹配)。另一方面,如果我的DoWork接受一個DateTimeOffset參數,那麼同一行會給我一個不同的構建錯誤,告訴我DoWork的重載不會有0個參數。你能幫我把這個弄清楚嗎? – Bovaz

+1

其實,我通過添加一個強制轉換到我分配任務並將參數傳遞給DoWork的行來解決了我的問題:task =(ActionBlock )CreateNeverEndingTask(now => DoWork(now),wtoken.Token); – Bovaz

+0

您也可以更改「ActionBlock 任務的類型;」到ITargetBlock 任務; – XOR

61

我找到了新的基於任務的界面是做這樣的事情很簡單 - 比使用Timer類更容易。

有一些小的調整,你可以讓你的例子。相反的:

task = Task.Factory.StartNew(() => 
{ 
    while (true) 
    { 
     wtoken.Token.ThrowIfCancellationRequested(); 
     DoWork(); 
     Thread.Sleep(10000); 
    } 
}, wtoken, TaskCreationOptions.LongRunning); 

你可以這樣做:

task = Task.Run(async() => // <- marked async 
{ 
    while (true) 
    { 
     DoWork(); 
     await Task.Delay(10000, wtoken.Token); // <- await with cancellation 
    } 
}, wtoken.Token); 

這樣的取消將在瞬間發生,如果Task.Delay內,而不必等待Thread.Sleep完成。

另外,使用Task.Delay而不是Thread.Sleep意味着你沒有綁定一個線程在睡眠期間無所作爲。

如果你能,你也可以讓DoWork()接受取消標記,並取消將更加敏感。

+0

如果使用async lambda作爲Task.Factory.StartNew的參數,將獲得什麼任務? - http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx當您做task.Wait();請求取消後,您將等待不正確的任務。 –

+0

是的,這實際上應該是Task.Run,​​它具有正確的過載。 – porges

+0

根據[http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx](http://blogs.msdn.com/b/pfxteam/archive/2011/10 /24/10229468.aspx)它看起來像'Task.Run'使用線程池,所以你的例子使用Task.Run而不是'Task.Factory.StartNew'與'TaskCreationOptions.LongRunning'不完全相同同樣的事情 - 如果我需要使用「LongRunning」選項的任務,我是不是可以象使用過的那樣使用Task.Run,​​或者我錯過了什麼? – Jeff

3

這裏是我想出了:

  • NeverEndingTask繼承並覆蓋ExecutionCore方法與您想要做的工作。
  • 更改ExecutionLoopDelayMs可讓您調整環路之間的時間,例如如果你想使用退避算法。
  • Start/Stop提供了啓動/停止任務的同步界面。
  • LongRunning表示您將獲得一個專用線程,每個NeverEndingTask
  • 與上面的基於ActionBlock的解決方案不同,此類不會在循環中分配內存。
  • 下面的代碼是小品,不一定是生產代碼:)

public abstract class NeverEndingTask 
{ 
    // Using a CTS allows NeverEndingTask to "cancel itself" 
    private readonly CancellationTokenSource _cts = new CancellationTokenSource(); 

    protected NeverEndingTask() 
    { 
     TheNeverEndingTask = new Task(
      () => 
      { 
       // Wait to see if we get cancelled... 
       while (!_cts.Token.WaitHandle.WaitOne(ExecutionLoopDelayMs)) 
       { 
        // Otherwise execute our code... 
        ExecutionCore(_cts.Token); 
       } 
       // If we were cancelled, use the idiomatic way to terminate task 
       _cts.Token.ThrowIfCancellationRequested(); 
      }, 
      _cts.Token, 
      TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning); 

     // Do not forget to observe faulted tasks - for NeverEndingTask faults are probably never desirable 
     TheNeverEndingTask.ContinueWith(x => 
     { 
      Trace.TraceError(x.Exception.InnerException.Message); 
      // Log/Fire Events etc. 
     }, TaskContinuationOptions.OnlyOnFaulted); 

    } 

    protected readonly int ExecutionLoopDelayMs = 0; 
    protected Task TheNeverEndingTask; 

    public void Start() 
    { 
     // Should throw if you try to start twice... 
     TheNeverEndingTask.Start(); 
    } 

    protected abstract void ExecutionCore(CancellationToken cancellationToken); 

    public void Stop() 
    { 
     // This code should be reentrant... 
     _cts.Cancel(); 
     TheNeverEndingTask.Wait(); 
    } 
}