0

I am using the code below for creating multiple tasks in C#:

private static List<Task> _taskList = new List<Task>();
private static ConcurrentQueue<string> cred = new ConcurrentQueue<string>();

private static void TaskMethod1(string usercred)
{
    // I am doing a bunch of operations here, all of them can be replaced with
    // a sleep for 25 minutes.
    // After all operations are done, enqueue again.
    cred.Enqueue("usercred")
}

private static void TaskMethod()
{
    while(runningService)
    {
        string usercred;
        // This will create more than one task in parallel to run,
        // and each task can take up to 30 minutes to finish.
        while(cred.TryDequeue(out usercred))
        {
            _taskList.Add(Task.Run(() => TaskMethod1(usercred)));
        }
    }
}

internal static void Start()
{
    runningService = true;
    cred.enqueue("user1");
    cred.enqueue("user2");
    cred.enqueue("user3");
    Task1 = Task.Run(() => TaskMethod());
}

I am encountering a strange behaviour in the code above. By putting a breakpoint at line _taskList.Add(Task.Run(() => TaskMethod1(usercred)));, I am checking value of usercred every time TaskMethod1 is called and it is not null while being called but in one of the cases the value of usercred is null inside TaskMethod1. I have no clue how this could be happening.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
sporty_1993
  • 133
  • 10
  • I suspect that you got [captured variable problem](https://stackoverflow.com/q/271440/12833205). Please, add code that shows all usages of the variable *arg1* inside method *TaskMethod*. – Iliar Turdushev Apr 25 '20 at 06:34
  • Edited with the code that shows the usage of the variable inside TaskMethod. – sporty_1993 Apr 25 '20 at 07:27
  • 4
    Can you please change `while(cred.TryDequeue(out usercred)) { _taskList.Add(Task.Run(() => TaskMethod1(usercred))); }` to `while(cred.TryDequeue(out usercred)) { var uc = usercred; _taskList.Add(Task.Run(() => TaskMethod1(uc))); }` and let us know if the problem goes away? – Enigmativity Apr 25 '20 at 08:44
  • Is this a problem that only occurs when the debugger is attached, and goes away when you run your program with Ctrl+F5 (without debugging)? I am asking because you mentioned a breakpoint, and the debugger in not 100% trustworthy with multithreaded code. – Theodor Zoulias Apr 25 '20 at 08:55
  • Also are you aware that the `TaskMethod` method creates a tight loop? – Theodor Zoulias Apr 25 '20 at 09:23
  • Sorry, @TheodorZoulias but what do you mean by tight loop? – sporty_1993 Apr 25 '20 at 09:38
  • The `TryDequeue` will return `false` most of the time, so the thread will keep spinning non-stop, in essentially a `while(runningService) {}` loop. – Theodor Zoulias Apr 25 '20 at 09:50
  • yeah, there are conditions to not let that happen, I have put. But this can not the reason for the problem I am facing right, or can it be? – sporty_1993 Apr 25 '20 at 09:56
  • No, that's certainly unrelated. But I had to mention it because there are cheaper ways to produce heat than using spinning CPU cores. :-) – Theodor Zoulias Apr 25 '20 at 10:08
  • @Enigmativity, it seems to have worked what you suggested but I am still not sure of the reason, Can you please explain why it? (I am stress testing it but it passed the basic testing.) – sporty_1993 Apr 25 '20 at 15:56

3 Answers3

1

You are using Task.Run where in you are using variables from while loop. You are not passing it to the task. So, by the time task executes, its value gets changed.

You should use

while (runningService)
{
    string usercred;
    // This will create more than one task in parallel to run,
    // and each task can take upto 30 minutes to finish.
    while (cred.TryDequeue(out usercred))
    {
        _taskList.Add(Task.Factory.StartNew((data) => TaskMethod1(data.ToString()), usercred)
    }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
ajay gandhi
  • 545
  • 4
  • 13
  • 1
    Using the `Task.Factory.StartNew` method instead of `Task.Run` for the reason of passing an argument [is considered](https://devblogs.microsoft.com/pfxteam/task-run-vs-task-factory-startnew/) a performance optimization technique (avoids closures for performance-sensitive code paths). For normal application code the `Task.Run` is preferable because it's more readable. – Theodor Zoulias Apr 25 '20 at 09:01
1

You should declare the usercred variable inside the inner while loop, so that the lambda inside the Task.Run captures a separate variable for each loop, and not the same variable for all loops.

while(runningService)
{
    while(cred.TryDequeue(out var usercred))
    {
        _taskList.Add(Task.Run(() => TaskMethod1(usercred)));
    }
}

As a side note, I would consider using a BlockingCollection instead of the ConcurrentQueue, to have a way to block the current thread until an item is available, so that I don't have to worry about creating inadvertently a tight loop.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
0

The following change solved the problem.

private static void TaskMethod()
{
    while(runningService)
    {
        string usercred;
        // This will create more than one task in parallel to run,
        // and each task can take up to 30 minutes to finish.
        while(cred.TryDequeue(out usercred))
        {
            var uc = usercred;
            _taskList.Add(Task.Run(() => TaskMethod1(uc)));
        }
    }
}
sporty_1993
  • 133
  • 10
  • 2
    Both this an Theodor's answer solves the problem. Your code was re-using the same reference to `usercred` so if the task did not start fast enough the next `cred.TryDequeue(out usercred)` would overwrite the value of `usercred`. What made it `null` is still a mystery to me. But I was curious if my suggestion would work and it did. – Enigmativity Apr 26 '20 at 04:31