【问题标题】:Correct way to perform asynchronous operations in C#在 C# 中执行异步操作的正确方法
【发布时间】:2020-07-03 11:34:37
【问题描述】:

我有一个类需要很长时间才能启动,因为它在构造函数中执行了一些长时间运行的操作。 我需要创建 3 个这样的对象并将它们添加到字典中。

我正在尝试使用 async,以便创建 3 个线程,每个线程启动其中一个对象,但发现它有点混乱。

最初我以为我会等到所有线程都完成后,然后将结果添加到字典中,但是我无法编译代码,所以我求助于做字典。在线程中添加(这样可以吗?)

我写了以下代码

public void CreateAccountStuffs(ICollection<Account> accounts)
{
    CreateStuff(accounts);
    
}

        
private async void CreateStuff(ICollection<Account> accounts)
{
     var tasks = accounts.Select(a => CreateStuffForAccount(a));
     await Task.WhenAll(tasks);
}

private Task CreateStuffForAccount(Account account)
{
        //Long Running process due to website calls in AccountWrapper construction
        var accountWrapper = new AccountWrapper(account);
                
        
        return Task.FromResult(accountWrapper);
}

最后一行似乎毫无意义,但没有它我无法编译代码。我觉得我在这里遗漏了一些非常明显的东西,但是我的多线程技能太生疏了,我很难看到什么。

我的直觉是,有一种更简单的方法来编写我需要做的事情,但我不知道那是什么。

例如我注意到this question 似乎建议您可以编写一个返回 Task 的方法,并简单地在方法主体中编写返回“This String”,但是,该构造不会为我编译。我得到错误无法从字符串转换为任务

注意 - 已编辑以删除不属于问题的代码 - 之前涉及 2 个构造函数,这使事情变得混乱 - 这是 AccountWrapper 中长期运行的构造函数。

【问题讨论】:

  • 您没有任何异步代码。简单地用async 标记方法不会使其异步。您可以与Task.Run(() =&gt; .... 并行运行非异步代码
  • 您需要以异步方式执行长时间运行的任务,但这在构造函数中是不可能的。也许这为你清除了一些问题:stackoverflow.com/questions/23048285/… 或者这个:blog.stephencleary.com/2013/01/async-oop-2-constructors.html
  • @Homungus 第二个很有用谢谢 - 我将重构使用那里建议的模式,看看我最终会在哪里。
  • 我已经重构 AccountWrapper 类以使用工厂构造函数 public static Task CreateAsync(Account account) 但是,现在在调用代码中,如果我尝试访问 AccountWrapper - 它会告诉我它无法从 Task 转换为 AccountWrapper,我应该使用 await,但如果我将 await 放入此代码中,那么我也需要使其异步!

标签: c# asynchronous task


【解决方案1】:

将异步方法放在构造函数中而不等待它是不好的设计。因为当您的代码从构造函数返回时,如果构造已经完成并且您的类实例可能处于不一致状态,您现在不会这样做。您可以简单地创建静态工厂方法

public class MyClass
{
    private readonly IDictionary<int, IAccountStuff> _accountStuffs;
    private readonly ICollection<Account> _accounts;

    protected MyClass()
    {
        _accounts = dataLayer.GetAccounts();
        // remove it
        // CreateStuff(accounts);
    }

    private async void CreateStuff(ICollection<Account> accounts)
    {
        var tasks = accounts.Select(a => CreateStuffForAccount(a));
        await Task.WhenAll(tasks);
    }

    private Task CreateStuffForAccount(Account account)
    {
        //Long Running process due to website calls in AccountWrapper construction
        return Task.Run(async () =>
        {
            // do something with you account
            // to do this you dictionary should be concurrent
            this._accountStuffs.Add(account.Id, accountWrapper.Stuff);
        });
    }

    // factory method
    public static async Task Create()
    {
        var myClass = new MyClass();
        await myClass.CreateStuff(); 
    }
}


....
// and somwhere you may use
public async Task Foo()
{
    var myClass = await MyClass.Create();
    // now you can safely use you class instance myClass 
}

【讨论】:

    【解决方案2】:

    记住并行性和异步性之间的区别很重要。前者旨在使用多个内核以获得更好的性能,后者旨在隐藏延迟,尤其是在执行 IO 操作时。目前尚不清楚在这种情况下您要做什么。

    你使用Task.FromResult(accountWrapper),这只是创建了一个已经完成的任务,所以这里没有异步。这适用于同步方法需要实现异步接口时,例如如果结果被缓存,那么它可以直接返回。

    要并行创建对象,您可以使用Parallel.ForParallel.Foreach 并行创建所有accountWrapper。但这不会使用异步,即调用会阻塞,直到所有帐户包装器都创建完毕。

    要异步创建你的类,你可以使用

    Task.Run(() => new MyClass())
    

    这将在后台线程上创建类并返回完成后完成的任务。使用 await 从任务中获取结果。请记住,该结果可能是一个例外。您通常希望避免使用async void,以便调用者可以处理这些异常。见best practices

    您可以结合使用这两种方法,即在后台线程上创建您的类,并且构造函数可以使用 Parallel.For/Foreach 来创建昂贵的AccountWrappers。

    【讨论】:

      猜你喜欢
      • 2019-02-24
      • 1970-01-01
      • 2017-06-23
      • 1970-01-01
      • 2012-07-14
      • 2018-09-16
      • 1970-01-01
      • 1970-01-01
      • 2017-04-25
      相关资源
      最近更新 更多