线程会导致GUI卡死

6
所以我并不是很熟悉 C# 编程语言,但是我已经在做一些测试应用程序。
我注意到,如果我为正在工作的应用程序创建更多的线程,我的 GUI 就越容易冻结。我不确定为什么会发生这种情况,我之前认为多线程应用程序的一部分目的是避免 GUI 冻结。
我希望能够得到解释。
此外,这是我用来创建线程的代码:
private void runThreads(int amount, ThreadStart address)
{
    for (int i = 0; i < amount; i++)
    {
        threadAmount += 1;
        Thread currentThread = new Thread(address);
        currentThread.Start();
    }
}

以下是线程运行的内容:

private void checkProxies()
{
    while (started)
    {
        try
        {
            WebRequest request = WebRequest.Create("http://google.co.nz/");
            request.Timeout = (int)timeoutCounter.Value * 1000;
            request.Proxy = new WebProxy(proxies[proxyIndex]);
            Thread.SetData(Thread.GetNamedDataSlot("currentProxy"), proxies[proxyIndex]);
            if (proxyIndex != proxies.Length)
            {
                proxyIndex += 1;
            }
            else
            {
                started = false;
            }
            request.GetResponse();
            workingProxies += 1;
        }
        catch (WebException)
        {
            deadProxies += 1;
        }

        lock ("threadAmount")
        {
            if (threadAmount > proxies.Length - proxyIndex)
            {
                threadAmount -= 1;
                break;
            }
        }
    }
}

4
amount 参数传入的是什么值?你正在启动多少个线程? - Yuval Itzchakov
2
深入解释一下@YuvalItzchakov为什么要询问“amount”的值——如果您启动了大量线程,那么GUI将会受到影响,因为您正在超载系统。 - MBender
我目前在我的应用程序中使用.NET 4.5。我使用多个线程的原因是为了使进程更快。那么,任务类是线程的替代品吗?这样做是否有助于解决GUI冻结问题?此外,我对C#的经验非常少,老实说,我不确定如何利用这些方法。编辑:目前,我的GUI在大约100个线程时开始出现滞后。然而,我感到困惑的原因是,我运行过不同的应用程序(不是我自己编写的,它们是用Java编写的),其中大约有256个线程,并且没有GUI冻结的问题。 - Potato Gun
3
等等,你并不指望你的CPU能够处理任意数量的线程吧? - M.kazem Akhgary
4
你是否试图攻击http://google.co.nz的网站? - Jodrell
显示剩余8条评论
2个回答

6

虽然我不能告诉你为什么你的代码会导致GUI变慢,但是有一些你应该做的事情可以让它更好。如果问题仍然存在,那么定位问题就会变得更容易。

  1. 创建Thread对象是昂贵的。这就是为什么在C#中添加了新类以更好地处理多线程。现在您可以访问Task类或Parallel类(下面有描述)。
  2. 从评论中可以看出,您同时运行了很多线程。虽然只是运行它们不应该成为问题,但如果您正在发射WebRequests(除非您拥有强大的网络),则实际上并没有充分利用它们。确保使用多个线程,但限制其数量。
  3. 当您想要在后台执行特定操作时,Task非常好用。但是,当您想要为特定数据集重复执行单个操作时...为什么不使用System.Threading.Tasks.Parallel类呢?特别是,Parallel.ForEach(其中您可以将代理列表指定为参数)。此方法还可让您使用ParallelOptions设置每次并发运行的线程数。
  4. 另一种编码方式是利用.NET 4.5中提供的asyncawait关键字。在这种情况下,您的GUI(按钮按下?)应调用一个async方法。
  5. 使用线程安全的方法,如Interlocked.IncrementInterlocked.Add来增加/减少可从多个线程访问的计数器。此外,考虑将代理列表更改为ConcurrentDictionary<string, bool>(其中bool表示代理是否有效),并设置值而不必担心,因为每个线程只会访问其自己在字典中的条目。例如,您可以轻松使用LINQ对总数进行排队: dictionary.Where(q => q.Value).Count()以获取工作代理的数量。当然,根据您想要解决问题的方式,也可以使用其他类 - 也许是一个Queue(或ConcurrentQueue)?
  6. 您的lock实际上不应该起作用...也就是说,在您的代码中它似乎是偶然起作用而不是按设计起作用(感谢Luaan的评论)。但是您确实不应该这样做。请参阅有关lockMSDN文档以更好地了解其工作原理。MSDN示例中创建的Object并不仅仅是为了展示。
  7. 您还可以使用BeginGetResponseEndGetResponse方法使请求本身多线程化。实际上,您可以将其与Task类结合使用,以获得更清晰的代码(Task类可以将Begin/End方法对转换为单个Task对象)。

简单回顾一下 - 使用Parallel类进行多线程操作,使用并发类来保持事物有序。

这里是我写的一个快速示例:

    private ConcurrentDictionary<string, bool?> values = new ConcurrentDictionary<string, bool?>();

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        var result = await CheckProxies();
        label.Content = result.ToString();
    }

    async Task<int> CheckProxies()
    {
        //I don't actually HAVE a list of proxies, so I make up some data
        for (int i = 0; i < 1000; i++)
            values[Guid.NewGuid().ToString()] = null;
        await Task.Factory.StartNew(() => Parallel.ForEach(values, new ParallelOptions() { MaxDegreeOfParallelism = 10 }, this.PeformOperation));
        //note that with maxDegreeOfParallelism set to a high value (like 1000)
        //then I'll get a TON of failed requests simply because I'm overloading the network
        //either that or google thinks I'm DDOSing them... >_<
        return values.Where(v => v.Value == true).Count();
    }

    void PeformOperation(KeyValuePair<string, bool?> kvp)
    {
        try
        {
            WebRequest request = WebRequest.Create("http://google.co.nz/");
            request.Timeout = 100;
            //I'm not actually setting up the proxy from kvp,
            //because it's populated with bogus data
            request.GetResponse();

            values[kvp.Key] = true;
        }
        catch (WebException)
        {
            values[kvp.Key] = false;
        }
    }

2
很遗憾,我害怕“lock”实际上会起作用 - 字符串常量将被内部化,因此它始终指向同一实例。然而,这仍然是一个可怕的想法 - 这就像拥有一个“public readonly static”的同步对象。 - Luaan
1
非常好的方法,首先清理所有明显的线程问题,然后再尝试解决实际问题 - 如果它仍然存在的话。顺便说一下,如果您已经在那里,请考虑使用自然异步的HttpClient来提高并行性... - AviD

3
尽管其他评论正确地指出您应该使用Task类或者更好的async API,但这并不是导致线程锁死的原因。
导致线程锁死的代码行是这个:
request.Timeout = (int)timeoutCounter.Value * 1000;

我假设timeoutCounter是WinForm上的控件- 运行在主GUI线程上。换句话说,您的线程代码正在尝试访问不属于其自己线程的控件,这并不真正“允许”,至少不是那么简单。 例如,这个问题展示了如何做到这一点,尽管那里的大多数答案有些过时。 从快速谷歌搜索中(好吧,我承认我用的是必应),我找到了这篇文章很好地解释了问题。

我已经尝试过(并且刚刚重新尝试)将其设置为静态值(5000),但问题仍然存在。编辑:顺便说一下,您正确地假设timeoutCounter是一个控件(它是一个NumericUpDown控件)。 - Potato Gun
@PotatoGun 你在主GUI线程上没有引用控件吗?例如,proxies变量(字符串数组?)是否在窗体的线程上定义? - AviD
等等,我可能错了-还有其他东西需要查看,这行代码 Thread.SetData(Thread.GetNamedDataSlot("currentProxy"), proxies[proxyIndex]); 是否是增量冻结的原因?MSDN声明“为了更好的性能,请使用标有ThreadStaticAttribute属性的字段。” 使用.SetData()间接访问TLS(更不用说装箱)-我很好奇这是否会在许多线程中造成实质性的开销? - AviD
变量被定义在线程可以访问的方法之外,这可能是问题所在。然而,原因是因为我在多个方法中使用它(最初检查和加载代理),我不确定是否有解决方法。其他变量,如proxyIndex变量,存储在任何方法之外,仅仅是因为我不希望它们在每次进入方法时都重新定义为0。编辑:刚刚注意到您的新消息,我马上会看一下。谢谢! - Potato Gun
我认为获取timeoutCounter不可能是问题所在。当尝试从GUI线程外部访问受GUI控制的数据时,C#会抛出错误。 - MBender

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接