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

如何以线程安全方式在请求作用域类中返回同一IAzureMediaServicesClient实例

为IAzureMediaServicesClient创建Scoped单例包装类

问题背景

需要为IAzureMediaServicesClient实现一个包装类,将其注册为Scoped服务后,在单个HTTP请求的生命周期内,所有调用都返回同一个客户端实例。当前代码每次调用GetClient()都会新建实例,无法满足同一请求内复用的需求。

当前包装类代码

public class AzureMediaServicesClientProvider : IAzureMediaServicesClientProvider
{
    private readonly IConfiguration _configuration;

    public AzureMediaServicesClientProvider(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public async Task<IAzureMediaServicesClient> GetClient()
    {
        ServiceClientCredentials credentials = await ApplicationTokenProvider.LoginSilentAsync(
            _configuration[ConfigurationConstants.AadTenantId],
            _configuration[ConfigurationConstants.AmsAadClientId],
            _configuration[ConfigurationConstants.AmsAadSecret]);

        return new AzureMediaServicesClient(new Uri(_configuration[ConfigurationConstants.ArmEndpoint]), credentials)
        {
            SubscriptionId = _configuration[ConfigurationConstants.SubscriptionId],
        };
    }
}

DI注册代码

public static IServiceCollection AddAzureMediaServiceClient(this IServiceCollection serviceCollection)
{
      return serviceCollection.AddScoped<IAzureMediaServicesClientProvider, AzureMediaServicesClientProvider>();
}

示例用法

public async Task<Job> CreateJobAsync(string transformName, Job job)
{
    IAzureMediaServicesClient client = await _azureMediaServicesClientFactory.GetClient();

    return await client.Jobs.CreateAsync(_resourceGroupName, _accountName, transformName, job.Name, job);
}

public async Task<Job> GetJobAsync(string transformName, string jobName)
{
    IAzureMediaServicesClient client = await _azureMediaServicesClientFactory.GetClient();

    return await client.Jobs.GetAsync(_resourceGroupName, _accountName, transformName, jobName);
}

需求场景

同一个HTTP请求中调用GetJobAsync和CreateJobAsync时,需确保两个方法使用同一个AzureMediaServicesClient实例:

public async Task TestMethod()
{
    var job = await GetJobAsync(...);

    // 执行一些代码修改

    await CreateJobAsync(...);

    // 需要确保此处与上面的GetJobAsync使用同一个客户端实例
}

不安全的尝试

以下代码实现了实例复用,但存在异步并发下的线程安全问题:

public class AzureMediaServicesClientProvider : IAzureMediaServicesClientProvider
{
    private readonly IConfiguration _configuration;
    private IAzureMediaServicesClient _client;

    public AzureMediaServicesClientProvider(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public async Task<IAzureMediaServicesClient> GetClient()
    {
        if (_client == null)
        {
            ServiceClientCredentials credentials = await ApplicationTokenProvider.LoginSilentAsync(
                _configuration[ConfigurationConstants.AadTenantId],
                _configuration[ConfigurationConstants.AmsAadClientId],
                _configuration[ConfigurationConstants.AmsAadSecret]);

            _client = new AzureMediaServicesClient(new Uri(_configuration[ConfigurationConstants.ArmEndpoint]), credentials)
            {
                SubscriptionId = _configuration[ConfigurationConstants.SubscriptionId],
            };
        }

        return _client;
    }
}

解决方案

因为提供者是Scoped服务,每个请求对应一个独立的AzureMediaServicesClientProvider实例,只需在单个Provider实例内实现线程安全的延迟初始化即可。

方式一:使用SemaphoreSlim保证线程安全

public class AzureMediaServicesClientProvider : IAzureMediaServicesClientProvider, IDisposable
{
    private readonly IConfiguration _configuration;
    private IAzureMediaServicesClient _client;
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public AzureMediaServicesClientProvider(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public async Task<IAzureMediaServicesClient> GetClient()
    {
        if (_client != null)
        {
            return _client;
        }

        await _semaphore.WaitAsync();
        try
        {
            // 双重检查,防止等待期间已完成初始化
            if (_client == null)
            {
                ServiceClientCredentials credentials = await ApplicationTokenProvider.LoginSilentAsync(
                    _configuration[ConfigurationConstants.AadTenantId],
                    _configuration[ConfigurationConstants.AmsAadClientId],
                    _configuration[ConfigurationConstants.AmsAadSecret]);

                _client = new AzureMediaServicesClient(new Uri(_configuration[ConfigurationConstants.ArmEndpoint]), credentials)
                {
                    SubscriptionId = _configuration[ConfigurationConstants.SubscriptionId],
                };
            }
        }
        finally
        {
            _semaphore.Release();
        }

        return _client;
    }

    public void Dispose()
    {
        _semaphore.Dispose();
        (_client as IDisposable)?.Dispose();
    }
}
  • 用SemaphoreSlim限制同一时间只有一个线程执行初始化逻辑
  • 双重检查锁定避免重复创建实例
  • 实现IDisposable释放信号量和客户端资源

方式二:使用AsyncLazy(.NET 6+适用)

如果项目基于.NET 6或更高版本,可直接使用内置的AsyncLazy<T>简化实现:

public class AzureMediaServicesClientProvider : IAzureMediaServicesClientProvider
{
    private readonly AsyncLazy<IAzureMediaServicesClient> _lazyClient;

    public AzureMediaServicesClientProvider(IConfiguration configuration)
    {
        _lazyClient = new AsyncLazy<IAzureMediaServicesClient>(async () =>
        {
            ServiceClientCredentials credentials = await ApplicationTokenProvider.LoginSilentAsync(
                configuration[ConfigurationConstants.AadTenantId],
                configuration[ConfigurationConstants.AmsAadClientId],
                configuration[ConfigurationConstants.AmsAadSecret]);

            return new AzureMediaServicesClient(new Uri(configuration[ConfigurationConstants.ArmEndpoint]), credentials)
            {
                SubscriptionId = configuration[ConfigurationConstants.SubscriptionId],
            };
        });
    }

    public Task<IAzureMediaServicesClient> GetClient()
    {
        return _lazyClient.Task;
    }
}
  • AsyncLazy<T>内置线程安全的异步延迟初始化逻辑,无需手动处理锁
  • 代码更简洁,自动处理并发场景

内容的提问来源于stack exchange,提问作者cah1r

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 09:48:28