在Foreach中创建和启动任务

时间:2015-12-30 20:20:47

标签: c# closures task

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

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 <html> apple </html>

orange.html <html> orange </html>

pear.html <html> peach </html>

peach.html <html> peach </html>

grape.html <html> apple </html>

plum.html <html> plum </html>

(我无法弄清楚如何很好地格式化)

起初我很困惑,因为磁盘上的文件名是正确的,我确信我的Dictionary<string, string>已正确形成(我检查了6次,所有不同的方式),这意味着Id与网站位置的关联很好。

我想也许这是一个关闭问题,回忆起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正在发生变化。我非常确定代码是线程安全的,没有共享状态,对吧?

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

也许我在这里遗漏了一些关于封装,线程或内存等的基本观点。我不知道,但我的桌子上盖着头发,我正在接近秃头。

3 个答案:

答案 0 :(得分:1)

任务并行库有一个针对你正在做的事情而设计的每个方法。您可能会发现您目前正在尝试做的事情很有趣/相关:

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

答案 1 :(得分:0)

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

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

答案 2 :(得分:0)

如果您更改代码以使用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,更改辅助方法以返回Task s,并且在一些asyncawait中加入胡椒以使代码编译。我没有方便的编译器,但这应该非常接近正确。

我实际上并没有看到原始问题,但是通过将任务创建封装在以输入作为参数的函数中,我们应该能够最小化与闭包相关的错误蔓延的可能性。