在Foreach中创建和启动任务

本文关键字:启动 任务 创建 Foreach | 更新日期: 2023-09-27 18:28:35

我试图从一个站点中抓取一些数据。这是我的课:

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>

葡萄.html <html> apple </html>

plum.html <html> plum </html>

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

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

我想这可能是一个结束问题,回忆起埃里克·利珀特教我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正在发生变化。我确信代码是线程安全的,没有共享状态,对吧?

但显然不是,因为当我同步地遍历字典时,一切都和预期的一样。

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

在Foreach中创建和启动任务

任务并行库有一个for each方法,它非常适合您正在做的事情。你可能会发现这与你目前正在尝试做的事情很有趣/相关:

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

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

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

如果您将代码更改为使用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,更改您的helper方法以返回Tasks,并在一些asyncs和awaits中添加胡椒粉以编译代码。我手头没有编译器,但这应该很接近正确。

我实际上没有看到你的原始版本有什么问题,但通过将任务创建封装在一个以其输入为参数的函数中,我们应该能够最大限度地减少与关闭相关的错误悄悄出现的可能性。