我试图从网站上抓取一些数据。这是我的班级:
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
正在发生变化。我非常确定代码是线程安全的,没有共享状态,对吧?
但显然不是,因为当我同步遍历字典时,一切都按预期完成。
也许我在这里遗漏了一些关于封装,线程或内存等的基本观点。我不知道,但我的桌子上盖着头发,我正在接近秃头。
答案 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
返回版本Write
和DownloadString
,更改辅助方法以返回Task
s,并且在一些async
和await
中加入胡椒以使代码编译。我没有方便的编译器,但这应该非常接近正确。
我实际上并没有看到原始问题,但是通过将任务创建封装在以输入作为参数的函数中,我们应该能够最小化与闭包相关的错误蔓延的可能性。