65.9K
CodeProject 正在变化。 阅读更多。
Home

多线程与 Windows 窗体

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (22投票s)

2013年8月4日

CPOL

7分钟阅读

viewsIcon

72115

downloadIcon

3631

一个 Windows 线程客户端和 MVC 服务器, 展示了通过 XML 交换进行交互。

引言

我曾多次遇到需要开发一个与远程 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 是一个很好的选择。

(附注:如果您觉得本文有用或下载了代码,请在下方给出评分让我知道!) 

© . All rights reserved.