【问题标题】:Create an Awaitable Cold Task创建一个可等待的冷任务
【发布时间】:2015-04-23 13:35:50
【问题描述】:

我有一个异步方法,完成后我希望运行另一个方法。如果我简单地调用该方法并添加 .ContinueWith()

,这很好用

但是,我有一个新要求,即只有在我能够将任务添加到并发字典时才开始任务。

我希望构建任务,尝试添加它然后启动任务

但是,Task.Start() 似乎立即完成了导致继续操作运行的任务,并且任何等待......而不是等待。

谁能解释为什么会发生这种情况以及实现我的目标的正确方法?

namespace UnitTestProject2
{
    [TestClass]
    public class taskProblem
    {
        [TestMethod]
        public void Test()
        {
            CancellationTokenSource cancel = new CancellationTokenSource();
            ConcurrentDictionary<Guid, Task> tasks = new ConcurrentDictionary<Guid,Task>();
            Guid id = Guid.NewGuid();
            Task t = new Task(async () => await Get(), cancel.Token);
            t.ContinueWith(Complete);
            if (tasks.TryAdd(id, t))
            {
                t.Start();
            }
            else
            {
                //another thread is stopping stuff dont start new tasks
            }

            t.Wait(); //expected to wait for the get function to complete
            Console.WriteLine("end test");
        }

        public async Task Get()
        {
            Console.WriteLine("start task");
            await Task.Delay(10000);
            Console.WriteLine("end task");
        }

        public void Complete(Task t)
        {
            Console.WriteLine("Complete");
        }
    }
}

输出:

start task
end test
Complete

预期输出:

start task
end task
Complete
end test

更新:似乎没有办法创建一个不会立即在 Task.Start 上立即启动或完成的新任务?

【问题讨论】:

  • 您要解决的真正问题是什么?该要求没有任何意义,并且将任务包装两层深也无济于事。创建冷任务并致电Start从不的充分理由。此外,没有理由包装Get,它已经在另一个任务中返回了正在运行的任务。只需写Task t=Get();。至于要求,没有意义。您始终可以将新项目添加到并发字典中,因为您实际上是在为其创建新的 Guid 键。
  • 看起来您将异步委托传递给 Task 构造函数,它不会等待从 Get 返回的任务完成。你为什么要在另一个任务中包装对Get 的调用?
  • 真正的应用程序从队列发送推送通知,并有一个 Stop() 方法(从另一个线程调用)来停止启动新任务。在这种情况下,我需要未启动的任务,因此我可以以线程安全的方式将其添加到并发字典中。显然还有其他方法可以做到这一点,如果我无法解决这个问题,我将不得不使用这些方法
  • 也是第一个应该被取消令牌取消的任务,而不是第二个

标签: c# async-await task-parallel-library


【解决方案1】:

您的委托是异步无效的。 异步 void 方法是一劳永逸的方法。

参见模式和反模式总结的第一点:http://rarcher.azurewebsites.net/Post/PostContent/31

也许你可以这样做:

[TestFixture]
public class FIXTURENAMETests {
  [Test]
  public async Task NAME() {
    var tcs = new TaskCompletionSource<bool>();
    Task t = LongRunningStuff(tcs);

    if (CanInsertInDictionary(t)) {
      tcs.SetResult(true);
    } else {
      tcs.SetException(new Exception());
    }

    Trace.WriteLine("waiting for end");

    try {
      await t;
    }
    catch (Exception exception) {
      Trace.WriteLine(exception);
    }

    Trace.WriteLine("end all");
  }

  private bool CanInsertInDictionary(Task task) {
    return true;
  }

  private async Task LongRunningStuff(TaskCompletionSource<bool> tcs) {
    Trace.WriteLine("start");
    try {
      await tcs.Task;
    }
    catch (Exception) {
      return;
    }
    Trace.WriteLine("do long running stuff");
    await Task.Delay(10000);
    Trace.WriteLine("end");
  }
}

【讨论】:

  • 啊!我有点意识到这一定是关于anon delgate的。但是你也有解决办法吗?
  • 我已经编辑了答案。也许你可以做这样的事情。
  • 嗯,另一种可能的解决方法,但真的没有办法创建一个未启动的等待任务吗?真的我想做 Task t = Get().DontStartYet() 什么的
  • 我从来没有遇到过需要做这样的事情的问题。
【解决方案2】:

但是,我有一个新要求,即只有在我能够将任务添加到并发字典时才开始任务。

这是上述问题的一种可能的解决方案:

  • Task&lt;T&gt; 包裹在Lazy&lt;&gt;
  • 通过GetOrAdd将惰性任务添加到ConcurrentDictionary
  • 使用GetOrAdd 的输出来等待任务

这样,当ConcurrentDictionary 正在执行它的内部工作以处理冲突等时,任务将不会启动,并且所有后续await 将使用与字典中给定键关联的同一个任务对象.

总之,在多个线程向字典添加任务的情况下,下面的代码只允许对字典中的给定键执行一次任务主体。

代码如下:

[TestMethod]
public void ConcurrentMapLazyTask() {

  Func<Task> Get = async () => {
    Trace.WriteLine("Start task.");
    await Task.Delay(200);
    Trace.WriteLine("End task.");
  };

  Action<Task> Complete = (t) => {
    Trace.WriteLine("Complete.");
  };

  var mp = new ConcurrentDictionary<string, Lazy<Task>>();

  Func<string, Lazy<Task>> valueFactory = (sKey) => {
    var s = Guid.NewGuid().ToString();
    Trace.WriteLine(string.Format("valueFactory called => {0}", s));
    return new Lazy<Task>(() => {
      Trace.WriteLine(string.Format("LazyTask factory called for {0}", s));
      var t = Task.Run(async () => {
        Trace.WriteLine(string.Format("Task.Run executed for {0}", s));
        await Get();
      });
      return Task.WhenAll(t, t.ContinueWith(Complete));
    });
  };

  Func<Task> TestAsync = async () => {
    var lazyTask = mp.GetOrAdd("test", valueFactory);
    await lazyTask.Value;
  };

  Action TestSync = () => {
    TestAsync().Wait();
    Trace.WriteLine("End test.");
  };
  Action TestSyncSlow = () => {
    Thread.Sleep(50);
    TestAsync().Wait();
    Trace.WriteLine("End slow test.");
  };
  //Parallel.Invoke(TestSync);
  Parallel.Invoke(TestSync, TestSyncSlow, TestSync, TestSync);
}

这是输出:

valueFactory called => e35b0e9e-3326-41bb-b3b9-eb4e6dc37391
valueFactory called => e4929d93-4c77-4fbf-8e90-e8d6e4d7b009
LazyTask factory called for e35b0e9e-3326-41bb-b3b9-eb4e6dc37391
valueFactory called => 8863fb7f-1309-4c27-b805-71467022ac74
Task.Run executed for e35b0e9e-3326-41bb-b3b9-eb4e6dc37391
Start task.
End task.
Complete.
End test.
End test.
End slow test.
End test.
  • Task.Run 只执行一次
  • Lazy factory 只被调用过一次
  • GetOrAdd 中使用valueFactory 参数可以减少创建的惰性对象的数量(3 个惰性包装器由4 次并发调用GetOrAdd 产生)

【讨论】:

  • 超级狡猾!不确定它是否与我想要的完全一样,但这是一个老问题,我不再记得现实生活中的要求了
  • 如果我没记错的话,对我来说关键是因为任务已经完成,所以没有执行。你没有继续,如果我正确地阅读了代码,其他任务就不会因为 GetOrAdd 而存在?我需要在编辑器中获取您的代码才能正确理解它
  • 对我来说,关键点是将任务对象用作承诺或未来,并将其存储在 ConcurrentDictionary 中。这就是我在研究问题时发现此任务的方式。目标是围绕由字典键标识的一条数据建立一个“命名动态锁”。我想我会在这里记录下来,以防其他人搜索相同的关键字。
  • 主要困难在于 GetOrAdd 调用 valueFactory 参数,即使它最终没有将结果值添加到字典中,因此需要未来或冷任务。 Lazy 包装器允许在没有副作用的情况下使用 Task.Run,​​我猜 ContinueWith 应该可以正常工作。我会尝试一下并更新答案。
  • 这是一个更新的版本,其中包含更少的函数嵌套和对 ContinueWith(Complete) 的调用。默认情况下,ContinueWith 异步运行,因此需要等待 Task.WhenAll(t, t.ContinueWith(Complete)) 的两个任务才能在原始问题的“预期输出”中实现事件序列。
【解决方案3】:

首先,ContinueWith 将返回一个新的Task,您想等到Complete 方法完成,但您正在等待第一个任务t

所以,要在end test 之前输出Complete,你必须等待第二个任务:

Task t = new Task(async () => await Get(), cancel.Token);

// NOTE: t2 is a new Task returned from ContinueWith
Task t2 = t.ContinueWith(Complete);

if (tasks.TryAdd(id, t))
{
    t.Start();
}
else
{
}

// NOTE: Waiting on t2, NOT t
t2.Wait();

Console.WriteLine("end test");

现在输出将是:

start task 
Complete
end test
end task

好的,这仍然不是预期的输出。 end task 应该在Complete 之前打印。这是因为您的异步操作不可等待:How to await on async delegate

不知道我是否正确理解了您的要求。如果是我,我可能会这样做:

添加一个新的支持类:

public class TaskEntry
{
    public Task Task { get; set; }
}

然后将代码更改为:

Guid id = Guid.NewGuid();

Task task = null;
var entry = new TaskEntry();
if (tasks.TryAdd(id, entry))
{
    entry.Task = Get();

    // Notice this line of code:
    task = entry.Task.ContinueWith(Complete);
}

if (task != null)
{
    task.Wait();
}

Console.WriteLine("end test");

这里我假设TaskEntry 不会被其他线程修改。

【讨论】:

  • 你是对的,我应该等待继续任务以确保我的预期结果。但真正的问题是异步无效委托。您额外的 TaskEntry 类是一个很好的解决方法。但你是说没有办法创建等待任务?
猜你喜欢
  • 2014-08-05
  • 1970-01-01
  • 2015-02-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-23
相关资源
最近更新 更多