4

I'm using Asp .Net 4.5.1. I have tasks to run, which call a web-service, and some might fail. I need to run N successful tasks which perform some light CPU work and mainly call a web service, then stop, and I want to throttle.

For example, let's assume we have 300 URLs in some collection. We need to run a function called Task<bool> CheckUrlAsync(url) on each of them, with throttling, meaning, for example, having only 5 run "at the same time" (in other words, have maximum 5 connections used at any given time). Also, we only want to perform N (assume 100) successful operations, and then stop.

I've read this and this and still I'm not sure what would be the correct way to do it.

How would you do it?

  1. Assume ASP .Net
  2. Assume IO call (http call to web serice), no heavy CPU operations.
H.Wolper
  • 699
  • 1
  • 13
  • 26
  • How about splitting this up in 5 arrays, spawning 5 threads and doing `CheckUrlSync(url)`? (Notice the **Sync**, not async.) Then share a "int CompletedTasks" between the threads to halt them. – Automatico Jul 20 '14 at 09:51
  • @Cort3z that is clearly not the most efficient way to do this, not to mention you need to write code to have the 5 threads pull work off the work queue thread safe. Not to mention the 40MB of ram you use to instantiate the threads. – Aron Jul 20 '14 at 10:04
  • Well, you can use thread-safe collections, however as I tried to state: You split the work into 5 in the beginning. Another solution would of course to use thread-pool, but it would be more fiddling with getting the right number of tasks to run at the same time. And if you are going to do stuff async anyways, you will need the threads, unless it is IO, which it might be in this case. – Automatico Jul 20 '14 at 10:11
  • @Cort3z .net (most frameworks in fact) has already got a class for limiting the number of times you enter a piece of code, its call a Semaphore. So you just need to use a the Task aware version `SemaphoreSlim`. – Aron Jul 20 '14 at 10:15

2 Answers2

6

Use Semaphore slim.

var semaphore = new SemaphoreSlim(5);
var tasks = urlCollection.Select(async url => 
{
    await semaphore.WaitAsync();
    try
    {
        return await CheckUrlAsync(url);
    } 
    finally 
    {
        semaphore.Release();
    }
};
while(tasks.Where(t => t.Completed).Count() < 100)
{
    await.Task.WhenAny(tasks);
}

Although I would prefer to use Rx.Net to produce some better code.

using(var semaphore = new SemaphoreSlim(5))
{
    var results = urlCollection.ToObservable()
              .Select(async url => 
    {
        await semaphore.WaitAsync();
        try
        {
            return await CheckUrlAsync(url);
        } 
        finally 
        {
            semaphore.Release();
        }
    }).Take(100).ToList();
}

Okay...this is going to be fun.

public static class SemaphoreHelper
{
    public static Task<T> ContinueWith<T>(
         this SemaphoreSlim semaphore, 
         Func<Task<T>> action)
    var ret = semaphore.WaitAsync()
                   .ContinueWith(action);
    ret.ContinueWith(_ => semaphore.Release(), TaskContinuationOptions.None);
    return ret;
}
var semaphore = new SemaphoreSlim(5);
var results = urlCollection.Select(
              url => semaphore.ContinueWith(() => CheckUrlAsync(url)).ToList();

I do need to add that the code as it stands will still run all 300 URLs, it just will return quicker...thats all. You would need to add the cancelation token to the semaphore.WaitAsync(token) to cancel the queued work. Again I suggest using Rx.Net for that. Its just easier to use Rx.Net to get the cancelation token to work with .Take(100).

Aron
  • 15,464
  • 3
  • 31
  • 64
  • could you do that without Rx and without async/await (i understand this is NOT in the context of the question, but say i was on .Net 4.0 and can't use backports of Bcl). – zaitsman Jul 20 '14 at 10:25
  • Rx.Net can be used in .net 4...but let me give it a shot...Oh...there isn't SemaphoreSlim in 4.0 – Aron Jul 20 '14 at 10:26
  • this: http://msdn.microsoft.com/en-us/library/system.threading.semaphoreslim(v=vs.100).aspx says there is? and i know Rx can be used with .net 4, my main question is how'd you work around the lack of async/await keywords. – zaitsman Jul 20 '14 at 10:32
  • Thanks, @Aron . You mentioned that the code will still run for all URLs, and as I mentioned, I have to stop at N (100 in our case) *without* running the rest of the URLs. That means I have to start by adding tasks up to 100 and then, with a WaitAny, add more, only the success count is less than 100. Can you update your answer accordingly? – H.Wolper Jul 20 '14 at 11:17
-1

Try something like this?

private const int u_limit = 100;
private const int c_limit = 5;

List<Task> tasks = new List<Task>();

int totalRun = 0;

while (totalRun < u_limit)
{
    for (int i = 0; i < c_limit; i++)
    {
      tasks.Add(Task.Run (() => {
      // Your code here.
      }));
    }
  Task.WaitAll(tasks);
  totalRun += c_limit;
}
zaitsman
  • 8,984
  • 6
  • 47
  • 79
  • -1 Code is clearly not threadsafe. Use Interlocked.Increment if you want to do what you are trying to do. Also your total run isn't volitile. – Aron Jul 20 '14 at 09:57
  • @Aron where did the question say that the overall process would be run on more than one thread? This code will run fine if invoked from one thread only. – zaitsman Jul 20 '14 at 10:07
  • Task.Run is threaded. You do not make best usage of each connection, as if a single call took a lot longer it would hold up the entire batch. Its really bad to use threads in ASP.Net. – Aron Jul 20 '14 at 10:11