Home > Software design >  List.Add not added perfectly with Parallel and async
List.Add not added perfectly with Parallel and async

Time:10-13

I have a Blazor WASM application that needs to call an API every second without blocking the UI. This codes demonstrates how I tried to do that:

List<int> testList = new();
testList.Add(1);
testList.Add(2);
testList.Add(3);
testList.Add(4);

List<int> emptyTestlist = new();

CancellationTokenSource cts;

Test();

void Test()
{
    Parallel.Invoke(async () =>
    {
        do
        {
            Console.WriteLine("Start");
            await Task.Delay(1000);
            await Test2();
            Console.WriteLine("END");
        } while (true);
    });
}
Console.ReadLine();

async ValueTask Test2()
{
    emptyTestlist.Clear();
    cts = new();
    await Parallel.ForEachAsync(testList, cts.Token, async (test, token) =>
    {
        await Test4(test);
    });
    foreach (var test in emptyTestlist)
    {
        await Test3(test);
    }
}

async Task Test4(int i)
{
    await Task.Delay(300);
    //Console.WriteLine("if I Add this console.WriteLine It's added perfectly");
    emptyTestlist.Add(i);
    Console.WriteLine($"from TEST4: {i}");
}

async Task Test3(int i)
{
    Console.WriteLine($"TEST3 {i}.");
    await Task.Delay(1000);
    Console.WriteLine($"TEST3 {i}, after 1sec");
}

If I comment the line Console.WriteLine("if I Add this console.WriteLine It's added perfectly");, it's not adding perfectly. (emptyTestlist.Count is not always 4). But if I add Console.WriteLine before emptyTestlist.Add(i) it works correctly (emptyTestlist.Count is always 4).

I don't know how to solve it. What's the problem?

CodePudding user response:

The easiest way to poll an API is to use a timer:

@code {
    private List<Customer> custs=new List<Customer>();
    
    private System.Threading.Timer timer;

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        custs = await Http.GetFromJsonAsync<List<Customer>>(url);

        timer = new System.Threading.Timer(async _ =>
        {
            custs = await Http.GetFromJsonAsync<List<Customer>>("/api/customers");
            InvokeAsync(StateHasChanged); 
        }, null, 1000, 1000);
    }

In this case InvokeAsync(StateHasChanged); is needed because the state was modified from a timer thread and Blazor has no idea the data changed.

If we wanted to add the results to a list though, we'd either have to use a lock or a thread-safe collection, like a ConcurrentQueue.

@code {
    private ConcurrentQueue<Customer> custs=new ConcurrentQueue<Customer>();
    
    private System.Threading.Timer timer;

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        custs = await Http.GetFromJsonAsync<List<Customer>>(url);

        timer = new System.Threading.Timer(async _ =>
        {
            var results = await Http.GetFromJsonAsync<List<Customer>>("/api/customers");
            foreach(var c in results)
            {
                custs.Enqueue(c);
            }
            InvokeAsync(StateHasChanged); 
        }, null, 1000, 1000);
    }

Polling an API every second just in case there's any new data isn't very efficient though. It would be better to have the API notify clients of any new data using eg SignalR or Push Notifications

Borrowing from the documentation example this would be enough to receive messages from the server:

@code {
    private HubConnection hubConnection;
    private List<string> messages = new List<string>();
    private string userInput;
    private string messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            StateHasChanged();
        });

        await hubConnection.StartAsync();
    }
  • Related