1

I'm creating a list in C# synchronously but it's slow. There's about 50 items in the list and it can take up to 20 seconds to add each item. The method to add the item can be asynchronous so I'm wondering if I can add all of them using async/await somehow.

Here's a simple example of adding 2 items to a list synchronously. How would I do it asynchronously? Is it even possible? Would you get "collisions" if you tried to add to the list at the same time?

using System.Threading;
using System.Collections.Generic;

class Program
{

    static void Main(string[] args)
    {
        List<int> lst = null;
        lst=GetList();
    }
    

    static List<int> GetList()
    {
        List<int> lst = new List<int>();

        //add to list
        lst.Add(Task1()); 
        lst.Add(Task2()); 

        //return list
        return lst;
    }

    static int Task1()
    {
        Thread.Sleep(10000);
        return 1; 
    }

    static int Task2()
    {
        Thread.Sleep(10000);
        return 2; 
    }

}

Compufreak's answer was just what I was looking for. I have a related question although I'm not sure if this is how I'm supposed to ask it. I modified Compufreak's code slightly to a way that makes more sense to me and I'm wondering if there is any actual difference in the way the code works. I took the async/await out of Task1 and Task2 and just created a Task and passed Task1 and Task2 in as parameters.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

class Program
{

    static async Task Main(string[] args)
    {
        List<int> lst = null;
        DateTime StartTime = DateTime.Now;
        lst = await GetList();
        DateTime EndTime = DateTime.Now;
        Console.WriteLine("ran tasks in " + (EndTime - StartTime).TotalSeconds + " seconds");
        Console.ReadLine();
    }


    static async Task<List<int>> GetList()
    {
        var tasks = new List<Task<int>>();

        //add task to list of tasks
        tasks.Add(new Task<int>(Task1));
        tasks.Add(new Task<int>(Task2));
        
        //start tasks
        foreach(Task<int> t in tasks)
        {
            t.Start();
        }

        //await results
        int[] results = await Task.WhenAll(tasks);

        //return list
        return results.ToList();
    }

    static int Task1()
    {
        Thread.Sleep(10000);
        return 1;
    }

    static int Task2()
    {
        Thread.Sleep(10000);
        return 2;
    }

}

Madison320
  • 247
  • 3
  • 11
  • You should look in to `async`/`await`. It was designed for scenarios like this (or other TPL type patterns like `Parallel.ForEach`) – Andy Aug 04 '20 at 17:21
  • @tnw That seems like a different scenario. In my case each "add" takes about 10 seconds to calculate the value to add. – Madison320 Aug 04 '20 at 17:26
  • @Andy Yes I tried to do it using async/await but I'm new to it and I'm not even sure it's possible in this case. – Madison320 Aug 04 '20 at 17:27
  • Take a look at the answer below. It's a really good example. Keep in mind the Async/Await pattern is a very deep rabbit hole, but, it is invaluable in todays coding standards. Better to learn it sooner than later. – Andy Aug 04 '20 at 17:28
  • Async is for IO-heavy work, not for CPU-heavy work – Hans Kesting Aug 04 '20 at 17:41
  • @HansKesting Asynchronous is for any time you need to not block the current thread until an operation finishes. Whether the long running work to do is IO or CPU bound is irrelevant to that. How you go about *making* the long running operation asynchronous varies depending on what type of actual work it is, but not *reasons for making something asynchronous*. – Servy Aug 04 '20 at 17:50
  • 1
    Although the linked question changed after my last reopen-vote I still think this is no 100% match. The [now linked answer](https://stackoverflow.com/questions/17197699/awaiting-multiple-tasks-with-different-results) is about awaiting tasks with *different result types*. This one is about awaiting tasks with *equal result types*. The difference is that you can directly use the array returned by `Task.WhenAll` in this case instead of handling the results seperately like in the linked answer. – Christoph Sonntag Aug 04 '20 at 18:07

2 Answers2

3

No, you cannot add to a list asynchronously.

You need to change your pattern here - if you have many long running tasks and want to run them asynchronously you should initiate those tasks seperately and collect/transform the results afterwards:

static async Task Main(string[] args)
{
    List<int> lst = null;
    DateTime StartTime = DateTime.Now;
    lst = await GetList();
    DateTime EndTime = DateTime.Now;
    Console.WriteLine("ran tasks in " + (EndTime - StartTime).TotalSeconds + " seconds");
 }


  static async Task<List<int>> GetList()
  {
      var tasks = new List<Task<int>>();
      tasks.Add(Task1());
      tasks.Add(Task2());
      int[] results = await Task.WhenAll(tasks);
      //return list
      return results.ToList();
  }

 static async Task<int> Task1()
 {
     await Task.Delay(10000);
     return 1;
 }

 static async Task<int> Task2()
 {
     await Task.Delay(10000);
     return 2;
 }

This will output:

ran tasks in 10,0727812 seconds

Here is a slightly modified fiddle to try it out online. I reduced the delay and added an additional output there.

Alternatively you could use a shared concurrentbag to add the results inside the tasks (adding results to lists in seperate threads might cause weird issues like null entries), but I see no good reason for this in your usecase.

P.S.: Your functions need to be async at their core, otherwise you need to use something like Parallel.ForEach. Async should usually be prefered if possible as it uses your resources more effectively.

Christoph Sonntag
  • 4,459
  • 1
  • 24
  • 49
0

The key to remaining threadsafe is to use an IProgress implementation like Progress and pass that instance to your task. That way all the threads are reporting back to a manager thread for the actual addition, but the work is being performed on seperate threads.

Before starting tasks

Progress prog = new Progress(p=>lst.Add(p));

Task function

int Tast1(IProgess prog)
{
   int status = 0;
   Sleep(1000);
   prog.Report(1)
   status = 1;
   return status;
}
yugami
  • 379
  • 1
  • 10
  • Is this easier than async/await? Is async/await even possible under my scenario? I'm trying to determine the best method before I start trying to figure out how to do it. – Madison320 Aug 04 '20 at 17:31
  • If you're only processing 1 item per thread then what you outlined would be fine. If you want multiple threads to loop on a dataset then IProgress will update the main thread. – yugami Aug 04 '20 at 17:41