1

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?

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • 3
    "What's the problem." - you're using a non-thread-safe collection from multiple threads. Anything could happen. – Damien_The_Unbeliever Oct 13 '21 at 07:06
  • thank you. do you know how to solve it? – otter otter Oct 13 '21 at 07:14
  • 2
    @otterotter what are you trying to do in the first place? This code is using unsuitable classes and methods. `Parallel.Invoke` is used as if it was `Task.Run` for starters or worse, a `Thread.Start`. Tasks aren't threads, they *use* threads. Why not use a plain old timer? – Panagiotis Kanavos Oct 13 '21 at 07:20
  • 1
    @otterotter to avoid threading issues you need to either use locks or thread-safe collections like ConcurrentQueue or ConcurrentDictionary. There's no ConcurrentList. If you want a pub/sub collection though, you can use Channel. – Panagiotis Kanavos Oct 13 '21 at 07:22
  • 1
    The problem with these small pseudo examples is that it doesn't represent the actual problem you are trying to solve, therefor an actual robust best practice solution cant be tendered other than to say, you need to make this thread safe with either a lock or thread safe collection https://learn.microsoft.com/en-us/dotnet/standard/collections/thread-safe/ or https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statement – TheGeneral Oct 13 '21 at 07:29
  • thank you. in blazor I using `do while` in viewModel . so I have to use parallel for UI not stopping. is there more good solution? – otter otter Oct 13 '21 at 07:34
  • @otterotter using any of these in Blazor WASM is a major bug - a browser tab has only a single thread. And Blazor works with components, not ViewModels. It's essentially React#, not Razor Pages WASM. What are you actually trying to do? – Panagiotis Kanavos Oct 13 '21 at 07:44
  • 1
    See [X/Y](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) problem. Describe the actual goal you are trying to accomplish. Also find some resources about thread safety, because using unsafe collections is only one of many possible hazards for multi threaded programs. – JonasH Oct 13 '21 at 07:45
  • @otterotter `so I have to use parallel for UI not stopping.` no, you need to use async operations and *avoid* blocking. In Blazor Server you're essentially running on the server so there's no UI thread to block (although there *is* a sync context). Any changes to the UI are sent to the browser using SignalR. In Blazor WASM you're running on the browser - why would any SPA want to use multiple threads? It's not meant to crunch numbers. When they talk to APIs they always have to use async methods – Panagiotis Kanavos Oct 13 '21 at 07:46
  • @PanagiotisKanavos I'm using Blazor wasm. also can use do while with async and not blocking UI? I have to call API every second. Thank you for your answer. you really saved me. thank you JonasH too. thank you – otter otter Oct 13 '21 at 07:50
  • @otterotter describe your actual problem in the question itself. As for calling an API every 1 second, a System.Threading.Timer enough. What do you want to do with the results though? Is that what the list is about, to buffer responses for processing? You can use a Channel for this. A better solution though would be to have the service send notifications to the SPA with SignalR or notifications. This way, instead of hitting a service only to get the same answer you'd get updates immediately without bothering the server – Panagiotis Kanavos Oct 13 '21 at 07:56
  • @otterotter as a side note, the `Parallel.Invoke` method, as well as the `Parallel.ForEach`, [are not async-friendly](https://stackoverflow.com/questions/23137393/parallel-foreach-and-async-await). If you pass an async delegate as argument, the delegate is [async void](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void). The `Parallel.ForEachAsync` is OK though (it is designed specifically for working with async delegates). – Theodor Zoulias Oct 13 '21 at 08:01
  • @otterotter please explain the actual problem in the question itself to avoid people closing for the wrong reason – Panagiotis Kanavos Oct 13 '21 at 08:08

1 Answers1

1

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();
    }
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236