【问题标题】:are there any drawbacks or risks of using Parallel.Foreach with WebClient() inside my asp.net mvc web application在我的 asp.net mvc Web 应用程序中使用 Parallel.Foreach 和 WebClient() 是否有任何缺点或风险
【发布时间】:2016-06-29 00:12:43
【问题描述】:

我正在开发一个 asp.net MVC-5 web 应用程序,根据我读到的一些文章,我不应该在 web 服务器和 .net web 应用程序中使用并行方法。 现在就我而言,我需要在 foreach 中发出大约 1,500 个 WebClient() 调用,然后反序列化来自 WebClient() 调用的返回 json 对象。我在使用 Parallel.Foreach 之前的原始代码如下,大约需要 15 分钟才能完成:-

    public async Task <List<Details2>> Get()
            {       

              try
                {

                    using (WebClient wc = new WebClient()) 
                    {
                        string url = currentURL + "resources?AUTHTOKEN=" + pmtoken;
                        var json = await wc.DownloadStringTaskAsync(url);
                        resourcesinfo = JsonConvert.DeserializeObject<ResourcesInfo>(json);

                    }


                    ForEach( var c in resourcesinfo.operation.Details)
                   {

                        ResourceAccountListInfo resourceAccountListInfo = new ResourceAccountListInfo();
                        using (WebClient wc = new WebClient()) 
                        {

                            string url = currentURL + "resources/" + c.RESOURCEID + "/accounts?AUTHTOKEN=" + pmtoken;
                            string tempurl = url.Trim();



                            var json =  await wc.DownloadStringTaskAsync(tempurl);
                            resourceAccountListInfo = JsonConvert.DeserializeObject<ResourceAccountListInfo>(json);


                        }

                   if (resourceAccountListInfo.operation.Details.CUSTOMFIELD.Count > 0)
                    {
                        List<CUSTOMFIELD> customfield = resourceAccountListInfo.operation.Details.CUSTOMFIELD.Where(a =>
                                 a.CUSTOMFIELDLABEL.ToLower() == "name"
                                ).ToList();
                        if (customfield.Count == 1)
                        {
                            PMresourcesOnly.Add(resourceAccountListInfo.operation.Details);

                        }

                    }

                   }//end of foreach             

                    return PMresourcesOnly.ToList();

                }
                catch (Exception e)
                {
                }
                return new List<Details2>();
            }

现在我做了以下修改:-

  • 我将foreach 替换为Parallel.ForEach
  • 因为我不应该在Parallel.ForEach 中使用异步方法,所以我将DownloadStringTaskAsync 中的DownloadString 更改为Parallel.Foreach 中的DownloadString :-

    public async Task <List<Details2>> Get()
            {
    
    
    
                try
                {
    
                    using (WebClient wc = new WebClient()) 
                    {
                        string url = currentURL + "resources?AUTHTOKEN=" + pmtoken;
                        var json = await wc.DownloadStringTaskAsync(url);
                        resourcesinfo = JsonConvert.DeserializeObject<ResourcesInfo>(json);
    
                    }
    
    
                    Parallel.ForEach(resourcesinfo.operation.Details, new ParallelOptions { MaxDegreeOfParallelism = 7 }, (c) =>
                    {
    
                        ResourceAccountListInfo resourceAccountListInfo = new ResourceAccountListInfo();
                        using (WebClient wc = new WebClient()) 
                        {
    
                            string url = currentURL + "resources/" + c.RESOURCEID + "/accounts?AUTHTOKEN=" + pmtoken;
                            string tempurl = url.Trim();
    
    
    
                            var json =  wc.DownloadString(tempurl);
                            resourceAccountListInfo = JsonConvert.DeserializeObject<ResourceAccountListInfo>(json);
    
    
                        }
    
                    if (resourceAccountListInfo.operation.Details.CUSTOMFIELD.Count > 0)
                    {
                        List<CUSTOMFIELD> customfield = resourceAccountListInfo.operation.Details.CUSTOMFIELD.Where(a =>
                                 a.CUSTOMFIELDLABEL.ToLower() == "name"
                                ).ToList();
                        if (customfield.Count == 1)
                        {
                            PMresourcesOnly.Add(resourceAccountListInfo.operation.Details);
    
                        }
    
                    }
    
    
    
                    });//end of foreach
    
    
    
    
                return PMresourcesOnly.ToList();
    
                }
                catch (Exception e)
                {
                }
                return new List<Details2>();
            }
    

现在,当我使用 Parallel.Foreach 时,执行时间从 15 分钟减少到大约 7 分钟。但是如果我的第二种方法有效,我会有点困惑,所以任何人都可以就这些问题(或任何问题)提出建议:-

  1. 使用Parallel.ForeachWebclient() 是一种有效的方法吗?或者我应该避免在 .net 和 Web 应用程序中使用并行方法?

  2. 在使用Parallel.Foreach 时,我是否会遇到任何问题,例如return PMresourcesOnly.ToList(); 已返回给客户端,而仍有一些wc.DownloadString(tempurl); 未完成?

  3. 如果我想比较 2 种方法(Parallel.Foreach 和 Foreach),结果是否相同?

  4. 在一些在线文章中,他们使用Task.Factory.StartNew(() 而不是使用Parallel.foreach,那么它们之间的主要区别是什么?

编辑 我尝试将SemaphoreSlim 定义如下:-

public async Task <List<Details2>> Get()
{
SemaphoreSlim throttler = new SemaphoreSlim(initialCount: 15);       
  try
  {
//code goes here

var tasks = resourcesinfo.operation.Details.Select(c => TryDownloadResourceAsync(c.RESOURCEID,throttler)).ToList();
}

///---

private async Task<Details2> TryDownloadResourceAsync(string resourceId, SemaphoreSlim throttler)
        {
            await throttler.WaitAsync();
try
            {
                using (WebClient wc = new WebClient()) //get the tag , to check if there is a server with the same name & tag..
                {}
             }
 finally
            {
                throttler.Release();
            }

【问题讨论】:

  • Parallel.ForEach 是正确的方法。在您的情况下使用起来更容易。您也可以使用 Task.Factory.StartNew 但您还必须添加“等到全部完成”。这就是为什么 Parallel.ForEach 更容易使用的原因。我还将 Parallel.ForEach 与 WebClient 一起使用。如果您使用 Parallel,请注意同步您的代码。不要忘记使用锁。
  • @Stanley 您能否就“如果您使用 Parallel,那么请注意同步您的代码。不要忘记使用锁定”提供更多建议。就我而言,我在 paralle.foreach 中使用了同步方法,但是我应该在哪里放置锁以及为什么要使用锁?
  • PMresourcesOnly.Add(resourceAccountListInfo.operation.Details);必须锁定。您正在从多个线程添加到集合中。看到这个:msdn.microsoft.com/en-us/library/c5kehkcz.aspx
  • @Stanley 那么如果我向其中添加来自多个线程的集合有什么问题???不知道使用锁的想法是什么?

标签: c# asp.net .net asp.net-mvc parallel-processing


【解决方案1】:

使用 Parallel.Foreach 和 Webclient() 是一种有效的方法吗?或者我应该避免在 .net 和 Web 应用程序中使用并行方法?

不,您绝对应该避免在 ASP.NET 应用程序中使用并行方法。

在一些在线文章中,他们使用 Task.Factory.StartNew(() 而不是使用 Parallel.foreach 那么它们之间的主要区别是什么?

Parallel 用于data parallism(在数据项集合上运行相同的 CPU 绑定代码)。 StartNew 用于 dynamic task parallelism(在处理时更改的项目集合上运行相同或不同的 CPU 绑定代码)。

这两种方法都不合适,因为您要做的工作是 I/O 密集型的,而不是 CPU 密集型的。

您真正想要的是并发(一次做多件事),而不是并行。而不是使用并行并发(使用多个线程一次做多件事),你想要的是异步并发(不使用线程一次做多件事)。

可以通过await Task.WhenAll在代码中实现异步并发,例如:

private async Task<string> TryDownloadResourceAsync(string resourceId)
{
  ResourceAccountListInfo resourceAccountListInfo = new ResourceAccountListInfo();
  using (WebClient wc = new WebClient()) 
  {
    string url = currentURL + "resources/" + resourceId + "/accounts?AUTHTOKEN=" + pmtoken;
    string tempurl = url.Trim();

    var json =  await wc.DownloadStringTaskAsync(tempurl);
    resourceAccountListInfo = JsonConvert.DeserializeObject<ResourceAccountListInfo>(json);
  }

  if (resourceAccountListInfo.operation.Details.CUSTOMFIELD.Count > 0)
  {
    List<CUSTOMFIELD> customfield = resourceAccountListInfo.operation.Details.CUSTOMFIELD.Where(a =>
        a.CUSTOMFIELDLABEL.ToLower() == "name"
    ).ToList();
    if (customfield.Count == 1)
    {
      return resourceAccountListInfo.operation.Details;
    }
  }
  return null;
}

public async Task <List<Details2>> Get()
{       
  try
  {
    using (WebClient wc = new WebClient()) 
    {
      string url = currentURL + "resources?AUTHTOKEN=" + pmtoken;
      var json = await wc.DownloadStringTaskAsync(url);
      resourcesinfo = JsonConvert.DeserializeObject<ResourcesInfo>(json);
    }

    var tasks = resourcesinfo.operation.Details.Select(c => TryDownloadResourceAsync(c.RESOURCEID)).ToList();
    var results = await Task.WhenAll(tasks).Select(x => x != null);
    return results.ToList();
  }
  catch (Exception e)
  {
  }
  return new List<Details2>(); // Please, please don't do this in production.
}

作为最后一点,您可能需要查看HttpClient,它专为异步操作而设计,并且具有您只需要 一个 来进行任意数量的同时调用的好属性。

【讨论】:

  • 感谢您的宝贵回复.. 虽然我在使用 Paralle.Foreach 的正确轨道上,因为与仅使用 foreach 相比,它减少了近 50% 的执行时间.. 但现在您提到了使用 Parallel 不是正确的方法,相反我需要使用 Task.WhenAll .. 所以请你就这些问题提出建议。 1)目前“resourcesinfo.operation.Details.Select(c => TryDownloadResourceAsync(c.RESOURCEID)).ToList();”将调用 TryDownloadResourceAsync 大约 1,500 次,
  • @johnG: 1) 一种限制异步并发的方法是使用SemaphoreSlim; SO上有几个答案显示了如何。 2)异步任务不“执行”,所以他们do not use up a thread。 3) 不是真的; TPL Dataflow 为您提供了一个网格,您可以将其用作生产者/消费者队列。异步并发启动它们,然后(异步)等待它们全部完成,因此没有排队。您可以进行节流,如果您倾斜头部并眯起眼睛,这看起来有点像队列。
  • @johnG: 4) 任务数量无关紧要;它们占用的资源很少(就是一点内存)。这是因为任务不会“执行”。您可以限制(参见 (1)),这将允许您限制同时发出的 web 请求 的数量(对于 1500 个请求,服务器可能会在其中一些请求上超时)。但是您不会限制任务的数量,因为这样做没有任何好处。最后一点,您可能对my book 感兴趣,它涵盖了并行、数据流和异步,包括每个方面的限制。
  • @johnG:是的,如果你想限制请求的数量,你应该使用SemaphoreSlim
  • @johnG:不,我是说锁不是必要的。
【解决方案2】:

看看:
object syncObj = new object();
锁定(syncObj)

try
{
    using (WebClient wc = new WebClient()) 
    {
        string url = currentURL + "resources?AUTHTOKEN=" + pmtoken;
        var json = await wc.DownloadStringTaskAsync(url);
        resourcesinfo = JsonConvert.DeserializeObject<ResourcesInfo>(json);
    }

    object syncObj = new object();  // create sync object
    Parallel.ForEach(resourcesinfo.operation.Details, new ParallelOptions { MaxDegreeOfParallelism = 7 }, (c) =>
    {
        ResourceAccountListInfo resourceAccountListInfo = new ResourceAccountListInfo();
        using (WebClient wc = new WebClient()) 
        {
            string url = currentURL + "resources/" + c.RESOURCEID + "/accounts?AUTHTOKEN=" + pmtoken;
            string tempurl = url.Trim();

            var json =  wc.DownloadString(tempurl);
            resourceAccountListInfo = JsonConvert.DeserializeObject<ResourceAccountListInfo>(json);
        }

        lock(syncObj)  // lock using sync object
        {
            PMresourcesOnly.Add(resourceAccountListInfo.operation.Details);
        }
    });//end of foreach

    return PMresourcesOnly.ToList();
}
catch (Exception e)
{
}

【讨论】:

  • 我仍然不确定为什么我需要使用 lock ?根据我的测试,我上面的 2 种方法返回相同数量的对象,但顺序不同,所以我没有遇到任何损坏的数据,即使我没有使用锁……所以你能就使用锁的目的提出建议吗?
  • 再次,请先阅读:msdn.microsoft.com/en-us/library/mt679037.aspx
  • 现在基于另一个答案,建议不要使用 Parallel.foreach ,而是我应该使用 Task.whenAll 因为它更适合.net和web应用程序,你能建议吗请问这个?
  • 我还是更喜欢 Parallel.ForEach...:=)
  • 你能提供为什么你仍然更喜欢 Paralle.Foreach 而不是 Task.WhenAll
猜你喜欢
  • 2016-11-19
  • 1970-01-01
  • 2011-12-30
  • 1970-01-01
  • 2015-03-21
  • 1970-01-01
  • 1970-01-01
  • 2020-07-19
  • 2016-03-30
相关资源
最近更新 更多