2

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?

13
  • 3
    "What's the problem." - you're using a non-thread-safe collection from multiple threads. Anything could happen. Commented Oct 13, 2021 at 7:06
  • 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? Commented Oct 13, 2021 at 7: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. Commented Oct 13, 2021 at 7: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 learn.microsoft.com/en-us/dotnet/standard/collections/… or learn.microsoft.com/en-us/dotnet/csharp/language-reference/… Commented Oct 13, 2021 at 7:29
  • 1
    See X/Y 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. Commented Oct 13, 2021 at 7:45

1 Answer 1

2

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();
    }
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you. they only provide me api. so can not use hub. thank you!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.