LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

C#如何正確實現一個 BackgroundService

freeflydom
2025年8月8日 9:5 本文熱度 354

相信大家都知道如何在 .NET 中執行后臺(定時)任務。首先我們會選擇實現 IHostedService 接口或者繼承BackgroundService 來實現后臺任務。然后注冊到容器內,然后注冊到容器內,之后這些后臺任務 service 就會自動被 觸發(trigger)。本文不是初級的入門教程,而是試圖告訴讀者一些容易被忽略的細節。

IHostedService

IHostedService 是一個.NET Core 的接口,用于實現后臺服務。通過實現這個接口,你可以在應用程序運行期間在后臺執行任務,例如定時任務、監聽事件、處理隊列等。IHostedService 提供了 StartAsync() 和 StopAsync() 方法,分別用于啟動和停止后臺服務,并且框架會根據應用程序的生命周期自動調用這兩個方法。
以下是這個接口的源碼:

其中 StartAsync 方法由 IApplicationLifetime.ApplicationStarted 事件觸發
其中 StopAsync 方法由 IApplicationLifetime.ApplicationStopped 事件觸發

 //
 // 摘要:
 //     Defines methods for objects that are managed by the host.
 public interface IHostedService
 {
  
     Task StartAsync(CancellationToken cancellationToken);
     Task StopAsync(CancellationToken cancellationToken);
 }

通常我們的后臺任務會被框在一個while循環里,定時去執行某些邏輯。以下是我們模擬的一段演示代碼。StartAsync 方法被 call 的時候就會執行這個 while。代碼很簡單,不過多解釋。

    public class HostServiceTest_A : IHostedService
    {
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("HostServiceTest_A starting.");
            while (!cancellationToken.IsCancellationRequested)
            {
                // Simulate some work
                Console.WriteLine("HostServiceTest_A is doing work.");
                await Task.Delay(3000, cancellationToken); // Delay for 3 second
            }
        }
        public Task StopAsync(CancellationToken cancellationToken)
        {
            // to do
            return Task.CompletedTask;
        }
    }

把這個服務注冊到容器內。

    builder.Services.AddHostedService<HostServiceTest_A>();

下面讓我們啟動一下程序試試。可以看到程序可以啟動,這個 while 循環也是一直在工作。咋看好像沒啥問題,但是仔細看看的話好像缺了點什么。

問題

對了,我們這個 ASP.NET Core 程序啟動日志沒有了。也就是整個程序的啟動過程被 block 住了。原因在于 HostedService 是順序的,一旦某個 HostedService 的 StartAsync 方法沒有盡快 return 的話,后面所有的任務全部不能執行了。比如你注冊了多個 HostedService,第一個使用了這種錯誤的方法來執行任務,后面的 HostedService 全部都沒有機會被執行。

HostServiceTest_A starting.
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
···

下面讓我們改進一下,使用 Task.Run 來讓這個任務變成異步,并且不去 await 這個 task。

    public class HostServiceTest_A : IHostedService
    {
        public Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("HostServiceTest_A starting.");
            Task.Run(async () => {
                while (!cancellationToken.IsCancellationRequested)
                {
                    // Simulate some work
                    Console.WriteLine("HostServiceTest_A is doing work.");
                    await Task.Delay(3000, cancellationToken); // Delay for 3 second
                }
            });
            return Task.CompletedTask;
        }
        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }

再次執行一下程序,可以看到 HostedService 跟 ASP.NET Core 主程序都可以正確執行了。

HostServiceTest_A starting.
HostServiceTest_A is doing work.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5221
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo
HostServiceTest_A is doing work.

改進

我們的后臺任務通常是一個長期任務,這種情況下更加推薦 LongRunning Task 來 handle 這種任務。至于為什么可以參考以下文檔:
https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-9.0

           Task.Factory.StartNew(async () => {
               while (!cancellationToken.IsCancellationRequested)
               {
                   // Simulate some work
                   Console.WriteLine("HostServiceTest_A is doing work.");
                   await Task.Delay(3000, cancellationToken); // Delay for 3 second
               }
           }, TaskCreationOptions.LongRunning);
           return Task.CompletedTask;

退出

以上我們都在說如何啟動后臺任務,還沒討論如何取消這個后臺任務。參入的那個 cancellationToken 在 Application 被 stop 的時候并不會主動 cancel。所以我們需要在 StopAsync 方法觸發的時候手動來 Cancel 這個 token。

    public class HostServiceTest_A : IHostedService
    {
        private CancellationTokenSource _cancellationTokenSource;
        public Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("HostServiceTest_A starting.");
            _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            Task.Factory.StartNew(async () => {
                while (!_cancellationTokenSource.Token.IsCancellationRequested)
                {
                    // Simulate some work
                    Console.WriteLine("HostServiceTest_A is doing work.");
                    await Task.Delay(1000, cancellationToken); // Delay for 3 second
                }
                Console.WriteLine("HostServiceTest_A task done.");
            }, TaskCreationOptions.LongRunning);
            return Task.CompletedTask;
        }
        public Task StopAsync(CancellationToken cancellationToken)
        {
            if (!cancellationToken.IsCancellationRequested)
            {
                _cancellationTokenSource.Cancel();
            }
            Console.WriteLine("HostServiceTest_A stop.");
            return Task.CompletedTask;
        }
    }

讓我們運行一下,然后按下 Ctrl + C 來主動退出程序,可以看到我們的 while 被安全退出了。

HostServiceTest_A starting.
HostServiceTest_A is doing work.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5221
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
HostServiceTest_A is doing work.
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
HostServiceTest_A stop.
HostServiceTest_A task done.

BackgroundService

除了,HostedService,微軟還給我們提供了 BackgroundService 這個類。一看這個類名就知道他能干嘛。其實也未必想的這么簡單。BackgroundService 實際上是 IHostedService 的一個實現類。它的核心是將后臺任務邏輯放在 ExecuteAsync 這個抽象方法中。下面我們通過一個具體案例來分析。。

    public class BackgroundServiceTest_A : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                Console.WriteLine("ExecuteAsyncA is running.");
                await Task.Delay(3000);
            }
        }
    }

運行這個代碼,可以看到 BackgroundService 正常啟動了,而且也沒 block 住 ASP.NET Core 的程序。看是一切完美。

ExecuteAsyncA is running.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5221
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo
ExecuteAsyncA is running.
ExecuteAsyncA is running.
ExecuteAsyncA is running.
ExecuteAsyncA is running.
ExecuteAsyncA is running.

問題

以上代碼真的沒有問題嗎?其實不盡然。讓我們上點強度。如果我們在循環中加一個耗時很長的步驟。事實上這個很常見。比如以下代碼:

    public class BackgroundServiceTest_A : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                Console.WriteLine("ExecuteAsyncA is running.");
                LongTermTask();
                await Task.Delay(3000);
            }
        }
        private void LongTermTask()
        {
            // Simulate some work
            Console.WriteLine("LongTermTaskA is doing work.");
            Thread.Sleep(30000);
        }
    }

再次運行以下,我們可以發現 ASP.NET Core 的主程序起不來了,被 block 住了。只有等第一個循環周期過后,主程序才能啟動起來。

ExecuteAsyncA is running.
LongTermTaskA is doing work.

那么問題到底出在哪?讓我們看看 BackgroundService 的源碼。

        public virtual Task StartAsync(CancellationToken cancellationToken)
        {
            // Create linked token to allow cancelling executing task from provided token
            _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            // Store the task we're executing
            _executeTask = ExecuteAsync(_stoppingCts.Token);
            // If the task is completed then return it, this will bubble cancellation and failure to the caller
            if (_executeTask.IsCompleted)
            {
                return _executeTask;
            }
            // Otherwise it's running
            return Task.CompletedTask;
        }

可以看到 StartAsync 方法會調用 ExecuteAsync,但是它沒有 await 這個方法,也就是說 StartAsync 內部實現是個同步方法。也就是說 ExecuteAsync 方法跟 StartAsync 會在同一個線程上被執行(在遇到第一個 await 之前)。如果你注冊了多個 BackgroundService 并且他們一次 loop 都非常耗時,那么這個程序啟動將會非常耗時。其實微軟已經在文檔上提醒大家了:

Avoid performing long, blocking initialization work in ExecuteAsync.

改進

那么改進方法,同樣使用 Task.Factory.StartNew 來構造一個 LongRunning 的 task 就可以解決。

    public class BackgroundServiceTest_A : BackgroundService
    {
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            return Task.Factory.StartNew(async () =>
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    // Simulate some work
                    Console.WriteLine("HostServiceTest_A is doing work.");
                    LongTermTask();
                    await Task.Delay(1000, stoppingToken); // Delay for 1 second
                }
                Console.WriteLine("HostServiceTest_A task done.");
            }, TaskCreationOptions.LongRunning);
        }
        private void LongTermTask()
        {
            // Simulate some work
            Console.WriteLine("LongTermTaskA is doing work.");
            Thread.Sleep(30000);
        }
    }

運行一下,完美啟動后臺任務跟主程序。

HostServiceTest_A is doing work.
LongTermTaskA is doing work.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5221
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\workspace\BackgroundServiceDemo\BackgroundServiceDemo

繼續改進

如果要繼續吹毛求疵的話,我們還可以改進一下。從 .NET6 開始 PeriodicTimer 被加入進來。它是一個 timer,可以替換一部分 Task.Delay 活。使用 PeriodicTimer 話相對于 Task.Delay 來說可以讓 loop 的間隔更加精準的被控制。
詳見這里 https://learn.microsoft.com/en-us/dotnet/api/system.threading.periodictimer.waitfornexttickasync?view=net-9.0

     protected override Task ExecuteAsync(CancellationToken stoppingToken)
     {
         return Task.Factory.StartNew(async () =>
         {
             var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
             while (await timer.WaitForNextTickAsync(stoppingToken))
             {
                 // Simulate some work
                 Console.WriteLine("HostServiceTest_A is doing work.");
                 LongTermTask();
             }
             Console.WriteLine("HostServiceTest_A task done.");
         }, TaskCreationOptions.LongRunning);
     }

總結

通過以上的演示,我們可以感受到,實現一個后臺任務還是有非常多的點需要被注意的。特別是不要在 StartAsync 或者 ExcuteAsync 方法內執行耗時的同步方法。如果有耗時任務請包裹在新的 Task 內執行。我們要保證這兩個方法輕量化能夠被快速的執行完畢,這樣的話不會影響應用程序的啟動。

轉自https://www.cnblogs.com/kklldog/p/19020718


該文章在 2025/8/8 9:06:01 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved

黄频国产免费高清视频,久久不卡精品中文字幕一区,激情五月天AV电影在线观看,欧美国产韩国日本一区二区
伊人大杳蕉久久动漫 | 中文字幕国产第一页首页 | 中文字幕AV制服丝袜综合 | 亚洲日本成本人动漫 | 正在播放约酒店少妇高潮 | 久久午夜视频一二三区 |