簡體   English   中英

如何防止應用程序在所有“即發即忘”任務完成之前終止?

[英]How to prevent an application from terminating before the completion of all fire-and-forget tasks?

我有一個定期啟動即發即忘任務的應用程序,主要用於日志記錄,我的問題是當應用程序關閉時,任何當前正在運行的即發即忘任務都會中止。 我想防止這種情況發生,所以我正在尋找一種機制,讓我在關閉我的應用程序之前await所有正在運行的即發即棄操作完成。 我不想處理他們可能的異常,我不在乎這些。 我只是想讓他們有機會完成(可能會超時,但這不是問題的一部分)。

你可能會爭辯說,這個要求使我的任務並不是真正的即發即忘,這有一定的道理,所以我想澄清這一點:

  1. 這些任務在本地是即發即忘的,因為啟動它們的方法對它們的結果不感興趣。
  2. 這些任務不是全局性的,因為應用程序作為一個整體關心它們。

這是該問題的最小演示:

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM