如果一个对象没有静态字段,我是否需要担心它是线程安全的?

Do I need to worry about an object being thread safe if it has no static fields?

我正在写一个装饰器来实现缓存。该对象将被我的 DI 容器注册为单例。因为我知道我正在将我的对象注册为单例,所以代表我的缓存的字段不是静态的。我不确定这是否是最佳做法,但我正试图不惜一切代价避免锁定。我的缓存是延迟初始化的,expensive/run 只有一次操作。我的问题是我是否需要担心多线程 运行 缓存初始化逻辑?我的直觉告诉我 "yes I do need to worry",但我听到其他开发者说 "No point in locking if it's not static"。

//SimpleInjector DI Container configuration
public static class Bootstrapper
{
    public static void ConfigureContainer(Container container)
    {
        container.Register<IQueryHandler<GetFoos, Foo[]>, GetFoosHandler>(Lifestyle.Singleton);
        container.RegisterDecorator<IQueryHandler<GetFoos, Foo[]>, GetFoosCachingHandler>(Lifestyle.Singleton);
    }
}

public class Foo
{
    public int Id;
    public string FooTypeCode;
    public string Name;
}

public class GetFoos : IQuery<Foo[]>
{
    public string FooTypeCode;
}

public class GetFoosCachingHandler : IQueryHandler<GetFoos, Foo[]>
{
    private Lazy<Dictionary<string, Foo[]>> _cache;

    private readonly IQueryHandler<GetFoos, Foo[]> _queryHandler;

    public GetFoosCachingHandler(IQueryHandler<GetFoos, Foo[]> queryHandler)
    {
        _queryHandler = queryHandler;

        _cache = new Lazy<Dictionary<string, Foo[]>>(() =>
        {
            //expensive and run only once operation e.g. subscribe to bus for cache invalid messages and reset cache

            return new Dictionary<string, Foo[]>();
        });
    }

    public Foo[] Handle(GetFoos query)
    {
        var cache = _cache.Value;

        if (!cache.ContainsKey(query.FooTypeCode))
        {
            cache[query.FooTypeCode] = _queryHandler.Handle(new GetFoos { FooTypeCode = query.FooTypeCode });
        }

        return cache[query.FooTypeCode];
    }
}

是的,您需要锁定以防止多个线程运行使用相同的代码。

"No point in locking if it's not static"

只有当每个线程都有自己的 class 实例时才适用。一旦在线程之间共享实例,就需要同步访问。

古法说得对。此时无需添加任何内容。我想添加一点重构。您可能应该从装饰器中提取缓存行为,如下所示:

public class GetFoosCachingHandler : IQueryHandler<GetFoos, Foo[]>{
    private readonly ICache _cache;
    private readonly IQueryHandler<GetFoos, Foo[]> _queryHandler;

    public GetFoosCachingHandler(ICache cache, IQueryHandler<GetFoos, Foo[]> queryHandler){
        _cache = cache;
        _queryHandler = queryHandler;
    }

    public Foo[] Handle(GetFoos query) {
        var result = _cache.Load<Foo[]>(query.FooTypeCode);

        if (result == null) {
            _cache.Store<Foo[]>(query.FooTypeCode, result = _queryHandler.Handle(query));
        }

        return result;
    }
}

这里有几点需要注意:

  • 装饰者不应创建查询,而只是将传入的查询消息传递给其装饰者。
  • 您可能想要创建一个通用的缓存装饰器;这允许您将它应用于多个处理程序。
  • 使用Lazy没用,因为创建一个Dictionary<,>真的很轻量级,总是创建字典。
  • 如果使装饰器通用,则需要采用不同的方法来确定缓存键。过去对我有用的是将完整的查询消息序列化为 JSON(使用 JSON.NET)并将其用作键。你可能想对结果做同样的事情,因为这些结果是可变对象(Foo 和数组都是),使得在多个线程上重用它们很危险(你永远不知道谁改变了它们)。