如何异步初始化static class

How to initialize static class asynchronously

我有一个 Singleton(好吧,它可以是静态的 class,没关系),它是我的 WPF 应用程序的一些数据的外观。我想通过 WCF 异步加载此数据。这是我的实现:

public class Storage
{
    private static readonly Lazy<Storage> _lazyInstance = new Lazy<Storage>(()=>new Storage());

    public static Storage Instance
    {
        get { return _lazyInstance.Value; }
    }

    private Storage()
    {
        Data = new Datastorage(SettingsHelper.LocalDbConnectionString);
        InitialLoad().Wait();
    }

    public Datastorage Data { get; private set; }

    private async Task InitialLoad()
    {
        var tasks = new List<Task>
        {
            InfoServiceWrapper.GetSomeData()
            .ContinueWith(task => Data.StoreItem(task.Result)),
            InfoServiceWrapper.GetAnotherData()
            .ContinueWith(task => Data.StoreItem(task.Result)),
            InfoServiceWrapper.GetSomeMoreData()
            .ContinueWith(task => Data.StoreItem(task.Result)),
        };
        await Task.WhenAll(tasks.ToArray());
    }
}

然后我从我的 ViewModel 中访问这个 class,如下所示:

public class MainWindowViewModel:ViewModelBase
{
    public  SensorDTO RootSensor { get; set; }
    public  MainWindowViewModel()
    {
        var data = Storage.Instance.Data.GetItem<SensorDTO>(t=>t.Parent==t);
        RootSensor = data;
    }
}

在我看来,我绑定了 RootSensor。一切都很好,但我有一个问题:我所有的异步代码都执行了,然后我在 InitialLoad().Wait(); 上遇到了死锁。我知道它以某种方式涉及 WPF UI 线程,但不明白如何解决这个问题。

如有任何帮助,我将不胜感激!

解决方案 1 - 如果您根本不等待怎么办?

如果您在构造函数中等待某个任务,那么您的应用在获取数据之前不会启动。因此,应用程序的启动时间会增加,用户体验会不太令人满意。

但是,如果您只是设置一些虚拟默认数据并启动完全异步的数据检索而无需等待或等待,您将无需 Wait 并改善整体用户体验。为了防止用户进行任何不必要的操作,您可以禁用相关控件或使用 Null object pattern:

public class Waiter : INotifyPropertyChanged
{
    public async Task<String> Get1()
    {
        await Task.Delay(2000);            
        return "Got value 1";
    }

    public async Task<String> Get2()
    {
        await Task.Delay(3000);
        return "Got value 2";
    }

    private void FailFast(Task task)
    {
        MessageBox.Show(task.Exception.Message);
        Environment.FailFast("Unexpected failure");
    }

    public async Task InitialLoad()
    {          
        this.Value = "Loading started";

        var task1 = Get1();
        var task2 = Get2();

        // You can also add ContinueWith OnFaulted for task1 and task2 if you do not use the Result property or check for Exception
        var tasks = new Task[]
        {
            task1.ContinueWith(
                (prev) => 
                    this.Value1 = prev.Result),
            task2.ContinueWith(
                (prev) => 
                    this.Value2 = prev.Result)
        };

        await Task.WhenAll(tasks);

        this.Value = "Loaded";
    }

    public Waiter()
    {
        InitialLoad().ContinueWith(FailFast, TaskContinuationOptions.OnlyOnFaulted);
    }

    private String _Value,
        _Value1,
        _Value2;

    public String Value
    {
        get
        {
            return this._Value;
        }
        set
        {
            if (value == this._Value)
                return;
            this._Value = value;
            this.OnPropertyChanged();
        }
    }


    public String Value1
    {
        get { return this._Value1; }
        set
        {
            if (value == this._Value1)
                return;
            this._Value1 = value;
            this.OnPropertyChanged();
        }
    }


    public String Value2
    {
        get { return this._Value2; }
        set
        {
            if (value == this._Value2)
                return;
            this._Value2 = value;
            this.OnPropertyChanged();
        }
    }

    public void OnPropertyChanged([CallerMemberName]String propertyName = null)
    {
        var propChanged = this.PropertyChanged;

        if (propChanged == null)
            return;

        propChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        this.DataContext = new Waiter();          
    }
}

XAML:

    <StackPanel>
        <TextBox Text="{Binding Value}"/>
        <TextBox Text="{Binding Value1}"/>
        <TextBox Text="{Binding Value2}"/>
    </StackPanel>

重要警告:正如@YuvalItzchakov 所指出的,最初发布的解决方案将默默地错过异步方法中可能发生的任何异常,因此您必须包装您的异步带有一些 try-catch 逻辑的方法体调用 Environment.FailFast 以快速大声地失败或使用适当的 ContinueWithTaskContinuationOptions.OnlyOnFaulted.

错误的解决方案 2(!实际上可能会失败!)- ConfigureAwait(false)

Configure await false 所有异步调用将阻止(在大多数情况下)使用原始同步上下文,让您等待:

   public async Task<String> Get1()
    {
        await Task.Delay(2000).ConfigureAwait(false);            
        return "Got value 1";
    }

    public async Task<String> Get2()
    {
        await Task.Delay(3000).ConfigureAwait(false);
        return "Got value 2";
    }

    public async Task InitialLoad()
    {          
        this.Value = "Loading started";

        var tasks = new Task[]
        {
            Get1().ContinueWith(
                (prev) => 
                    this.Value1 = prev.Result),
            Get2().ContinueWith(
                (prev) => 
                    this.Value2 = prev.Result)
        };

        await Task.WhenAll(tasks).ConfigureAwait(false);

        this.Value = "Loaded";
    }

    public Waiter()
    {
        InitialLoad().Wait();
    }

大多数情况下都可以,但实际上不能保证不会使用同一个线程等待导致同样的死锁问题。

错误的解决方案 3 - 使用 Task.Run 来确保避免任何死锁。

您可以使用一种不太好的异步实践,并将您的整个操作包装到新的线程池任务中 Task.Run:

private void SyncInitialize()
{
    Task.Run(() => 
                 InitialLoad().Wait())
        .Wait();
}

它会在等待时从线程池中浪​​费一个线程,但它肯定会工作,而解决方案 2 可能会失败。

您基本上遇到了 async/await 中的限制:构造函数不能标记为异步。解决这个问题的正确方法不是从构造函数中调用 Wait。那是作弊 - 它会阻塞,让你所有漂亮的异步变得毫无意义,更糟糕的是,它会像你发现的那样引发死锁。

正确的做法是重构您的 Storage class 以确保其所有异步工作都是从异步方法而不是构造函数完成的。我建议您将 Instance 属性 替换为 GetInstanceAsync() 方法。由于这是获取单例实例的唯一 public 接口,因此您将确保 InitialLoad(我将其重命名为 InitialLoadAsync)将始终被调用。

public class Storage
{
    private static Storage _instance;

    public static async Task<Storage> GetInstanceAsync()
    {
        if (_instance == null)
        {
            // warning: see comments about possible thread conflict here
            _instance = new Storage();
            await _instance.InitialLoadAsync();
        }
        return _instance;
    }

    private Storage() 
    {
        Data = new Datastorage(SettingsHelper.LocalDbConnectionString);
    }

    // etc

 }

现在,如何在不阻塞的情况下从 MainWindowViewModel 的构造函数中调用 Storage.GetInstanceAsync()?正如您可能已经猜到的那样,您不能,因此您需要以类似的方式重构它。类似于:

public class MainWindowViewModel : ViewModelBase
{
    public  SensorDTO RootSensor { get; set; }

    public async Task InitializeAsync()
    {
        var storage = await Storage.GetInstanceAsync()
        RootSensor.Data.GetItem<SensorDTO>(t=>t.Parent==t);
    }
}

当然,任何调用 await MainWindowViewModel.InitializeAsync() 也需要标记为 async。 async/await 据说通过您的代码像僵尸病毒一样传播,这是很自然的。如果在调用堆栈的任何地方你用 .Wait().Result 打破了那个循环,你就招来了麻烦。