简体   繁体   中英

Run multiple tasks with continuation

I found some strange behavior running tasks within a for loop and awaiting them all. As mentioned below, what the code does is starting a "loop" for definite number of tasks, each task creates a "ToDo" item and each task has a continuation that assigns each created task to a person and finally but them in a listBox using Invoke so the UI thread is called. this works fine, but i do not get the expected data in the listBox. I do not expect them to be ordered, but i do expect them to be paired, eg :

Person_8, Todo 8 Person 5, Todo 5 etc...

And they should only appear ones in the listBox of course ! But instead, i get strange output (and output is never the same for each run), here are some examples running the code:

enter image description here enter image description here

And here is the code:

private async void buttonCreateToDo_Click(object sender, EventArgs e){
    await CreateToDoAsync();
}

private async Task CreateToDoAsync(){
    List<Task> taskList = new List<Task>();
    for (int i = 1; i < 10; i++){
        var task = Task.Run(() => CreateToDo(i));
        Task continuation = task.ContinueWith((antecedent) => Invoke(new AssignTaskDelegate(AssignTask), (new Person() {
            Name = $"Person_{i}",
            ToDoForPerson = antecedent.Result
        })));
        taskList.Add(task);

    }
    await Task.WhenAll(taskList.ToArray());
}

private ToDo CreateToDo(int toDoId) {
    return new ToDo(){
        Id = toDoId,
        Description = $"Todo {toDoId}"
    };
}

private void AssignTask(Person person){
    listBoxToDo.Items.Add($"{person.Name}, {person.ToDoForPerson.Description}");
}

Your issue is that for loop runs much faster than the creation of the tasks and so by the time the tasks run the variable i has reached the end of the loop.

To fix this you need to take a copy of i inside the loop and use that.

Try this code:

private async Task CreateToDoAsync()
{
    List<Task> taskList = new List<Task>();
    for (int i = 1; i < 10; i++)
    {
        var local_i = i;
        var task = Task.Run(() => CreateToDo(local_i));
        Task continuation = task.ContinueWith((antecedent) => Invoke(new AssignTaskDelegate(AssignTask), (new Person()
        {
            Name = $"Person_{local_i}",
            ToDoForPerson = antecedent.Result
        })));
        taskList.Add(task);
    }
    await Task.WhenAll(taskList.ToArray());
}

Now, in preference I'd use Microsoft's Reactive Framework (NuGet "System.Reactive") to do this work. Your code would look like this:

private async Task CreateToDoAsync()
{
    var query =
        from i in Observable.Range(1, 9)
        from t in Observable.Start(() => CreateToDo(i))
        select new Person() { Name = $"Person_{i}", ToDoForPerson = t };

    await query.ObserveOn(listBoxToDo).Do(x => AssignTask(x));
}

Done. That's it.

When I run my code (with the AssignTask outputting to the Console) I get this:

Person_1, Todo 1
Person_2, Todo 2
Person_3, Todo 3
Person_6, Todo 6
Person_7, Todo 7
Person_4, Todo 4
Person_5, Todo 5
Person_8, Todo 8
Person_9, Todo 9

The .ObserveOn(listBoxToDo) works for Windows Forms to marshall back to the UI thread.

You are using variable "i" from the loop inside the task. "i" might have changed while your task started.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM