如何以线程安全方式在请求作用域类中返回同一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
相关产品推荐
相关产品推荐

