处理多个HTTP请求和响应

3
我有一个程序,它会循环遍历应用程序列表。
Apps
--------
App1
App2
App3

现在,我需要为每个应用做一个HTTP请求,以获取每个应用程序构建列表的XML格式数据。
例如,以下是一个请求示例:
http://example.com/getapplist.do?appid=App1

让我得到的回应是:

<appid name="App1">
  <buildid BldName="Bld3" Status="Not Ready"></buildid> 
  <buildid BldName="Bld2" Status="Ready"></buildid>
  <buildid BldName="Bld1" Status="Ready"></buildid>
</appid>

现在我通过状态为“就绪”的高版本号来获取最高构建号,然后执行另一个 Web API 调用,如下所示:
http://example.com/getapplist.do?appid=App1&bldid=Bld2

这让我得到了一个响应,类似于:
 <buildinfo appid="App1" buildid="Bld2" value="someinfo"></build>

我将这些数据输入内部数据表中。但是,由于我有接近2000个appids,每个id都需要进行2次Web请求,所以现在这个程序需要非常长的时间才能完成(3小时)。为了解决这个问题,我尝试使用一个BackgroundWorker,如此处所述。我考虑将所有来自http响应的信息汇总到单个XML中,然后使用该XML进行进一步处理。但这会引发以下错误:

文件正在被另一个进程使用

所以我的代码看起来像这样:

if (!backgroundWorker1.IsBusy) 
{
    for(int i = 0; i < appList.Count; i++)
    { 
        BackgroundWorker bgw = new BackgroundWorker();
        bgw.WorkerReportsProgress = true;  
        bgw.WorkerSupportsCancellation = true;                     
        bgw.DoWork += new DoWorkEventHandler(bgw_DoWork);                   
        bgw.ProgressChanged += new ProgressChangedEventHandler(bgw_ProgressChanged);
        bgw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgw_RunWorkerCompleted);
        //Start The Worker 
        bgw.RunWorkerAsync();
    }
}

DoWork函数会提取标记的值并将其放到XML中。

我该如何最好地从所有后台工作进程的HTTP响应中获取app-buildinfo详细信息,并将其放入一个公共文件中?


1
这会生成2000个后台工作线程...不好...相反,应该从一个后台工作线程异步调用您的WebAPI...并在其中一个异步Web请求完成事件触发并写入XML文件时使用锁定。 - rene
请问您能否添加您的 DoWork 方法代码? - Yuval Itzchakov
@rene,那我需要将限制设置为多少呢?另外,如果我设置了5个后台工作线程的限制,这是否意味着线程会并行运行,直到处理完所有2K个URL? - mhn
1
尝试使用Parallel.ForParallel.ForEach,这两个方法更简单,而且不必担心线程过多导致性能下降的问题。 - bansi
@bansi 没有理由在IO绑定的工作中使用Parallel.ForEach - Yuval Itzchakov
显示剩余2条评论
2个回答

4

HTTP请求本质上是IO绑定和异步的,因此没有必要使用后台工作进程来完成您所需的操作。

您可以利用async-await,它在.NET 4中通过Microsoft.Bcl.AsyncHttpClient兼容:

private async Task ProcessAppsAsync(List<string> appList)
{
    var httpClient = new HttpClient();

    // This will execute your IO requests concurrently,
    // no need for extra threads.
    var appListTasks = appList.Select(app => httpClient.GetAsync(app.Url)).ToList();

    // Wait asynchronously for all of them to finish
    await Task.WhenAll(appListTasks);

   // process each Task.Result and aggregate them to an xml
    using (var streamWriter = new StreamWriter(@"PathToFile")
    {
        foreach (var appList in appListTasks)
        {
           await streamWriter.WriteAsync(appList.Result);
        }
    }
}

这样,您可以同时处理所有请求,并在它们全部完成后处理结果。

@rene 在Task.WhenAll之后,如果需要同步编写代码,OP可以这样做,尽管我看不出他需要这么做的原因。 - Yuval Itzchakov
@mhn 如果需要的话,我已经使用 StreamWriter 将代码异步写入文件。 - Yuval Itzchakov
尽管我按照你的建议添加了Microsoft.Bcl.Async和HttpClient,但我仍然遇到了“找不到类型或命名空间async”的错误。你有什么指针吗? - mhn
我偶然发现了https://dev59.com/FWIk5IYBdhLWcg3wRcUo#19421907。但是我没有安装VS 2012 :( - mhn
你可以下载免费的VS2012 Express。 - Yuval Itzchakov
我可以在我的本地机器上尝试这个。但是工作代码将成为另一台服务器机器上的SSIS脚本组件中的一步,而我没有安装权限。那么有什么解决方法吗? - mhn

0

这个解决方案适用于 .Net 2.0 及以上版本,它使用 WebClient 类的异步方法,并使用一个计数器来递减 Interlocked 类和普通的 lock 来序列化将结果写入文件。

var writer = XmlWriter.Create(
    new FileStream("api.xml",
                    FileMode.Create));
writer.WriteStartElement("apps"); // root element in the xml
// lock for one write
object writeLock = new object(); 
// this many calls            
int counter = appList.Count;

foreach (var app in appList)
{
    var wc = new WebClient();

    var url = String.Format(
        "http://example.com/getapplist.do?appid={0}&bldid=Bld2", 
        app);
    wc.DownloadDataCompleted += (o, args) =>
        {
            try
            {
                var xd = new XmlDocument();
                xd.LoadXml(Encoding.UTF8.GetString(args.Result));
                lock (writeLock)
                {
                    xd.WriteContentTo(writer);
                }
            }
            finally
            {
                // count down our counter in a thread safe manner
                if (Interlocked.Decrement(ref counter) == 0)
                {
                    // this was the last one, close nicely
                    writer.WriteEndElement();
                    writer.Close();
                    ((IDisposable) writer).Dispose();
                }
            }
        };
    wc.DownloadDataAsync(
        new Uri(url));   
}

你真的认为频繁同步写入文件比聚合结果后一次性写入更有益,从而完全避免锁竞争吗? - Yuval Itzchakov
我对这种情况不太确定,但如果结果足够大,内存可能会成为一个问题。或者在失败的情况下,如果重新运行代价昂贵,您可以拥有中间结果(但这将需要重新启动逻辑)。我更关心的是打开那么多网络连接的可能性。 - rene
他始终可以根据需要限制请求量。他还可以使用“Task.WhenAny”在请求完成后进行处理。 - Yuval Itzchakov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接