多线程与 Windows 窗体






4.77/5 (22投票s)
一个 Windows 线程客户端和 MVC 服务器,
引言
我曾多次遇到需要开发一个与远程 Web 服务器交互的 Windows 应用程序的需求。该 Windows 应用程序可能涉及 Web 服务,或者只是自动化表单输入或屏幕抓取——共同点是方程的一端是基于 Web 的,因此可能支持多请求,从而允许我们更快地完成处理。本文是一个包含两部分的项目:一个多线程 Windows 客户端,以及它交互的简单 MVC 应用程序。由于并非所有远程服务都允许多连接,因此该项目提供了顺序或并行(多线程)模式的运行选项。两个项目的源代码都已附加。
背景
本文介绍的主要概念是 Windows 多线程。我采取的方法是开发人员可以采用的*众多方法之一*。
这里演示的技术包括:
- 在线程中使用异步模式的 HttpClient
- XML 消息
- 与 Windows 应用程序主操作线程交互,以线程安全的方式更新用户界面上的对象
多线程的概念如下:
- 创建线程并设置其各种属性
- 当线程完成工作时,让它调用主窗体线程中的一个工作完成方法,并根据需要更新用户。
设置
简单服务器 - 一个 MVC 应用
为了测试我们的工作,并且不触发拒绝服务警告(大量多线程可能会导致这种情况!),我们将创建一个测试平台。在这种情况下,一个简单的 MVC 应用程序就足够了。我们将创建一个控制器方法 GetXML
,该方法接收 Windows 应用程序发送的 ID,并返回 XML 响应。
GetXML
方法的调用方式为:https://:4174/home/GetXML?ItemID=23。
返回的 XML 输出如下:
<response type="response-out" timestamp="20130804132059">
<itemid>23</itemid>
<result>0</result>
</response>
注意:在本测试中,“result”为 0 = 失败,1 = 成功。
(1) 创建一个新的 MVC 应用,并添加一个新的 GetXML
控制器。我们还将添加一个小“sleep”命令来稍微减慢速度,以模拟非常繁忙的互联网的延迟。
public ContentResult GetXML()
{
// assign post parameters to variables
string ReceivedID = Request.Params["ItemID"];
// generate a random sleep time in milli-seconds
Random rnd = new Random();
// multiplier ensures we have good breaks between sleeps
// note that with Random, the upper bound is exclusive so this really means 1..5
int SleepTime = rnd.Next(1, 2) * 1000;
// generate XML string to send back
System.Threading.Thread.Sleep(SleepTime);
return Content(TestModel.GetXMLResponse(ReceivedID), "text/xml");
}
(2) 创建一个模型方法,负责构造要发送回的 XML 响应的逻辑。该方法将接收一个 ID(int
)作为参数,该 ID 代表用户正在处理的对象/查询列表的标识符。这些可以是信用卡、要抓取的网站、账号等。实际上,您可以发送任何需要处理的数据,然后将其返回到主调用应用程序。
public static string GetXMLResponse(string ItemID)
{
// generate a random result code, 0= fail, 1 = success
// note that with Random, the upper bound is exclusive so this really means 1..2
Random rnd = new Random();
string ResultCode = rnd.Next(0, 2).ToString();
string TimeStamp = GetTimeStamp();
// create an XML document to send as response
XmlDocument doc = new XmlDocument();
// add root node and some attributes
XmlNode rootNode = doc.CreateElement("response");
XmlAttribute attr = doc.CreateAttribute("type");
attr.Value = "response-out";
rootNode.Attributes.Append(attr);
attr = doc.CreateAttribute("timestamp");
attr.Value = TimeStamp;
rootNode.Attributes.Append(attr);
doc.AppendChild(rootNode);
// add child to root node sending back the item ID
XmlNode dataNode = doc.CreateElement("itemid");
dataNode.InnerText = ItemID;
rootNode.AppendChild(dataNode);
// add our random result
dataNode = doc.CreateElement("result");
dataNode.InnerText = ResultCode;
rootNode.AppendChild(dataNode);
// send back xml
return doc.OuterXml;
}
多线程客户端 - 一个 Windows 窗体应用
该客户端在视觉上相当简单。它包含两个用于输入变量的编辑框,一个列表视图用于向用户显示正在发生的情况,以及一个复选框用于告知程序是以顺序模式还是多线程模式运行。
我们将首先通过检查顺序过程来了解整体逻辑,然后研究多线程部分。
在窗体类的顶部,我们跟踪一些变量。
private int RunningThreadCount;
private int RunTimes;
private int TimeStart;
所有操作都由 RunProcess
按钮单击事件触发。
TimeStart = System.Environment.TickCount;
InitProcess();
if (chkRunThreaded.Checked)
RunProcessThreaded();
else RunProcess();
我们跟踪开始时间,并在所有进程完成后更新此时间以测试进程花费了多少时间。在此阶段,我们还调用一个 Init
方法来为我们进行设置,分配一些变量并用值填充列表视图。
// set up some default values
public void InitProcess()
{
btnExit.Enabled = false;
btnRunProcess.Enabled = false;
chkRunThreaded.Enabled = false;
RunTimes = int.Parse(edtTimesToRun.Text);
FillListView();
RunningThreadCount = 0;
}
// fill the ListView with the count items
public void FillListView()
{
lvMain.Items.Clear();
for (int i = 0; i < RunTimes; i++)
{
ListViewItem itm = new ListViewItem();
itm.Text = (i+1).ToString();
itm.SubItems.Add("Pending");
itm.SubItems.Add("-");
itm.SubItems.Add("-");
lvMain.Items.Add(itm);
}
}
现在让我们看一下顺序的 RunProcess
方法。这控制着每个 Web 请求的主要工作主体。运行进程的次数由 edtTimesToRun.Text
的值决定,该值已分配给变量 RunTimes
。
RunProcess
方法具有 async
关键字——这很重要,因为我们在 RunProcess
方法中使用 await
关键字。此代码的重要部分是 SendWebRequest
——它接受输入,查询 Web 服务器,并返回一个我们用于更新用户界面的值。
public async void RunProcess()
{
for (int i = 0; i < RunTimes; i++) {
updateStatusLabel("Processing: " + (i + 1).ToString() +
"/" + RunTimes.ToString());
lvMain.Items[i].Selected = true;
lvMain.Items[i].EnsureVisible();
lvMain.Items[i].SubItems[1].Text = "Processing...";
SimpleObj result = await Shared.SendWebRequest(
new SimpleObj()
{ ItemID = i.ToString(),
WebURL = edtTestServer.Text}
);
lvMain.Items[i].SubItems[1].Text = result.ResultCode;
if (result.ResultCode == "ERR")
lvMain.Items[i].SubItems[2].Text = result.Message;
}
CleanUp();
}
SendWebRequest
位于一个单独的shared.cs 文件中,因为它从两个不同的位置使用。shared.cs 文件还包含一个名为 SimpleObj
的简单对象。它用于在方法之间传递数据。
public class SimpleObj
{
public string WebURL; // web address to send post to
public string ResultCode; // 0 = failure, 1 = success
// Used to store the html received back from our HTTPClient request
public string XMLData;
public string Message; // What we will show back to the user as response
public string ItemID;
}
// Used to store the ListView item ID/index so
// we can update it when the thread completes
SendWebRequest
方法再次被标记为异步。稍后将对此进行解释。
在 SendWebRequest
方法中,我们设置了一个 HttpClient,并调用其 PostAsync
方法。在这里,我们告诉 HttpClient 对服务器执行“POST”操作。如果您之前做过 Web 编程,您会记得如何设置一个表单。
<form action="somedomain.com/someaction?somevalue=134" method="post">
<input type="text" id="ItemID">
<input type="submit" value="send">
</form>
这实际上就是我们在这里所做的。我们创建客户端对象,将我们要发送数据的网站 URL 与“content”(要发送的数据包)一起作为参数传入。在这种情况下,content 仅由参数 ItemID
及其值组成。
HttpResponseMessage response = await httpClient.PostAsync(rec.WebURL, content);
await
关键字指示代码在此处等待,直到 HttpClient 返回响应或引发异常。我们检查响应以确保它是有效的(response.IsSuccessStatusCode
),如果有效,我们将继续获取响应流并处理其 XML 结果。注意外部的 Try/Except 包装器——它将捕获任何 HTTP 连接错误并单独报告它们,从而允许应用程序继续顺利运行。
public static async Task<SimpleObj> SendWebRequest(SimpleObj rec)
{
SimpleObj rslt = new SimpleObj();
rslt = rec;
var httpClient = new HttpClient();
// we send the server the ItemID
StringContent content = new StringContent(rec.ItemID);
try
{
HttpResponseMessage response =
await httpClient.PostAsync(rec.WebURL, content);
if (response.IsSuccessStatusCode)
{
HttpContent stream = response.Content;
Task<string> data = stream.ReadAsStringAsync();
rslt.XMLData = data.Result.ToString();
XmlDocument doc = new XmlDocument();
doc.LoadXml(rslt.XMLData);
XmlNode resultNode = doc.SelectSingleNode("response");
string resultStatus = resultNode.InnerText;
if (resultStatus == "1")
rslt.ResultCode = "OK";
else if (resultStatus == "0")
rslt.ResultCode = "ERR";
rslt.Message = doc.InnerXml;
}
}
catch (Exception ex)
{
rslt.ResultCode = "ERR";
rslt.Message = "Connection error: " + ex.Message;
}
return rslt;
}
所以,这就是基本顺序工作流程。获取工作项列表,按顺序迭代它们,调用 Web 服务器,然后解析 XML 响应。
由于进程是顺序运行的,因此完成的总时间可能会很高。
现在让我们看看 RunProcessThread
代码,看看有什么不同。
这是我们在主窗体中的外部包装方法。
public void RunProcessThreaded()
{
updateStatusLabel("Status: threaded mode - watch thread count and list status");
lblThreadCount.Visible = true;
for (int i = 0; i < RunTimes; i++)
{
updateStatusLabel("Processing: " + (i + 1).ToString() + "/" + RunTimes.ToString());
lvMain.Items[i].Selected = true;
lvMain.Items[i].SubItems[1].Text = "Processing...";
SimpleObj rec = new SimpleObj() { ItemID = i.ToString(), WebURL = edtTestServer.Text };
CreateWorkThread(rec);
RunningThreadCount++;
UpdateThreadCount();
}
}
关键的变化是,我们不再在每次循环中顺序执行 WebRequest
任务,而是将该任务移交给 CreateWorkThread
方法,并传入所需的参数。
public void CreateWorkThread(SimpleObj rec){
ThreadWorker item = new ThreadWorker(rec);
//subscribe to be notified when result is ready
item.Completed += WorkThread_Completed;
item.DoWork();
}
这个小方法创建一个新的 ThreadWorker
对象,并告诉它在完成时调用 WorkThread_Completed
。窗体中的 WorkThread_Completed
方法仅在*窗体线程的上下文中*更新窗体 UI 并执行一些清理工作。
//handler method to run when work has completed
private void WorkThread_Completed(object sender, WorkItemCompletedEventArgs e)
{
lvMain.Items[int.Parse(e.Result.ItemID)].SubItems[1].Text = e.Result.ResultCode;
if (e.Result.ResultCode == "ERR")
lvMain.Items[int.Parse(e.Result.ItemID)].SubItems[2].Text = e.Result.Message;
RunningThreadCount--;
UpdateThreadCount();
if (RunningThreadCount == 0)
{
CleanUp();
}
}
我创建了一个单独的类/文件 ThreadWorker
来管理线程工作——这使得事物分开且整洁。
该类包含一些私有成员和一个公共事件。
class ThreadWorker
{
private AsyncOperation op; // async operation representing the work item
private SimpleObj ARec; // variable to store the request and response details
public event EventHandler<WorkItemCompletedEventArgs> Completed;
//event handler to be run when work has completed with a result
// constructor for the thread. Takes param ID index of the Listview to keep track of.
public ThreadWorker(SimpleObj Rec)
{
ARec = Rec;
}
}
您会记得,在我们主窗体中,当我们创建每个线程工作程序时,我们会设置线程对象,然后告诉它 DoWork
,所以这是线程对象的主要启动方法。
public void DoWork()
{
//get new async op object calling forms sync context
this.op = AsyncOperationManager.CreateOperation(null);
//queue work so a thread from the thread pool can pick it up and execute it
ThreadPool.QueueUserWorkItem((o) => this.PerformWork(ARec));
}
我使用 ThreadPool
对象的原因是创建线程是一项非常昂贵的操作,因此使用线程池意味着在完成后,线程可以放入池中重复使用。添加到池后,我们告诉线程启动 PreformWork
方法。此方法调用我们的共享方法 SendWebRequest
,并在完成后调用 PostCompleted
,后者由主窗体类中的 WorkThread_Completed
方法接收。
private void PostCompleted() //SimpleObj result
{
// call OnCompleted, passing in the SimpleObj result to it,
// the lambda passed into this method is invoked in the context of the form UI
op.PostOperationCompleted((o) =>
this.OnCompleted(new WorkItemCompletedEventArgs(ARec)), ARec);
}
protected virtual void OnCompleted(WorkItemCompletedEventArgs Args)
{
//raise the Completed event in the context of the form
EventHandler<WorkItemCompletedEventArgs> temp = Completed;
if (temp != null)
{
temp.Invoke(this, Args);
}
}
我们的主窗体“WorkThread_Completed
”方法会监视每个终止的传入线程,当所有线程都完成后,它会运行一些清理代码。
//handler method to run when work has completed
private void WorkThread_Completed(object sender, WorkItemCompletedEventArgs e)
{
lvMain.Items[int.Parse(e.Result.ItemID)].SubItems[1].Text = e.Result.ResultCode;
if (e.Result.ResultCode == "ERR")
lvMain.Items[int.Parse(e.Result.ItemID)].SubItems[2].Text = e.Result.Message;
RunningThreadCount--;
UpdateThreadCount();
if (RunningThreadCount == 0)
{
CleanUp();
}
}
就是这样,正如您所见,运行多线程会增加一些代码,但会大大提高性能。
正如我在本文开头所述,这只是处理多线程应用程序的一种方式。线程池有其优点和缺点,您需要权衡您的目标和细粒度需求与易用性。如果您对此领域感兴趣,还应该查看 Background worker,如果您想利用多核 CPU 的强大功能进行多线程处理,那么 Task Parallel Library 是一个很好的选择。
(附注:如果您觉得本文有用或下载了代码,请在下方给出评分让我知道!)