【问题标题】:How to initialize static class asynchronously如何异步初始化静态类
【发布时间】:2015-06-19 02:38:33
【问题描述】:

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

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

【问题讨论】:

标签: c# wpf wcf asynchronous async-await


【解决方案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 可能会失败。

【讨论】:

  • 这个实现可能既死锁又错过InitialLoad方法内部发生的任何异常。我不会用它。
  • @YuvalItzchakov 我知道的解决方案 1 将无法死锁,因为没有等待?使用解决方案 2...,是的,你是对的,理论上它可能会死锁,因为 ConfigureAwait 并不能真正保证它不会在同一个线程上等待它从而死锁。
  • @YuvalItzchakov 我已添加有关您指出的问题时刻的信息。谢谢。
【解决方案2】:

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

正确的做法是重构您的Storage 类,以确保其所有异步工作都通过异步方法而不是构造函数完成。我建议通过用 GetInstanceAsync() 方法替换您的 Instance 属性来做到这一点。由于这是获取单例实例的唯一公共接口,因此您将确保始终调用 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 打破了该循环,那么您已经招来了麻烦。

【讨论】:

  • 我在这个实现中看到的一个可能的意外后果是多个线程可以在实例初始化之前调用GetInstanceAsync(即_instance == null评估为真)导致对_instance.InitialLoadAsync()的多次调用,这可能或者可能不会有问题,具体取决于InitialLoadAsync() 的实现。实际上,这也是有问题的,因为每个任务都可能返回一个不同的 Storage 实例,而不一定是最终存储在私有变量中的获胜实例。
  • 好收获。 async lazy 将有助于避免此处的线程冲突,同时避免阻塞。在代码示例中注明。
  • @ToddMenier 非常感谢您的回答!我仍然不明白两件事:首先,谁会调用 InitializeAsync,其次,如果我要使用 AsyncLazy(看起来很有希望),我是否需要使用 InitialLoadAsync 作为它的任务的延续以及它将如何在多线程中工作环境?
  • @AlexVoskresenskiy 关于第一件事:您可以在异步 Window_Loaded 事件处理程序中等待 InitializeAsync。关于第二件事:你可以有类似 private static AsyncLazy&lt;Storage&gt; _lazyInstance = new AsyncLazy&lt;Storage&gt;(async () =&gt; { var st = new Storage(); await st.InitialLoad(); return st; }); public static async Storage GetInstance() { get { var ins = await _lazyInstance; return ins; } } 的东西。它应该在多线程环境中运行良好(如果正确遵循所有 async-await 做法)。
猜你喜欢
  • 1970-01-01
  • 2020-04-14
  • 1970-01-01
  • 1970-01-01
  • 2021-07-21
  • 1970-01-01
  • 1970-01-01
  • 2014-04-15
  • 2023-03-13
相关资源
最近更新 更多