【问题标题】:C# Locks - Is it better to lock before a loop or inside it?C# Locks - 在循环之前还是在循环内部锁定更好?
【发布时间】:2025-12-01 02:05:02
【问题描述】:

我目前正在用 C# 制作一个 Web Crawler,我有一个方法可以接收 HTML 字符串,从中提取链接并将链接插入到所有捕获的链接列表中。

由于它是多线程的,所以我使用了锁来防止同时从几个不同的线程访问所有字符串的列表。

锁用哪个比较好?

这个:

void ProcessHTML(string HTML)
{
    List<string> Links = GetLinks(HTML);
    for (int i = 0; i < Links.Count; i++)
    {
        lock (WebsitesHash)
        {
             lock (AllLinks)
             {
                  if (!WebsitesHash.ContainsKey(Links[i]))
                  {
                       WebsitesHash[Links[i]] = true;
                       AllLinks.Add(Links[i]);                    
                  }
             }
        }
    }
}

或者这个:

void ProcessHTML(string HTML)
{
    List<string> Links = GetLinks(HTML);
    lock (WebsitesHash)
    {
        lock (AllLinks)
        {
             for (int i = 0; i < Links.Count; i++)
             {
                  if (!WebsitesHash.ContainsKey(Links[i]))
                  {
                       WebsitesHash[Links[i]] = true;
                       AllLinks.Add(Links[i]);
                  }
             }
        }
    }
}

通常认为哪个更好 - 锁定每次迭代,还是锁定所有迭代?

其他可能相关的代码:

void StartCrawl(string Seed)
{
    AllLinks.Capacity = 1000 * 1000 * 10;
    StreamWriter Log = new StreamWriter(File.Open("Websites.txt", FileMode.Append));
    string HTML = GetHTML(Seed);
    ProcessHTML(HTML);
    for (int i = 0; i < AllLinks.Count; i++)
    {
        if (!Work)
        {
             Log.Close();
             WebsitesHash = new Dictionary<string, bool>();
             break;
        }
        Log.WriteLine(AllLinks[i]);
        websText.Text = AllLinks.Count + "";
        try { HTML = GetHTML(AllLinks[i]); }
        catch { continue; }
        Thread Parser = new Thread(() => ProcessHTML(HTML));
        Parser.Start();
    }
}

【问题讨论】:

  • 我会改用 ConcurrentDictionary。
  • 很少需要使用多个线程来执行此操作。执行 IO(可能是通过 Internet)的行为比解析页面慢数万倍。使用多线程对您的好处微乎其微。
  • @DixonD - 谢谢,但这还不够好,因为我需要一个列表来迭代数字索引。网络爬虫实际上应该是用递归爬虫构建的,我刚刚找到了另一种方法,可以将新链接附加到当前正在迭代的列表末尾以避免递归。这就是为什么我不能将它切换到 ConcurrentDictionary。即使我制作了一个 ,我也需要另一个 HashTable 以避免多次爬取一个网站。
  • @Enigmativity - 我知道网络请求很慢,可能需要 50-1000 毫秒(在某些情况下甚至更多),这比​​解析链接要慢得多。虽然这是真的,但链接的解析涉及带有 IndexOf、包含和替换操作(在字符串上)的循环,当重复如此多次时,这些操作可能会非常慢。我只是不希望解析影响爬行速度,但我认为你是对的。无论如何,即使我决定坚持使用多个线程,是否有人对锁有任何建议?

标签: .net loops thread-safety locks


【解决方案1】:

让AllLinks是链接的全局存储:

public List<string> AllLinks = new List<string>();

在代码中的某处使用 List.BinarySearch 方法来添加新链接:

// "link" contain string of html link
lock(AllLinks)
{
    int index = AllLinks.BinarySearch(link);
    if( index < 0 )
    { // link is not in AllLinks
        AllLinks.Add(~index, link);
    }
    else
    { // link exist, "index" contain its position in list
        // ...
    }
}

我认为 WebsitesHash 对象不是必需的。

UPD 使用 BinarySearch 的另一个优点是 AllLinks 的排序状态。

【讨论】:

  • 哇,非常感谢!我不知道 List 有这样的方法。绝对比使用 HashTable 更好,再次感谢。
【解决方案2】:

在这种情况下,这并不重要。

链接是在锁之外检索的,因此唯一的操作是将少量字符串添加到列表中。这非常小,所以这个问题没有实际意义。

如果工作量更大,锁定在循环内会更好。

虽然锁很便宜,但您只需锁定一次即可进行一些优化。您可以使用private object lockObject = new object(); 来更清楚地了解协议。

【讨论】: