【问题标题】:Force C# async tasks to be lazy?强制 C# 异步任务变得懒惰?
【发布时间】:2017-07-24 08:44:04
【问题描述】:

我有一个由特殊工厂创建的对象树的情况。这有点类似于 DI 容器,但不完全一样。

对象的创建总是通过构造函数发生,并且对象是不可变的。

在给定的执行中可能不需要对象树的某些部分,应该延迟创建。所以构造函数参数应该只是一个按需创建的工厂。这看起来像是 Lazy 的工作。

但是,对象创建可能需要访问慢速资源,因此始终是异步的。 (对象工厂的创建函数返回一个Task。)这意味着Lazy 的创建函数需要是异步的,因此注入的类型需要是Lazy<Task<Foo>>

但我宁愿没有双层包装。我想知道是否可以强制 Task 变得懒惰,即创建一个保证在等待之前不会执行的 Task。据我了解,Task.RunTask.Factory.StartNew 可能随时开始执行(例如,如果池中的线程空闲),即使没有任何东西在等待它。

public class SomePart
{
  // Factory should create OtherPart immediately, but SlowPart
  // creation should not run until and unless someone actually
  // awaits the task.
  public SomePart(OtherPart eagerPart, Task<SlowPart> lazyPart)
  {
    EagerPart = eagerPart;
    LazyPart = lazyPart;
  }

  public OtherPart EagerPart {get;}
  public Task<SlowPart> LazyPart {get;}
}

【问题讨论】:

标签: c# .net asynchronous .net-core


【解决方案1】:

我不确定您为什么要避免使用 Lazy&lt;Task&lt;&gt;&gt;,,但如果只是为了让 API 更易于使用,因为这是一个属性,您可以使用支持字段:

public class SomePart
{
    private readonly Lazy<Task<SlowPart>> _lazyPart;

    public SomePart(OtherPart eagerPart, Func<Task<SlowPart>> lazyPartFactory)
    {
        _lazyPart = new Lazy<Task<SlowPart>>(lazyPartFactory);
        EagerPart = eagerPart;
    }

    OtherPart EagerPart { get; }
    Task<SlowPart> LazyPart => _lazyPart.Value;
}

这样,使用就好像它只是一个任务,但是初始化是惰性的,并且只会在需要时才产生工作。

【讨论】:

    【解决方案2】:

    @Max' 的回答很好,但我想添加基于 cmets 中提到的 Stephen Toub' 文章之上的版本:

    public class SomePart: Lazy<Task<SlowPart>>
    {
        public SomePart(OtherPart eagerPart, Func<Task<SlowPart>> lazyPartFactory)
            : base(() => Task.Run(lazyPartFactory))
        {
            EagerPart = eagerPart;
        }
    
        public OtherPart EagerPart { get; }
        public TaskAwaiter<SlowPart> GetAwaiter() => Value.GetAwaiter();
    }
    
    1. SomePart 明确继承自 Lazy&lt;Task&lt;&gt;&gt;,因此很明显它是 lazyasyncronous

    2. 调用基本构造函数将lazyPartFactory 包装到Task.Run 以避免在工厂需要在真正的异步部分之前进行一些cpu 繁重的工作时出现长阻塞。如果不是您的情况,只需将其更改为base(lazyPartFactory)

    3. SlowPart 可通过 TaskAwaiter 访问。所以 SomePart 的公共接口是:

      • var eagerValue = somePart.EagerPart;
      • var slowValue = await somePart;

    【讨论】:

    • @Max 答案可能不是更好,因为使用组合而不是继承? IsValueCreated 可能在 API 上下文中被误解,不是吗?
    • 是的,完全同意
    【解决方案3】:

    使用Task 的构造函数使任务变得懒惰,也就是在你说它运行之前不会运行,所以你可以这样做:

    public class TestLazyTask
    {
        private Task<int> lazyPart;
    
        public TestLazyTask(Task<int> lazyPart)
        {
            this.lazyPart = lazyPart;
        }
    
        public Task<int> LazyPart
        {
            get
            {
                // You have to start it manually at some point, this is the naive way to do it
                this.lazyPart.Start();
                return this.lazyPart;
            }
        }
    }
    
    
    public static async void Test()
    {
        Trace.TraceInformation("Creating task");
        var lazyTask = new Task<int>(() =>
        {
            Trace.TraceInformation("Task run");
            return 0;
        });
        var taskWrapper = new TestLazyTask(lazyTask);
        Trace.TraceInformation("Calling await on task");
        await taskWrapper.LazyPart;
    } 
    

    结果:

    SandBox.exe Information: 0 : Creating task
    SandBox.exe Information: 0 : Calling await on task
    SandBox.exe Information: 0 : Task run
    

    但是我强烈建议您使用Rx.NETIObservable,因为在您的情况下,您将获得更少的麻烦,因为您可以在正确的时刻开始您的任务。 在我看来,它也使代码更清晰

    public class TestLazyObservable
    {
        public TestLazyObservable(IObservable<int> lazyPart)
        {
            this.LazyPart = lazyPart;
        }
    
        public IObservable<int> LazyPart { get; }
    }
    
    
    public static async void TestObservable()
    {
        Trace.TraceInformation("Creating observable");
        // From async to demonstrate the Task compatibility of observables
        var lazyTask = Observable.FromAsync(() => Task.Run(() =>
        {
            Trace.TraceInformation("Observable run");
            return 0;
        }));
    
        var taskWrapper = new TestLazyObservable(lazyTask);
        Trace.TraceInformation("Calling await on observable");
        await taskWrapper.LazyPart;
    }
    

    结果:

    SandBox.exe Information: 0 : Creating observable
    SandBox.exe Information: 0 : Calling await on observable
    SandBox.exe Information: 0 : Observable run
    

    更清楚一点:这里的Observable 处理何时启动任务,默认情况下它是 Lazy 并且每次订阅时都会运行任务(这里 subscribe 由启用使用 @987654329 的等待者使用@关键字)。

    如果需要,您可以让任务每分钟(或永远)只运行一次,并将其结果发布给所有订阅者以节省性能,例如,就像在现实世界的应用程序中一样,所有这些等等由 observables 处理。

    【讨论】:

    • I strongly recommend you to use Rx.NET and IObservable - 这就是我所说的对于这个特定问题的过度杀伤力。反应性很酷,但我会再保留一次
    • 如果您不忽略现实世界应用程序中每天必须处理的所有特定问题,这并不过分。您可以争辩说这就像古老的“使用 jquery”一样,而且不知何故,但我真的觉得 OP 场景与此相关。另外,我确实在没有 Observables 的情况下回答了他的问题,我提到它是他应该使用的。
    • 这就是为什么最好不要通过选择不合适的工具来制造更多问题并增加熵,恕我直言
    • 这是我能想到的最合适的惰性异步处理工具。它不需要特殊的样板代码并且适应用例。我认为,而且我认为,在这种情况下,与通过快速解决方法解决问题相比,它会节省更多的麻烦
    • 好吧,也许我不太理解 Rx,但我认为它主要用于处理基于推送的事件/对象/等的异步流。
    【解决方案4】:

    声明:

    private Lazy<Task<ServerResult>> _lazyServerResult;`
    
    
    ctor()
    {
        _lazyServerResult = new Lazy<Task<ServerResult>>(async () => await 
            GetServerResultAsync())
    }
    

    用法:

    ServerResult result = await _lazyServerResult.Value;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-12
      • 2019-05-19
      • 1970-01-01
      • 2010-11-29
      • 1970-01-01
      相关资源
      最近更新 更多