如何异步初始化静态类

时间:2015-04-13 17:16:01

标签: c# wpf wcf asynchronous async-await

我有一个Singleton(好吧,它可以是一个静态类,无所谓),这是我的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访问此类,如下所示:

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线程,但不明白如何解决这个问题。

我会感激任何帮助!

2 个答案:

答案 0 :(得分:8)

您基本上遇到了异步/等待中的限制:无法将构造函数标记为异步。解决这个问题的正确方法是通过从构造函数中调用Wait来解决问题。那个作弊行为 - 它会阻止你,让你所有的好的异步都变得没有意义,更糟糕的是它会在你发现的时候引发死锁。

正确的方法是重构你的Storage类,以确保所有的异步工作都是从异步方法而不是构造函数完成的。我建议您使用Instance方法替换GetInstanceAsync()属性,从而实现此目的。由于这是获取单例实例的唯一公共接口,因此您必须确保始终调用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

 }

现在,如何在不阻止的情况下从Storage.GetInstanceAsync()的构造函数中调用MainWindowViewModel?正如您可能猜到的那样,您无法做到,因此您需要进行类似的重构。类似的东西:

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打破了这个周期,那么您已经在这个周期中打破了这个问题。

答案 1 :(得分:1)

解决方案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指出的那样,最初发布的解决方案会默默地忽略异步方法中可能发生的任何异常,因此您必须尝试包装异步方法体-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可能会失败。