[英]How to prevent an application from terminating before the completion of all fire-and-forget tasks?
我有一個定期啟動即發即忘任務的應用程序,主要用於日志記錄,我的問題是當應用程序關閉時,任何當前正在運行的即發即忘任務都會中止。 我想防止這種情況發生,所以我正在尋找一種機制,讓我在關閉我的應用程序之前await
所有正在運行的即發即棄操作完成。 我不想處理他們可能的異常,我不在乎這些。 我只是想讓他們有機會完成(可能會超時,但這不是問題的一部分)。
你可能會爭辯說,這個要求使我的任務並不是真正的即發即忘,這有一定的道理,所以我想澄清這一點:
這是該問題的最小演示:
static class Program
{
static async Task Main(string[] args)
{
_ = Log("Starting"); // fire and forget
await Task.Delay(1000); // Simulate the main asynchronous workload
CleanUp();
_ = Log("Finished"); // fire and forget
// Here any pending fire and forget operations should be awaited somehow
}
private static void CleanUp()
{
_ = Log("CleanUp started"); // fire and forget
Thread.Sleep(200); // Simulate some synchronous operation
_ = Log("CleanUp completed"); // fire and forget
}
private static async Task Log(string message)
{
await Task.Delay(100); // Simulate an async I/O operation required for logging
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} {message}");
}
}
輸出:
11:14:11.441 Starting
11:14:12.484 CleanUp started
Press any key to continue . . .
"CleanUp completed"
和"Finished"
條目不會被記錄,因為應用程序過早終止,並且掛起的任務被中止。 有什么辦法可以在關閉之前等待他們完成嗎?
順便說一句,這個問題的靈感來自@SHAFEESPS 最近提出的一個問題,遺憾的是該問題因不清楚而關閉。
說明:上面提供的最小示例包含單一類型的即發即忘操作,即Task Log
方法。 現實世界應用程序啟動的即發即棄操作是多種且異構的。 有些甚至返回通用任務,如Task<string>
或Task<int>
。
一個即發即棄的任務也有可能會觸發次要的即發即忘任務,這些任務也應該被允許開始並等待。
也許像一個等待退出的櫃台之類的東西? 這仍然會很容易忘記。
我只將LogAsync
移動到它自己的方法,因為每次調用 Log 時都不需要丟棄。 我想它還可以處理如果在程序退出時調用 Log 時會發生的微小競爭條件。
public class Program
{
static async Task Main(string[] args)
{
Log("Starting"); // fire and forget
await Task.Delay(1000); // Simulate the main asynchronous workload
CleanUp();
Log("Finished"); // fire and forget
// Here any pending fire and forget operations should be awaited somehow
var spin = new SpinWait();
while (_backgroundTasks > 0)
{
spin.SpinOnce();
}
}
private static void CleanUp()
{
Log("CleanUp started"); // fire and forget
Thread.Sleep(200); // Simulate some synchronous operation
Log("CleanUp completed"); // fire and forget
}
private static int _backgroundTasks;
private static void Log(string message)
{
Interlocked.Increment(ref _backgroundTasks);
_ = LogAsync(message);
}
private static async Task LogAsync(string message)
{
await Task.Delay(100); // Simulate an async I/O operation required for logging
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} {message}");
Interlocked.Decrement(ref _backgroundTasks);
}
}
一件合理的事情是在您的記錄器中擁有內存隊列(這適用於符合您的條件的其他類似功能),它是單獨處理的。 然后你的日志方法就變成了這樣:
private static readonly BlockingCollection<string> _queue = new BlockingCollection<string>(new ConcurrentQueue<string>());
public static void Log(string message) {
_queue.Add(message);
}
它對調用者來說非常快且無阻塞,並且在某種意義上是異步的,它在未來的某個時間完成(或失敗)。 調用者不知道也不關心結果,所以這是一個即發即忘的任務。
然而,這個隊列是單獨地、全局地、可能在一個單獨的線程中、或者通過等待(和線程池線程)單獨處理(通過將日志消息插入最終目的地,如文件或數據庫),這無關緊要。
然后在應用程序退出之前,您只需要通知隊列處理器不需要更多項目,並等待它完成。 例如:
_queue.CompleteAdding(); // no more items
_processorThread.Join(); // if you used separate thread, otherwise some other synchronization construct.
編輯:如果您希望隊列處理是異步的 - 您可以使用此AsyncCollection (可用作 nuget 包)。 然后你的代碼變成:
class Program {
private static Logger _logger;
static async Task Main(string[] args) {
_logger = new Logger();
_logger.Log("Starting"); // fire and forget
await Task.Delay(1000); // Simulate the main asynchronous workload
CleanUp();
_logger.Log("Finished"); // fire and forget
await _logger.Stop();
// Here any pending fire and forget operations should be awaited somehow
}
private static void CleanUp() {
_logger.Log("CleanUp started"); // fire and forget
Thread.Sleep(200); // Simulate some synchronous operation
_logger.Log("CleanUp completed"); // fire and forget
}
}
class Logger {
private readonly AsyncCollection<string> _queue = new AsyncCollection<string>(new ConcurrentQueue<string>());
private readonly Task _processorTask;
public Logger() {
_processorTask = Process();
}
public void Log(string message) {
// synchronous adding, you can also make it async via
// _queue.AddAsync(message); but I see no reason to
_queue.Add(message);
}
public async Task Stop() {
_queue.CompleteAdding();
await _processorTask;
}
private async Task Process() {
while (true) {
string message;
try {
message = await _queue.TakeAsync();
}
catch (InvalidOperationException) {
// throws this exception when collection is empty and CompleteAdding was called
return;
}
await Task.Delay(100);
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} {message}");
}
}
}
或者您可以使用單獨的專用線程來同步處理項目,就像通常那樣。
編輯 2:這是引用計數的變體,它不對“即發即棄”任務的性質做出任何假設:
static class FireAndForgetTasks {
// start with 1, in non-signaled state
private static readonly CountdownEvent _signal = new CountdownEvent(1);
public static void AsFireAndForget(this Task task) {
// add 1 for each task
_signal.AddCount();
task.ContinueWith(x => {
if (x.Exception != null) {
// do something, task has failed, maybe log
}
// decrement 1 for each task, it cannot reach 0 and become signaled, because initial count was 1
_signal.Signal();
});
}
public static void Wait(TimeSpan? timeout = null) {
// signal once. Now event can reach zero and become signaled, when all pending tasks will finish
_signal.Signal();
// wait on signal
if (timeout != null)
_signal.Wait(timeout.Value);
else
_signal.Wait();
// dispose the signal
_signal.Dispose();
}
}
您的樣本變為:
static class Program {
static async Task Main(string[] args) {
Log("Starting").AsFireAndForget(); // fire and forget
await Task.Delay(1000); // Simulate the main asynchronous workload
CleanUp();
Log("Finished").AsFireAndForget(); // fire and forget
FireAndForgetTasks.Wait();
// Here any pending fire and forget operations should be awaited somehow
}
private static void CleanUp() {
Log("CleanUp started").AsFireAndForget(); // fire and forget
Thread.Sleep(200); // Simulate some synchronous operation
Log("CleanUp completed").AsFireAndForget(); // fire and forget
}
private static async Task Log(string message) {
await Task.Delay(100); // Simulate an async I/O operation required for logging
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} {message}");
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.