创造一个等待冷酷的任务

时间:2015-04-23 13:35:50

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

我完成后有一个异步方法,我希望运行另一个方法。如果我只是调用方法并添加.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上立即启动或完成?

3 个答案:

答案 0 :(得分:8)

你的代表是异步无效的。 async void-methods是火不会忘记的。

参见模式和反模式摘要的第一点: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");
  }
}

答案 1 :(得分:0)

  

但是,我有一个新的要求,那就是只有在能够将其添加到并发字典中时才开始执行任务。

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

  • Task<T>中包装Lazy<>
  • 通过ConcurrentDictionary将惰性任务添加到GetOrAdd
  • 使用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工厂仅被调用一次
  • valueFactory中使用GetOrAdd参数可以减少所创建的惰性对象的数量(3个惰性包装器是由于4个并发调用GetOrAdd而产生的)

答案 2 :(得分:-1)

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

因此,要在Complete之前输出end test,您必须等待第二项任务:

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不会被其他线程修改。