20

I want to implement a class or pattern that ensures that I never execute more than one Task at a time for a certain set of operations (HTTP calls). The invocations of the Tasks can come from different threads at random times. I want to make use of the async-await pattern so that the caller can handle exceptions by wrapping the call in a try-catch.

Here's an illustration of the intended flow of execution:

enter image description here

Pseudo code from caller:

try {
    Task someTask = GetTask();
    await SomeScheduler.ThrottledRun(someTask);
} 
catch(Exception ex) { 
    // Handle exception
}

The Taskclass here might instead be an Action class depending on the solution.

Note that I when I use the word "Schedule" in this question I'm not necessarily using it with relation to the .NET Task Scheduler. I don't know the async-await library well enough to know at what angle and with what tools to approach this problem. The TaskScheduler might be relevant here, and it may not. I've read the TAP pattern document and found patterns that almost solve this problem, but not quite (the chapter on interleaving).

Nilzor
  • 18,082
  • 22
  • 100
  • 167

1 Answers1

31

There is a new ConcurrentExclusiveSchedulerPair type in .NET 4.5 (I don't remember if it was included in the Async CTP), and you can use its ExclusiveScheduler to restrict execution to one Task at a time.

Consider structuring your problem as a Dataflow. It's easy to just pass a TaskScheduler into the block options for the parts of the dataflow you want restricted.

If you don't want to (or can't) use Dataflow, you can do something similar yourself. Remember that in TAP, you always return started tasks, so you don't have the "creation" separated from the "scheduling" like you do in TPL.

You can use ConcurrentExclusiveSchedulerPair to schedule Actions (or async lambdas without return values) like this:

public static ConcurrentExclusiveSchedulerPair schedulerPair =
    new ConcurrentExclusiveSchedulerPair();
public static TaskFactory exclusiveTaskFactory =
    new TaskFactory(schedulerPair.ExclusiveScheduler);
...
public static Task RunExclusively(Action action)
{
  return exclusiveTaskFactory.StartNew(action);
}
public static Task RunExclusively(Func<Task> action)
{
  return exclusiveTaskFactory.StartNew(action).Unwrap();
}

There are a few things to note about this:

  • A single instance of ConcurrentExclusiveSchedulerPair only coordinates Tasks that are queued to its schedulers. A second instance of ConcurrentExclusiveSchedulerPair would be independent from the first, so you have to ensure the same instance is used in all parts of your system you want coordinated.
  • An async method will - by default - resume on the same TaskScheduler that started it. So this means if one async method calls another async method, the "child" method will "inherit" the parent's TaskScheduler. Any async method may opt out of continuing on its TaskScheduler by using ConfigureAwait(false) (in that case, it continues directly on the thread pool).
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    I could get your example to work when using the Action overload, but not when sending in a Func. I'd very much appreciate if you could explain why the unit test in this gist does not work, as in the second task not waiting for the first to complete: https://gist.github.com/3424332 – Nilzor Aug 22 '12 at 11:15
  • 6
    The `Func` doesn't work that way because the `ConcurrentExclusiveSchedulerPair` allows one executing task at a time, so when the `async` method `await`s, it's no longer executing. If you need to block until the *entire* `async` method is complete, you're probably better off using an [`async` lock](http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx). – Stephen Cleary Aug 22 '12 at 11:46
  • 1
    Thanks for your help. I'm starting to realize the two keywords "async" and "await" are just the tip of an iceberg that will take months to uncover – Nilzor Aug 22 '12 at 12:08
  • 5
    If you use `ExclusiveScheduler` with `async` actions, be aware that execution of several of those actions can be interleaved. So, when first action yields, second action may start executing. But the rest of the first action won't start until the second action yields or finishes. If this is not what you want, you can use a TPL Dataflow block with `MaxDegreeOfParallelism = 1`. – svick Aug 22 '12 at 16:18
  • Is there a way to make this truly single threaded? Right now it guarantees there is at max 1 thread running at any given time. But what if you wanted to guarantee that the entire program would generate no more than a single thread? – Asad Saeeduddin May 17 '15 at 02:52
  • @Asad: If you *must* use a single thread, then take one from the thread pool (or create your own) and install your own infinite processing loop on it. It's *extremely* rare to need this, though. There is no way to restrict an entire process to a single thread; every .NET process has multiple threads, even if you never create any yourself. – Stephen Cleary May 17 '15 at 14:54
  • @StephenCleary The reason I was wondering about this was because `ExclusiveScheduler` limits the degree of parallelism to 1, but not necessarily the degree of concurrency. For example, if you have a method that is not thread safe, `ExclusiveScheduler` doesn't guarantee that you won't be able to call it a second time before the continuation of the first call is processed. – Asad Saeeduddin May 17 '15 at 22:10
  • @Asad: No, the `ExclusiveScheduler` will only execute one thing at a time. – Stephen Cleary May 18 '15 at 12:05
  • @StephenCleary Right, but even so it is possible for a method to be suspended due to an await, then called again up to the await, all without more than one thing being executed at a time. Here's a snippet that hopefully better demonstrates what I mean: https://gist.github.com/masaeedu/fb1b5065b9df84004dd8. – Asad Saeeduddin May 18 '15 at 13:12
  • 1
    @Asad: Yes, that's the way task schedulers work. They schedule units of work represented by tasks. An `async` method is potentially multiple units of work, separated by `await` statements. Why don't you ask your own question, taking care to avoid the A/B problem (i.e., describe the *actual problem* you're trying to solve)? – Stephen Cleary May 18 '15 at 13:16
  • Succinct and to the point @StephenCleary as always, thank you! – John Leidegren Jun 17 '18 at 05:35