16

我有一个单例类,它加载了一些关于其构造的数据。问题是加载这些数据需要调用async方法,但构造函数不能async

换句话说,我的班级具有以下结构:

public class Singleton
{
   private static Singleton instance;

   private Singleton() 
   {
       LoadData();
   }

   public static Singleton Instance
   {
      get 
      {
         if (instance == null)
         {
            instance = new Singleton();
         }
         return instance;
       }
    }
}

LoadData()是一个async调用很多async函数以及初始化的函数。我怎样才能LoadData()正确调用,以便一切都正确初始化?

4

4 回答 4

25

如果我们只让类的内部机制为我们工作,那么线程安全、异步单例的解决方案实际上非常简单Task

那么,如何Task工作?假设您有一个a 的实例Task<T>而您await只使用了一次。现在任务被执行,T产生一个值并返回给你。如果您再次await使用相同的任务实例怎么办?在这种情况下,任务只是以完全同步的方式立即返回先前生成的值。

如果您同时从多个线程(通常会出现竞争条件)await执行相同的任务实例怎么办?好吧,第一个(因为会有一个首先到达那里)将执行任务代码,而其他人将等待处理结果。然后当结果产生时,所有的' 将同时完成(实际上)并返回值。await

所以async线程安全的单例的解决方案实际上非常简单:

public class Singleton
{
    private static readonly Task<Singleton> _getInstanceTask = CreateSingleton();

    public static Task<Singleton> Instance
    {
        get { return _getInstanceTask; }
    }

    private Singleton(SomeData someData)
    {
        SomeData = someData;
    }

    public SomeData SomeData { get; private set; }

    private static async Task<Singleton> CreateSingleton()
    {
        SomeData someData = await LoadData();
        return new Singleton(someData);
    }
}

现在您可以通过这种方式访问​​单例:

Singleton mySingleton = await Singleton.Instance;

或者

Singleton mySingleton = Singleton.Instance.Result;

或者

SomeData mySingletonData = (await Singleton.Instance).SomeData;

或者

SomeData mySingletonData = Singleton.Instance.Result.SomeData;

在这里阅读更多:异步单例初始化

于 2015-11-22T12:32:41.930 回答
14

问题是加载这些数据需要调用异步方法,但是构造函数不能是异步的。

虽然不能使构造函数本身异步,但可以从构造函数中调用异步方法。你只是不会立即得到结果。

如果异步方法返回Taskor Task<T>,一旦异步操作完成,您始终可以在任务上使用延续来在类中设置数据,或者只是阻塞结果,具体取决于您的场景中最有意义的内容。在不了解构造此对象的要求的情况下,很难知道在这种情况下什么是合适的。


编辑:

考虑到上面列出的目标,一种选择是更改您的Singleton声明,以便检索 的Instance方法是一种方法,而不是属性。这将允许您使其异步:

public class Singleton
{
   private static Singleton instance;

   private Singleton() 
   {
          // Don't load the data here - will be called separately
   }

   public static async Task<Singleton> GetInstance()
   {
         if (instance == null)
         {
            instance = new Singleton();
            await instance.LoadData();
         }

         return instance;
    }
}

这将允许您await在调用中使用以实际检索实例。这样做的好处是它确实非常清楚地表明您正在调用异步操作,并且您将正确处理结果,因为结果将像任何其他异步方法一样返回。

但是请注意,这不是线程安全的(尽管原来的也不是),所以如果您要从多个线程中使用这个 Singleton,您可能需要重新考虑整体设计。

另一种选择是让您的Singleton类不会自动加载数据。改为使从类中检索数据的方法是异步的。这提供了一些真正的优势,因为使用可能更加标准,并且您可以更轻松地支持来自多个线程的调用(因为您可以控制数据加载过程),而不是通过制作类实例的异步访问。

于 2012-09-21T16:52:20.787 回答
9

您可以为此使用异步延迟初始化

public class Singleton
{
  private static readonly AsyncLazy<Singleton> instance =
      new AsyncLazy<Singleton>(CreateAndLoadData);

  private Singleton() 
  {
  }

  // This method could also be an async lambda passed to the AsyncLazy constructor.
  private static async Task<Singleton> CreateAndLoadData()
  {
    var ret = new Singleton();
    await ret.LoadDataAsync();
    return ret;
  }

  public static AsyncLazy<Singleton> Instance
  {
    get { return instance; }
  }
}

然后你可以像这样使用它:

Singleton singleton = await Singleton.Instance;

使用的一个好处AsyncLazy<T>是它是线程安全的。但是,请注意,它始终在线程池线程上执行其委托。

于 2012-09-21T17:25:47.053 回答
1

好吧,你想要异步初始化一个单例并没有多大意义。如果您只是想在初始化中调用一个返回 Task 的方法,您可以简单地执行以下操作:

var task = MyAsyncMethod();
task.Wait();
return task.Result;

无需制作方法async

但是,如果您想要将单例值作为任务,则可以这样使用 Lazy:

Lazy<Task<int>> l = new Lazy<Task<int>>(async () => { int i = await calculateNumber(); return i; });

此外,Lazy<T>是实现“单例”的首选方法。单例类很难正确(或很难保持正确)......

于 2012-09-21T17:41:44.733 回答