You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 16:45:30