HtmlAgilityPack与Parallel.Invoke多线程调用异常问题及解决方法
这个问题我之前也碰到过,核心原因很明确:HtmlAgilityPack的HtmlDocument实例不是线程安全的。你现在的Table类里,所有方法共享了同一个doc对象,当Parallel.Invoke同时触发Foo1、Foo2、Foo3时,多个线程在同时修改这个共享的HtmlDocument,直接导致内部的HtmlNodeCollection在执行Append操作时出现并发冲突,也就是你看到的那个异常。
给你几个可行的解决方案,按推荐程度排序:
方案1:每个方法内部创建独立的HtmlDocument实例(最推荐)
既然每个方法都是独立加载自己的HTML内容,完全没必要共享同一个文档实例。修改你的Table类,让每个方法自己创建专属的HtmlDocument:
public class Table { public void Foo1() { HtmlDocument doc = new HtmlDocument(); doc.LoadHtml("some html"); // 这里写针对当前doc的业务操作 } public void Foo2() { HtmlDocument doc = new HtmlDocument(); doc.LoadHtml("some html"); // 这里写针对当前doc的业务操作 } public void Foo3() { HtmlDocument doc = new HtmlDocument(); doc.LoadHtml("some html"); // 这里写针对当前doc的业务操作 } }
这种方式完全避免了线程间的资源竞争,不需要任何同步锁,能完美保留Parallel.Invoke的并发性能,是最适合你当前场景的方案。
方案2:用ThreadLocal实现线程内的实例复用
如果你的场景需要在同一个线程的多次方法调用中复用HtmlDocument(比如同一个线程连续调用Foo1和Foo2),可以用ThreadLocal来确保每个线程拥有自己的专属实例:
public class Table { // 每个线程会生成自己的HtmlDocument实例,线程间互不干扰 private ThreadLocal<HtmlDocument> _threadDoc = new ThreadLocal<HtmlDocument>(() => new HtmlDocument()); public void Foo1() { var doc = _threadDoc.Value; doc.LoadHtml("some html"); // 业务操作 } public void Foo2() { var doc = _threadDoc.Value; doc.LoadHtml("some html"); // 业务操作 } public void Foo3() { var doc = _threadDoc.Value; doc.LoadHtml("some html"); // 业务操作 } }
这种方式既实现了同一线程内的实例复用,又保证了线程安全,性能也不会受太大影响。
方案3:给共享实例加同步锁(不推荐,仅作备选)
如果你的业务逻辑必须让所有方法共享同一个HtmlDocument实例(比如所有方法都要修改同一个文档内容),那只能通过加锁来强制串行访问:
public class Table { private HtmlDocument doc = new HtmlDocument(); // 专门用于锁的对象,推荐用私有专属对象而非this private readonly object _lockObj = new object(); public void Foo1() { lock(_lockObj) { doc.LoadHtml("some html"); // 业务操作 } } public void Foo2() { lock(_lockObj) { doc.LoadHtml("some html"); // 业务操作 } } public void Foo3() { lock(_lockObj) { doc.LoadHtml("some html"); // 业务操作 } } }
注意:这种方式会让Parallel.Invoke的并发调用退化为串行执行,完全失去多线程的性能优势,所以只有在万不得已的情况下才考虑使用。
总结一下:优先选方案1,因为你的每个方法都是独立处理HTML,完全不需要共享实例,这样既能解决线程安全问题,又能保留多线程的效率。
内容的提问来源于stack exchange,提问作者utop

