gpt4 book ai didi

c# - 使用 SemaphoreSlim,在等待所有线程完成时更新 UI

转载 作者:行者123 更新时间:2023-11-30 23:07:12 30 4
gpt4 key购买 nike

我最近开始学习 C#,但在更新 UI 时遇到了运行多线程的问题。根据我目前所学,SemaphoreSlim 似乎是运行多线程同时仍控制最大并发线程数的正确方法。

场景:我想向网站(例如http://www.somesite.com/keyword)发送GET请求,并使用返回的字符串来获取大量的关键字。在运行所有线程时,在每个线程之后我想更新 UI,比如计算好结果和坏结果。到目前为止,这是我的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
static SemaphoreSlim _pool;
static readonly int maxThreads = 4;// able to go up to 100-200
static List<string> keysList = new List<string>();
static List<string> liveKeys = new List<string>();
static List<string> deadKeys = new List<string>();
static Queue<string> keysQueue = new Queue<string>();
static int liveCount = 0;
static int deadCount = 0;

public Form1()
{
InitializeComponent();
getAllKeyWordsToList();// get keywords from file
}

private void startButton_Click(object sender, EventArgs e)
{
_pool = new SemaphoreSlim(maxThreads);
foreach (string keyWord in keysList)
{
keysQueue.Enqueue(keyWord);
}


foreach (string key in keysList)
{
new Thread(Worker).Start(key);
keysQueue.Dequeue();
}

// this part is skipped when startButton is pressed
// tried with looks but I'm too stupid or too new with C#
while(keysQueue.Count() > 0)
{
// update UI
this.statusLabel.Text = "Only " + keysQueue.Count().ToString() + " keywords left";
this.liveLabel.Text = liveLabel.ToString();
this.deadLabel.Text = deadLabel.ToString();
}

// this get's updated; only this...
// while threads are still running
this.statusLabel.Text = "Finished!";
}

private void Worker(object obj)
{
var uri = "http://www.somesite.com/";
var key = obj.ToString();

using(var wc = new WebClient())
{
string result = wc.DownloadString(uri + key);

_pool.Wait();
if (result.Contains("live"))
{
// do some more work with the result
liveCount++;
}
else
{
// do some work with the result
deadCount++;
}
_pool.Release();
}
}
}
}

最佳答案

你不需要,也不应该在这里使用 SemaphoreSlim

您的代码跳过 while 循环的原因是您在到达那里时已经清空了队列。这个循环:

foreach (string key in keysList)
{
new Thread(Worker).Start(key);
keysQueue.Dequeue();
}

…为 keysList 集合中的每个元素启动一个新线程,并从队列中删除一个项目(甚至不使用该值!)。这些都不会以任何方式阻止;线程独立于循环启动,并且不影响循环的进度。所以循环完成,很可能甚至在单个线程开始运行之前,但在任何情况下几乎可以肯定在任何线程完成之前,代码继续执行 while 循环,其中 keysQueue 集合已被上面的前一个 foreach 循环清空。

由于此时队列的Count 已经为0,因此永远不会进入while 循环。条件在第一次尝试执行时已经是 false

除此之外,您的代码还有许多其他方面低于标准。最大的问题是,除了您尝试在不需要信号量的地方使用信号量之外,代码为每个事件操作分配了一个完整的线程,并且它阻塞了 UI(或者,至少, 如果代码完成了您想要的操作,则在整个处理过程继续进行时阻止 UI。

WebClient 类有一个DownloadStringAsync() 方法,可用于以“可等待”的方式异步执行每个下载操作。 “等待”意味着该方法可以在操作完成之前返回,从而允许当前线程在操作进行时继续工作(即在这种情况下,它可以处理 UI 更新,甚至可以处理用户输入(如果需要)。

通过使用此异步版本的 DownloadString() 方法,并在任务开始时跟踪任务,可以轻松编写一个直接循环来完成您想要的所有处理,将操作限制为您想要的任何最大并发操作,所有这些都不会阻塞 UI 线程。

这是我的意思的一个例子:

public partial class Form1 : Form
{
private const int _kmaxTasks = 4;
private readonly List<string> _keysList =
new List<string> { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j" };
private readonly List<string> _liveKeys = new List<string>();
private readonly List<string> _deadKeys = new List<string>();

public Form1()
{
InitializeComponent();
}

private async void button1_Click(object sender, EventArgs e)
{
// Initialize a new queue, copying all elements from _keysList to the queue
Queue<string> keysQueue = new Queue<string>(_keysList);

// List of tasks, to keep track of active tasks
//
// NOTE: value tuple syntax requires that you use the NuGet Package Manager
// to get Microsoft's "ValueTuple" package installed for your project.
List<Task<(bool, string)>> tasks = new List<Task<(bool, string)>>();

button1.Enabled = false;
_liveKeys.Clear();
_deadKeys.Clear();
_UpdateStatus(keysQueue.Count, _liveKeys.Count, _deadKeys.Count);

// Keep working until we're out of keys *and* out of tasks
while (keysQueue.Count > 0 || tasks.Count > 0)
{
// If we've got the max number of tasks running already, wait for one to
// complete. Even if we don't have the max number of tasks running, if we're
// out of keys also wait for one to complete.
if (tasks.Count >= _kmaxTasks || keysQueue.Count == 0)
{
Task<(bool Live, string Key)> completedTask = await Task.WhenAny(tasks);

tasks.Remove(completedTask);

if (completedTask.Result.Live)
{
_liveKeys.Add(completedTask.Result.Key);
}
else
{
_deadKeys.Add(completedTask.Result.Key);
}

_UpdateStatus(
keysQueue.Count + tasks.Count, _liveKeys.Count, _deadKeys.Count);
}

if (keysQueue.Count > 0)
{
tasks.Add(Worker(keysQueue.Dequeue()));
}
}

statusLabel.Text = "Finished!";
button1.Enabled = true;
}

private void _UpdateStatus(int count, int liveCount, int deadCount)
{
statusLabel.Text = $"Only {count} keywords left";
liveLabel.Text = liveCount.ToString();
deadLabel.Text = deadCount.ToString();
}

private async Task<(bool, string)> Worker(string key)
{
string uri = "http://www.somesite.com/";

using (MockWebClient wc = new MockWebClient())
{
string result = await wc.DownloadStringAsync(uri + key);

return (result.Contains("live"), key);
}
}
}

请注意,除了将代码转换为使用异步操作,当然还有正确工作之外,所有这些本身都大大简化了代码,我还删除了不必要的变量并将队列对象移动到Click 事件处理程序,仅由局部变量引用。

我还稍微重构了 Worker,恕我直言,Worker() 方法只进行下载和检查 更有意义>"live",然后让调用者根据结果进行记账。为此,我还使用了新的 C# 值元组功能,它允许我使用简化的内置语法从方法中返回一对值来表示它们。

最后,由于您的问题不包括任何特定的网络服务器或实际可用于测试代码的关键值(为了将来引用,请注意需要提供一个好的Minimal, Complete, and Verifiable code example 正确说明了您的问题),我使用了一些虚拟测试数据,并编写了一个简单的 MockWebClient,它具有相同的 DownloadStringAsync() 方法,但实现只是假装做一些工作并返回一个结果,使用随机数生成器来确定操作的持续时间和结果本身:

class MockWebClient : IDisposable
{
private static readonly TimeSpan _kminDelay = TimeSpan.FromSeconds(1);
private static readonly TimeSpan _kmaxDelay = TimeSpan.FromSeconds(5);

private static readonly Random _random = new Random();
private static readonly object _lock = new object();

private static TimeSpan _NextRandomDelay(TimeSpan min, TimeSpan max)
{
lock (_lock)
{
return TimeSpan.FromSeconds(
(max.TotalSeconds - min.TotalSeconds) * _random.NextDouble());
}
}

private static bool _NextRandomBool()
{
lock (_lock)
{
return _random.Next(2) == 1;
}
}

public async Task<string> DownloadStringAsync(string uri)
{
await Task.Delay(_NextRandomDelay(_kminDelay, _kmaxDelay));
return _NextRandomBool() ? "live" : "dead";
}

public void Dispose()
{
// do nothing...it's a mock!
}
}

显然,您的实际程序不需要该类。它的存在只是为了让您(和我)可以运行代码并查看它的工作情况,而无需处理真正的 Web 服务器。

最后,为了完整起见,这里是*.Designer.cs代码:

partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;

/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

#region Windows Form Designer generated code

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.liveLabel = new System.Windows.Forms.Label();
this.deadLabel = new System.Windows.Forms.Label();
this.statusLabel = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// button1
//
this.button1.Location = new System.Drawing.Point(13, 13);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(182, 60);
this.button1.TabIndex = 0;
this.button1.Text = "Start";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(40, 110);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(83, 32);
this.label1.TabIndex = 1;
this.label1.Text = "Live: ";
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(25, 142);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(98, 32);
this.label2.TabIndex = 1;
this.label2.Text = "Dead: ";
//
// label3
//
this.label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(12, 174);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(111, 32);
this.label3.TabIndex = 1;
this.label3.Text = "Status: ";
//
// liveLabel
//
this.liveLabel.AutoSize = true;
this.liveLabel.Location = new System.Drawing.Point(123, 110);
this.liveLabel.Name = "liveLabel";
this.liveLabel.Size = new System.Drawing.Size(0, 32);
this.liveLabel.TabIndex = 1;
//
// deadLabel
//
this.deadLabel.AutoSize = true;
this.deadLabel.Location = new System.Drawing.Point(123, 142);
this.deadLabel.Name = "deadLabel";
this.deadLabel.Size = new System.Drawing.Size(0, 32);
this.deadLabel.TabIndex = 1;
//
// statusLabel
//
this.statusLabel.AutoSize = true;
this.statusLabel.Location = new System.Drawing.Point(123, 174);
this.statusLabel.Name = "statusLabel";
this.statusLabel.Size = new System.Drawing.Size(0, 32);
this.statusLabel.TabIndex = 1;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(16F, 31F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1159, 780);
this.Controls.Add(this.label3);
this.Controls.Add(this.label2);
this.Controls.Add(this.statusLabel);
this.Controls.Add(this.deadLabel);
this.Controls.Add(this.liveLabel);
this.Controls.Add(this.label1);
this.Controls.Add(this.button1);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
this.PerformLayout();

}

#endregion

private System.Windows.Forms.Button button1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.Label liveLabel;
private System.Windows.Forms.Label deadLabel;
private System.Windows.Forms.Label statusLabel;
}

关于c# - 使用 SemaphoreSlim,在等待所有线程完成时更新 UI,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47522590/

30 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com