List.Add 没有与 Parallel 和 async 完美添加

Posted

技术标签:

【中文标题】List.Add 没有与 Parallel 和 async 完美添加【英文标题】:List.Add not added perfectly with Parallel and async 【发布时间】:2021-10-13 06:58:07 【问题描述】:

我有一个 Blazor WASM 应用程序,它需要每秒调用一次 API 而不会阻塞 UI。这段代码演示了我是如何尝试这样做的:

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");

如果我评论Console.WriteLine("if I Add this console.WriteLine It's added perfectly"); 行,它的添加并不完美。 (emptyTestlist.Count 并不总是 4)。但如果我在emptyTestlist.Add(i) 之前添加Console.WriteLine,它就可以正常工作(emptyTestlist.Count 始终为 4)。

我不知道如何解决它。有什么问题?

【问题讨论】:

“有什么问题。” - 您正在使用来自多个线程的非线程安全集合。任何事情都有可能发生。 @otterotter 你一开始想做什么?此代码使用了不合适的类和方法。 Parallel.Invoke 的使用就像是 Task.Run 对于初学者或更糟的是 Thread.Start。任务不是线程,它们使用线程。为什么不使用普通的旧计时器? @otterotter 要避免线程问题,您需要使用锁或线程安全集合,如 ConcurrentQueue 或 ConcurrentDictionary。没有并发列表。如果你想要一个发布/订阅集合,你可以使用 Channel。 这些小伪示例的问题在于,它并不代表您要解决的实际问题,因此除了说,您需要做此线程安全,带有锁或线程安全集合 docs.microsoft.com/en-us/dotnet/standard/collections/… 或 docs.microsoft.com/en-us/dotnet/csharp/language-reference/… 见X/Y 问题。描述你正在努力实现的实际目标。还可以找到一些关于线程安全的资源,因为使用不安全的集合只是多线程程序的许多可能危害之一。 【参考方案1】:

轮询 API 的最简单方法是使用计时器:

@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);
    

在这种情况下需要InvokeAsync(StateHasChanged);,因为状态是从计时器线程修改的,而 Blazor 不知道数据已更改。

如果我们想将结果添加到列表中,我们要么必须使用锁,要么必须使用线程安全集合,例如 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);
    

每秒轮询一个 API 以防万一有任何新数据,但效率不是很高。最好让 API 使用 SignalR 或 Push Notifications 通知客户端任何新数据

从documentation example借用这足以接收来自服务器的消息:

@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();
    

【讨论】:

谢谢。他们只为我提供 api。所以不能使用集线器。谢谢!

以上是关于List.Add 没有与 Parallel 和 async 完美添加的主要内容,如果未能解决你的问题,请参考以下文章

字符串与List互转

数组与List之间的转换

线程安全ConcurrentBag

C#并行编程--命令式数据并行(Parallel.Invoke)---与匿名函数一起理解(转载整理)

List的遍历和删除元素

java ListIterator接口