【问题标题】:Creating and Starting Tasks inside a Foreach在 Foreach 中创建和启动任务
【发布时间】:2016-01-05 00:54:31
【问题描述】:

我试图从网站上抓取一些数据。这是我的课:

class ClosureCraziness
{
    public string SaveFolder { get; set; }

    public void Save(Dictionary<string, string> idToWebLocation)
    {
        var tasks = new List<Task>();
        foreach (var kvp in idToWebLocation)
        {
            var task = new Task(() => Download(kvp.Key, kvp.Value));
            task.Start();
            tasks.Add(task);
        }

        Task.WaitAll(tasks.ToArray());
    }

    void Download(string id, string location)
    {
        var filename = $"{id}.html";
        string source = string.Empty;
        try
        {
            source = GetSource(location);
        }
        catch (Exception e)
        {
            // handle exception
        }

        var path = Path.Combine(SaveFolder, filename);
        using (var sw = new StreamWriter(path))
            sw.Write(source);
    }

    string GetSource(string location)
    {
        using (var client = new WebClient())
        {
            return client.DownloadString(location);
        }
    }
}

当我执行时,我会得到如下内容。您会注意到文件的内容(下载的源)与名称不匹配:

磁盘上的文件名 | File Contents

apple.html &lt;html&gt; apple &lt;/html&gt;

orange.html &lt;html&gt; orange &lt;/html&gt;

pear.html &lt;html&gt; peach &lt;/html&gt;

peach.html &lt;html&gt; peach &lt;/html&gt;

grape.html &lt;html&gt; apple &lt;/html&gt;

plum.html &lt;html&gt; plum &lt;/html&gt;

(我不知道如何很好地格式化)

起初我很困惑,因为磁盘上的文件名是正确的,我确定我的Dictionary&lt;string, string&gt; 格式正确(我检查了 6 次,所有不同的方式),这意味着 Id 与 web 位置的关联很好.

我想这可能是一个关闭问题,回忆了Eric Lippert schooling me on the implementation of foreach。所以我尝试了:

public void Save(Dictionary<string, string> idToWebLocation)
{
    var tasks = new List<Task>();
    foreach (var kvp in idToWebLocation)
    {
        var innerKvp = kvp;
        var task = new Task(() => Download(innerKvp.Key, innerKvp.Value));
        task.Start();
        tasks.Add(task);
    }

    Task.WaitAll(tasks.ToArray());
}

而且,为了安全起见:

public void Save(Dictionary<string, string> idToWebLocation)
    {
        var tasks = new List<Task>();
        foreach (var kvp in idToWebLocation)
        {
            var innerKvp = kvp;
            var id = innerKvp.Key;
            var loc = innerKvp.Value;
            var task = new Task(() => Download(id, loc));
            task.Start();
            tasks.Add(task);
        }

        Task.WaitAll(tasks.ToArray());
    }

还有,因为谁知道:

public void Save(Dictionary<string, string> idToWebLocation)
{
    var tasks = new List<Task>();
    foreach (var kvp in idToWebLocation)
    {
        var innerKvp = kvp;
        var task = new Task(() =>
        {
            var id = innerKvp.Key;
            var loc = innerKvp.Value;
            Download(id, loc);
        });

        task.Start();
        tasks.Add(task);
    }

    Task.WaitAll(tasks.ToArray());
}

但这些都不起作用。显然,我对如何编译这段代码缺乏了解,但我的意思是,到底发生了什么。

似乎var filename = $"{id}.html";source = GetSource(location); 之间的某个地方location 正在发生变化。我很确定代码是线程安全的,没有共享状态,对吧?

但显然不是,因为当我同步遍历字典时,一切都按预期工作。

也许我在这里遗漏了一些基本点,关于外壳、线程或内存等。我不知道,但我的办公桌上满是头发,我快秃顶了。

【问题讨论】:

  • 如果你怀疑字典是线程的问题(我认为也不是,它不参与并行执行),试试 ConcurrentDictionary。仔细看看源文件,它们真的有你期望的内容吗?
  • @MartinMaat 他们没有我期望的内容,这就是问题所在。除非您的意思是当我运行同步时,在这种情况下,是的,我非常仔细地检查了它们。另外 - 我无法想象字典是这里的问题。
  • 它是针对 .NET 4.5.2 编译的 C# 6($"you can see the string {interpolation} at use here" - 这太棒了)。但我真的不认为这是一个关闭问题了。也许。我不知道。
  • 也许你应该试试老派的String.Format() 看看是否有什么不同。我猜$"{key}" 格式对于同一件事来说是句法糖果,但检查一下也无妨。
  • 对不起,我忘了你提到它确实同步工作。尝试在下载方法中的代码周围设置访问锁。如果可行,请通过减少锁定来缩小范围。

标签: c# closures task


【解决方案1】:

任务并行库为每个方法都有一个非常适合您正在做的事情的方法。您可能会发现这与您当前正在尝试做的事情很有趣/相关:

https://msdn.microsoft.com/en-us/library/dd460720(v=vs.110).aspx

【讨论】:

  • 除非我遗漏了什么,否则字典只会在一个线程上使用。
  • 你知道吗?你是对的。也就是说,假设您没有在 Save() 工作流程之外的另一个线程中修改该字典(修改后的闭包不好)。我将更改我的答案以删除并发字典,但您可能想查看 TPL 的 for-each 分区工作流。它专为此类问题而设计。
【解决方案2】:

我认为您应该在创建任务之前尝试创建键和值的局部变量。

var key = kvp.Key;
var value = kvp.Value;
var task = new Task(() => Download(key, value));

【讨论】:

    【解决方案3】:

    如果您将代码更改为使用 async/await 而不是自己构建任务,您是否仍然遇到问题?

    class ClosureCraziness
    {
        public string SaveFolder { get; set; }
    
        public void Save(Dictionary<string, string> idToWebLocation)
        {
            var tasks = new List<Task>();
            foreach (var kvp in idToWebLocation)
            {
                tasks.Add(Download(kvp.Key, kvp.Value));
            }
    
            Task.WaitAll(tasks.ToArray());
        }
    
        async Task Download(string id, string location)
        {
            var filename = $"{id}.html";
            string source = string.Empty;
            try
            {
                source = await GetSource(location);
            }
            catch (Exception e)
            {
                filename = "e-" + filename;
                var ex = e;
                while (ex != null)
                {
                    source += ex.Message;
                    source += Environment.NewLine;
                    source += Environment.NewLine;
                    source += ex.StackTrace;
                    ex = ex.InnerException;
                }
            }
    
            var path = Path.Combine(SaveFolder, filename);
            using (var sw = new StreamWriter(path))
                await sw.WriteAsync(source);
        }
    
        async Task<String> GetSource(string location)
        {
            using (var client = new WebClient())
            {
                return await client.DownloadStringTaskAsync(location);
            }
        }
    }
    

    我与你原来的唯一不同的是使用Task返回版本的WriteDownloadString,将你的帮助方法更改为返回Tasks,并在一些asyncs中加入胡椒粉和awaits 使代码编译。我手边没有编译器,但这应该非常接近正确。

    我实际上并没有看到您的原始文件存在问题,但是通过将任务创建封装在一个将其输入作为参数的函数中,我们应该能够最大限度地减少与闭包相关的错误蔓延的可能性。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-29
      • 1970-01-01
      • 1970-01-01
      • 2012-09-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多