.NET Core中如何为每个托管服务创建唯一全局变量
如何为每个托管服务执行实例创建唯一的全局变量?
问题描述
运行托管服务时,没有日志很难追踪执行过程。为了追踪托管服务的执行,我实现了带有唯一TraceLogId(GUID)的日志功能,但当前用静态内存存储TraceLogId时,两个任务同时运行会导致第一个任务的TraceLogId被第二个任务覆盖——Task1启动时获取了一个TraceLogId,但结束时显示的却是Task2设置的ID。
当前代码
全局变量类
public static class GlobalVariables { public static Guid TraceLogId { get; set; } }
任务服务实现
public class Task1Service : ITask1Service { public async Task StartAsync(CancellationToken cancellationToken) { GlobalVariables.TraceLogId = Guid.NewGuid(); Console.WriteLine($"Task1 executing with traceLogId = {GlobalVariables.TraceLogId}"); Console.WriteLine($"Task1 will wait 5 seconds = {GlobalVariables.TraceLogId}"); await Task.Delay(5000, cancellationToken); Console.WriteLine($"Task1 ending = {GlobalVariables.TraceLogId}"); } } public class Task2Service : ITask2Service { public async Task StartAsync(CancellationToken cancellationToken) { Console.WriteLine("Task2 executing"); GlobalVariables.TraceLogId = Guid.NewGuid(); Console.WriteLine($"Task2 executing with traceLogId = {GlobalVariables.TraceLogId}"); Console.WriteLine($"Task2 ending = {GlobalVariables.TraceLogId}"); } }
执行结果
执行结果显示Task1启动时的TraceLogId与结束时的不一致,结束时显示的是Task2设置的TraceLogId。
定时任务基类
using System; using System.Threading; using System.Threading.Tasks; using Cronos; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace HostedServicesPoc.Tasks { public abstract class CronJobServiceBase : IHostedService, IDisposable { private readonly ILogger _log; private readonly HostedServiceTaskSettingsBase _hostedServiceTaskSettingsBase; private System.Timers.Timer _timer; private readonly CronExpression _expression; private readonly TimeZoneInfo _timeZoneInfo; protected CronJobServiceBase(IOptions<HostedServiceTaskSettingsBase> hostedServiceSettings, ILogger<CronJobServiceBase> log) { _log = log; _hostedServiceTaskSettingsBase = hostedServiceSettings?.Value; _expression = CronExpression.Parse(_hostedServiceTaskSettingsBase.CronExpressionTimer, CronFormat.Standard); _timeZoneInfo = TimeZoneInfo.Local; } public virtual async Task StartAsync(CancellationToken cancellationToken) { _log.LogInformation($"{GetType()} is Starting"); if (_hostedServiceTaskSettingsBase.Active) { await ScheduleJob(cancellationToken); } } public Task StopAsync(CancellationToken cancellationToken) { _log.LogInformation($"{GetType()} is Stopping"); return Task.CompletedTask; } private async Task ScheduleJob(CancellationToken cancellationToken) { var next = _expression.GetNextOccurrence(DateTimeOffset.Now, _timeZoneInfo); if (next.HasValue) { var delay = next.Value - DateTimeOffset.Now; if (delay.TotalMilliseconds <= 0) // 避免将非正值传入Timer { await ScheduleJob(cancellationToken); } _timer = new System.Timers.Timer(delay.TotalMilliseconds); _timer.Elapsed += async (sender, args) => { _timer.Dispose(); // 重置并释放Timer _timer = null; if (!cancellationToken.IsCancellationRequested) { await ExecuteTaskAsync(cancellationToken); } if (!cancellationToken.IsCancellationRequested) { await ScheduleJob(cancellationToken); // 调度下一次执行 } }; _timer.Start(); } await Task.CompletedTask; } protected virtual async Task ExecuteTaskAsync(CancellationToken cancellationToken) { await Task.Delay(5000, cancellationToken); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool dispose) { try { if (dispose) { _timer?.Dispose(); } } finally { } } } }
任务托管服务
public class Task1HostedService : CronJobServiceBase { private readonly IServiceProvider _serviceProvider; public Task1HostedService( IOptions<Task1HostedServiceSettings> hostedServiceSettings, ILogger<CronJobServiceBase> log, IServiceProvider serviceProvider) : base(hostedServiceSettings, log) { _serviceProvider = serviceProvider; } protected override async Task ExecuteTaskAsync(CancellationToken cancellationToken) { using var scope = _serviceProvider.CreateScope(); var task1Service = scope.ServiceProvider.GetRequiredService<ITask1Service>(); await task1Service.StartAsync(cancellationToken); } }
解决方案
方案1:使用局部变量传递(最直接)
把TraceLogId作为任务内部的局部变量,避免全局共享:
public class Task1Service : ITask1Service { public async Task StartAsync(CancellationToken cancellationToken) { var traceLogId = Guid.NewGuid(); Console.WriteLine($"Task1 executing with traceLogId = {traceLogId}"); Console.WriteLine($"Task1 will wait 5 seconds = {traceLogId}"); await Task.Delay(5000, cancellationToken); Console.WriteLine($"Task1 ending = {traceLogId}"); } } public class Task2Service : ITask2Service { public async Task StartAsync(CancellationToken cancellationToken) { Console.WriteLine("Task2 executing"); var traceLogId = Guid.NewGuid(); Console.WriteLine($"Task2 executing with traceLogId = {traceLogId}"); Console.WriteLine($"Task2 ending = {traceLogId}"); } }
优点:简单直接,无额外依赖;缺点:若需在多个方法中使用,需手动传递变量。
方案2:使用AsyncLocal保持异步上下文
AsyncLocal<T>能在异步执行流中保持独立的变量副本,每个异步任务拥有专属实例:
public static class TraceContext { private static readonly AsyncLocal<Guid> _traceLogId = new AsyncLocal<Guid>(); public static Guid TraceLogId { get => _traceLogId.Value; set => _traceLogId.Value = value; } } // 任务服务中使用 public class Task1Service : ITask1Service { public async Task StartAsync(CancellationToken cancellationToken) { TraceContext.TraceLogId = Guid.NewGuid(); Console.WriteLine($"Task1 executing with traceLogId = {TraceContext.TraceLogId}"); Console.WriteLine($"Task1 will wait 5 seconds = {TraceContext.TraceLogId}"); await Task.Delay(5000, cancellationToken); Console.WriteLine($"Task1 ending = {TraceContext.TraceLogId}"); } }
优点:无需手动传递变量,整个异步调用链都能访问当前任务的TraceLogId;缺点:需理解异步上下文传递机制,避免非异步场景误用。
方案3:结合日志框架的Scope功能(推荐)
使用.NET内置ILogger的BeginScope创建日志范围,自动将TraceLogId附加到所有日志条目,无需手动管理变量:
public class Task1Service : ITask1Service { private readonly ILogger<Task1Service> _logger; public Task1Service(ILogger<Task1Service> logger) { _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { var traceLogId = Guid.NewGuid(); using (_logger.BeginScope(new Dictionary<string, object> { ["TraceLogId"] = traceLogId })) { _logger.LogInformation("Task1 executing"); _logger.LogInformation("Task1 will wait 5 seconds"); await Task.Delay(5000, cancellationToken); _logger.LogInformation("Task1 ending"); } } }
配置日志格式(如appsettings.json)时,添加Scope或直接指定TraceLogId:
{ "Logging": { "Console": { "FormatterName": "simple", "FormatterOptions": { "IncludeScopes": true, "TimestampFormat": "HH:mm:ss ", "LogLevel": { "Default": "Information" } } } } }
优点:符合.NET日志最佳实践,自动管理上下文,无需维护全局/静态变量;缺点:依赖日志框架的Scope功能,需配置日志格式。
内容的提问来源于stack exchange,提问作者mavi
相关产品推荐
相关产品推荐

