1

I've done a job that does several jobs. I launch a task for each job but I have to limit the number of tasks that are running in parallel. I've followed the approach explained into this post How to limit the maximum number of parallel tasks in c-sharp.

In my example, I have to run 4 jobs, but only 2 at a time. The problem is that when first 2 jobs ended, the ObjectDisposed exception is thrown and the program ends. The goal is that it continues with the pending tasks before check the result.

Here is the piece of code to run, wait and catch the results:

Task<int>[] taskArray = new Task<int>[recorder.Channels.Count];
//recorder.Channels.Count is 4
int index = 0;
int maxTasks = 2;

using (SemaphoreSlim concurrencySemaphore = new SemaphoreSlim(maxTasks))
{
    foreach (IDataChannel channel in recorder.Channels)
    {
        if (test(channel.Name))
        {
            concurrencySemaphore.Wait();
            taskArray[index] = Task<int>.Factory.StartNew(() =>
            {
                int resTask = -1;
                try
                {
                    resTask = ProcessChannel(channel, id, filename, recorder.Name);
                }
                finally
                {
                    concurrencySemaphore.Release();
                }
                return resTask;
            });
            index++;
        }
    }
    Task.WaitAll(taskArray);
}

// Check for task results
int[] results = new int[taskArray.Length];

for (int i = 0; i < taskArray.Length; i++)
{
    results[i] = taskArray[i].Result;
    if (results[i] == -1)
    {
        res = -1;
        break;
    }
}

I think there is something that I don't understand. So if anyone could help me I would be appreciated.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Nebuchanazer
  • 121
  • 1
  • 1
  • 11
  • Could you try `Task.Run` instead of `Task.Factory.StartNew`, to see if it makes any difference? – Theodor Zoulias Feb 19 '21 at 15:05
  • " the objectDisposed exception is thrown " in which line? – Mong Zhu Feb 19 '21 at 15:20
  • can you post the `ProcessChannel` method? – Mong Zhu Feb 19 '21 at 15:21
  • Theodor Zoulias, It is the same. I've tested replacing Task.Factory.StartNew by Task.Run and there is the same result. – Nebuchanazer Feb 19 '21 at 15:38
  • Mong Zhu, I don't know where the objectDisposed exception is thrown, the program exits. I think the ProcessChannel is not relevant. Indeed, It performs an insert into a database, returns 0 if it's ok, otherwise returns -1. ProcessChannel works fine. – Nebuchanazer Feb 19 '21 at 15:40
  • "ProcessChannel is not relevant" ok that might be, but it would be really cool if you could transform you code to an example that can be copy pasted. With some examplary methods. But still be possible to reproduce you issue. My LINQPad tells me that the exception is thrown at `concurrencySemaphore.Release();` – Mong Zhu Feb 19 '21 at 15:44
  • 1
    that is a wicked algorith that you have there. You get an exception if `test` should ever return `false` because `Task.WaitAll(taskArray)` does not like if you have `null` values in the array . Does it always return true? – Mong Zhu Feb 19 '21 at 15:56
  • Is the `ObjectDisposed` exception thrown by the program itself, or it's just shown in the debugger? In other words, does this exception show up if you run the program with Ctrl+F5 (without the debugger attached)? – Theodor Zoulias Feb 20 '21 at 04:41

1 Answers1

2

Ok I try to propose a solution here. You have basically a big problem if: if (test(channel.Name)) ever returns false. Because then Task.WaitAll(taskArray); will throw an ArgumentException, because it doesn't like null values in the array.

The second is a race condition, which is basically a follow up problem under certain circumstanes. The objectDisposed exception is only thrown when you have a null in the position of the second Task in your array. If I programm test to return always true the exceptions are never thrown.

Imagine the following scenario:

  1. test returns TRUE and Task_1 is created and starts running

  2. then in the next iteration test returns FALSE and no Task is created but the loop keeps on running. So now only 3 places in the array can be filled. Basically it leaves the last value as null

  3. now in the 3rd and 4.th iteration test again returns TRUE and Task_2 and Task_3 are created and start running.

  4. When all tasks are created and the loop is over you come to the point where Task.WaitAll(taskArray); is situated and it starts waiting, it unfortunately exits with an exception immediately because there are null values in the array. So the compiler leaves the using-block! and thus disposes the semaphore!!

  5. but you have still a task running! and when this task hits the line: concurrencySemaphore.Release(); the semaphore is long gone and disposed because the test method messed your algorith up!

So I suggest to use a dynamically growing collection like :

List<Task<int>> taskArray = new List<Task<int>>(4); 

then simply add each new task to the list:

taskArray.Add(Task<int>.Factory.StartNew(() => 

In the end you transform the list to an array to satisfy the WaitAll method:

Task.WaitAll(taskArray.ToArray());

In my example the exception is now gone

Mong Zhu
  • 23,309
  • 10
  • 44
  • 76
  • Indeed, I was debugging and in taskArray where null values because recorder.Channels.Count was greater than the number of channels to process. So the exception was thrown by the Task.WaitAll instruction. I have modified the way to allocate the tasks array and now works fine. Thank you Mong Zhu. – Nebuchanazer Feb 19 '21 at 16:20
  • @NEBUC I understood the problem now. I added an explanation that is suited for your algorithm. Glad I could help. – Mong Zhu Feb 19 '21 at 16:24