简体   繁体   English

如何异步初始化静态类

[英]How to initialize static class asynchronously

I have a Singleton (well, it can be a static class, doesn't matter), which is a facade for some data for my WPF application. 我有一个Singleton(好吧,它可以是一个静态类,无所谓),这是我的WPF应用程序的一些数据的外观。 I want to load this data async via WCF. 我想通过WCF加载此数据异步。 Here is my implementation: 这是我的实现:

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());
    }
}

And i access this class from my ViewModel like this: 我从我的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;
    }
}

In my view i have a binding for RootSensor. 在我看来,我有RootSensor的绑定。 Everything works great, but i have one problem: all my async code executes and then i catch a deadlock on InitialLoad().Wait(); 一切都很好,但我有一个问题:所有我的异步代码执行,然后我在InitialLoad().Wait();上捕获死锁InitialLoad().Wait(); . I understand that it involves WPF UI thread somehow, but don't understand how to fix this. 我知道它涉及WPF UI线程,但不明白如何解决这个问题。

I'll be gratefull for any help! 我会感激任何帮助!

You've basically come up against a limitation in async/await: Constructors cannot be marked async . 您基本上遇到了异步/等待的限制: 构造函数不能标记为异步 The right way to solve that isn't by calling Wait from the constructor. 解决问题的正确方法不是通过从构造函数调用Wait That's cheating - it'll block, rendering all your nice asynchrony moot, and what's worse is it'll invite deadlocks as you discovered. 这是作弊 - 它会阻止,使你所有的好的异步都变得没有意义,更糟糕的是它会在你发现时引发死锁。

The right way to do it is to refactor your Storage class to ensure all of its async work is done from an async method rather than a constructor. 正确的方法是重构您的Storage类,以确保它的所有异步工作都是从异步方法而不是构造函数完成的。 I'd suggest doing this by replacing your Instance property with a GetInstanceAsync() method. 我建议通过用GetInstanceAsync()方法替换您的Instance属性来做到这一点。 As this is the only public interface to getting the singleton instance, you'll ensure that InitialLoad (which I'd rename InitialLoadAsync ) will always get called. 由于这是获取单例实例的唯一公共接口,因此您将确保始终调用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

 }

Now, how do you call Storage.GetInstanceAsync() from MainWindowViewModel 's constructor without blocking? 现在,如何在不阻塞的情况下从MainWindowViewModel的构造函数中调用Storage.GetInstanceAsync() As you've probably guessed, you can't, so you'll need to refactor that similarly. 你可能已经猜到了,你做不到,所以你需要同样地重构它。 Something like: 就像是:

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);
    }
}

And of course whatever calls await MainWindowViewModel.InitializeAsync() needs to be marked async as well. 当然,任何调用await MainWindowViewModel.InitializeAsync()需要标记为async async/await are said to spread like a zombie virus through your code, and that's natural. async / await据说通过你的代码像僵尸病毒一样传播,这很自然。 If anywhere along the call stack you've broken that cycle with a .Wait() or .Result , you've invited trouble. 如果你在调用堆栈的任何地方用.Wait().Result打破了那个循环,你就会遇到麻烦。

Solution 1 - What if you do not wait at all? 解决方案1 ​​ - 如果你不等待怎么办?

If you wait for some task in the constructor then your app won't launch till it gets the data. 如果您在构造函数中等待某个任务,那么您的应用程序将无法启动,直到获取数据为止。 Thus the launch time of the app is increased and UX will be somewhat less satisfying. 因此,应用程序的启动时间增加,用户体验将不那么令人满意。

But if you just set some dummy default data and launch the data retrieval fully async without wait or await, you will have no need to Wait and improve the overall UX. 但是,如果您只是设置一些虚拟默认数据并在没有等待或等待的情况下完全异步启动数据检索,您将无需Wait并改善整体UX。 To prevent any unnecessary operations by user you can either make dependent controls disabled or use Null object pattern : 要防止用户执行任何不必要的操作,您可以禁用相关控件或使用Null对象模式

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: XAML:

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

Important warning: As it has been pointed by @YuvalItzchakov originally posted solution will silently miss any exceptions that can occur in the async method, so you will have to wrap your async method bodies with some try-catch logic calling Environment.FailFast to fail fast and loud or use appropriate ContinueWith with TaskContinuationOptions.OnlyOnFaulted . 重要警告:正如@YuvalItzchakov所指出的那样,最初发布的解决方案将默默地错过异步方法中可能发生的任何异常,因此您必须使用一些调试Environment.FailFast调用Environment.FailFast try-catch逻辑来包装异步方法体。大声或使用适当的ContinueWithTaskContinuationOptions.OnlyOnFaulted

Bad solution 2(!May actually fail!) - ConfigureAwait(false) 糟糕的解决方案2(!实际上可能会失败!) - ConfigureAwait(false)

Configure await false on all async calls will prevent(in most cases) any use of the original sync context allowing you to wait: 在所有异步调用上配置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();
    }

It will work in most cases, but it is not actually guaranteed that it won't use the same thread to await leading to the same deadlock problem. 它在大多数情况下都可以工作,但实际上并没有保证它不会使用相同的线程来等待导致相同的死锁问题。

Bad solution 3 - use Task.Run to assuredly avoid any deadlocking. 糟糕的解决方案3 - 使用Task.Run确保避免任何死锁。

You can use one not very good async practice and wrap your entire operation into new thread pool task with Task.Run: 您可以使用一个不是很好的异步练习,并使用Task.Run将整个操作包装到新的线程池任务中:

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

It will squander one thread from the thread pool on wait, but it will work for sure, while Solution 2 may fail. 它会在等待时从线程池中浪​​费一个线程,但它肯定会起作用,而解决方案2可能会失败。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM