单例服务异步调用并发问题:修复方案、模式违规及术语问询
问题背景
存在单例服务类ItemsDataSource: IItemsDataSource被注入至多个业务领域类,这些类会异步调用该服务的方法。代码实现如下:
public interface IItemsDataSource { Task<IEnumerable<string>> GetItemsAsync(); void SetSourceConfiguration(JToken src); } public class ItemsDataSource : IItemsDataSource { private JToken m_configuration; public Task<IEnumerable<string>> GetItemsAsync() { // 使用m_configuration获取数据 } public void SetSourceConfiguration(JToken config) { m_configuration = config; } }
当多线程异步运行时(如线程T1、T2),会出现问题:T1调用SetSourceConfiguration(config1)后异步执行GetItemsAsync(),T2在T1完成前调用SetSourceConfiguration(config2),导致T1意外使用config2引发异常。
问题解答
1. 是否有更优修复方案?
除了移除SetSourceConfiguration并将JToken config传入GetItemsAsync、业务类加锁外,还有两种更优的方案:
- 工厂模式创建带配置的实例:将
ItemsDataSource改为非单例,通过IItemsDataSourceFactory工厂类根据不同配置创建对应实例,每个业务类持有对应配置的数据源实例。从根源上避免共享可变状态,完全消除并发冲突,且无锁开销。 - 依赖注入范围实例(Scoped):若使用DI框架,可将
ItemsDataSource注册为Scoped实例,在每个业务操作的生命周期内设置一次配置,确保同一生命周期内的异步调用共享同一配置,不同生命周期互不干扰。 - 完全无状态化改造:彻底移除类级的
m_configuration成员,所有依赖配置的逻辑都通过方法参数传递,让服务天生支持高并发,符合无状态服务的设计原则。
这些方案相比加锁,避免了锁带来的性能损耗和死锁风险;相比简单传参,更符合领域设计的职责划分,代码可读性和可维护性更高。
2. 该bug源于哪种设计模式违规?
这个bug源于单例模式的误用:单例模式的核心是提供全局唯一、状态稳定的实例,适用于无状态服务或全局共享不变状态的场景。但此处将持有可变状态的类设计为单例,导致多个并发调用共享并修改同一状态,完全违背了单例模式的适用场景。
同时也违反了无状态服务原则:服务类应设计为无状态,所有必要上下文信息通过方法参数或上下文传递,而非存储为类成员变量,这样才能安全支持并发调用。
3. 该bug的技术术语是什么?
这个bug的技术术语是竞态条件(Race Condition):当多个线程/异步任务同时访问并修改共享可变状态时,最终执行结果依赖于线程调度顺序,从而出现不符合预期的行为。这里T1和T2对m_configuration的读写操作未做同步,引发竞态条件,导致T1使用了错误的配置。
内容的提问来源于stack exchange,提问作者EEAH

