为什么明确管理线程是件坏事?

时间:2010-06-24 13:04:50

标签: c# multithreading

a previous question中,我做了一个失礼。你知道,我一直在阅读有关线程的文章,并且给人的印象是它们是自奇异果以来最美味的东西。

当我读到这样的东西时,想象一下我的困惑:

  

[T] hreads是一件非常糟糕的事情。或者,至少,线程的显式管理是一件坏事

  

跨线程更新UI通常表明您正在滥用线程。

因为每当有什么事情让我感到困惑时我会杀了一只小狗,所以考虑一下这个机会让你的业力重新回到黑色......

我应该如何使用线程?

12 个答案:

答案 0 :(得分:107)

答案 1 :(得分:11)

明确管理线程不是本质上是一件坏事,但它会带来危险,除非绝对必要,否则不应该这样做。

说线程绝对是件好事就像说螺旋桨绝对是一件好事:螺旋桨在飞机上工作得很好(当喷气发动机不是更好的选择),但对于汽车来说不是一个好主意

答案 2 :(得分:8)

除非你调试了三方死锁,否则你无法理解线程可能导致什么样的问题。或者花了一个月的时间来追逐每天只发生一次的竞赛条件。所以,继续用双脚跳起来,做出你需要做的所有错误,学会害怕野兽以及如何避免麻烦。

答案 3 :(得分:6)

除非您处于能够编写完全成熟的内核调度程序的水平,否则您将得到明确的线程管理总是错误。

自从热巧克力以来,线程可能是最棒的东西,但并行编程非常复杂。但是,如果你设计你的线程是独立的,那么你就不能用脚射击自己。

作为大拇指的先行规则,如果问题被分解为线程,它们应尽可能独立,尽可能少但定义明确的共享资源,并采用最简约的管理概念。

答案 4 :(得分:6)

我无法提供比已经存在的更好的答案。但我可以提供一些具体的例子,说明我们在工作中实际拥有的一些多线程代码,这是灾难性的。

我的一个同事,像你一样,在第一次了解它们时非常热衷于线程。所以在整个程序中开始有这样的代码:

Thread t = new Thread(LongRunningMethod);
t.Start(GetThreadParameters());

基本上,他一直在创造线程。

所以最终另一个的同事发现了这个并告诉负责的开发人员:不要这样做!创建线程很昂贵,你应该使用线程池等等因此,代码中最初看起来像上面代码段的很多地方开始被重写为:

ThreadPool.QueueUserWorkItem(LongRunningMethod, GetThreadParameters());

大改进吧?一切都恢复了理智?

好吧,除了LongRunningMethod中有一个特定的电话可能会阻止 - 很长一段时间。突然间我们偶尔会看到它发生了我们的软件应该立即做出反应的事情......它只是没有。事实上,它可能没有几个秒的反应(澄清:我为一家贸易公司工作,所以这是一场彻底的灾难)。

最终发生的事情是线程池实际上填满了长时间阻塞的调用,导致假设的其他代码很快就会排队等待直到很久以后才运行应该有的。

这个故事的寓意当然不是创建自己的线程的第一种方法是正确的(事实并非如此)。这真的只是使用线程很难,并且容易出错,并且正如其他人已经说过的那样,当你使用它们时,你应该非常小心。

在我们的特殊情况下,犯了很多错误:

  1. 首先创建新线程是错误的,因为它比开发人员实现的成本高得多。
  2. 在线程池上排队所有后台工作是错误的,因为它不加选择地处理所有后台任务,并没有考虑到异步调用实际被阻止的可能性。
  3. 使用长阻塞方法本身就是lock关键字的一些粗心和非常懒惰的结果。
  4. 没有足够的注意力确保 在后台线程上运行的代码是线程安全的(它不是)。
  5. 没有充分考虑是否让许多受影响的代码多线程甚至值得开始。在很多情况下,答案是否定的:多线程只引入了复杂性和错误,使代码不易理解,(这里是踢球者):伤害性能。
  6. 我很高兴地说今天,我们还活着,我们的代码处于比以前更健康的状态。我们在我们认为合适的许多地方使用多线程并测量了性能提升(例如减少接收市场数据滴答和交换确认的传出报价之间的延迟)。但是我们很难学到一些非常重要的课程。如果你曾经在一个大型的高度多线程系统上工作,你也可能会这样做。

答案 5 :(得分:4)

我认为第一个语句最好这样解释:使用many advanced APIs now available,手动编写自己的线程代码几乎不需要。新的API是 lot 更容易使用,并且很多更难搞乱!然而,使用旧式线程,你必须非常好,陷入困境。旧式API(Thread et.al。)仍然可用,但新API(Task Parallel LibraryParallel LINQReactive Extensions)是未来的方式

第二个声明来自设计角度,IMO。在具有清晰的关注点分离的设计中,后台任务不应该直接进入UI以报告更新。那里应该有一些分离,使用像MVVM或MVC这样的模式。

答案 6 :(得分:3)

我首先会质疑这种看法:

  

我一直在阅读关于线程的文章,并且给人的印象是它们是自奇异果jello以来最美味的东西。

不要误解我的意思 - 线程是一种非常通用的工具 - 但这种程度的热情似乎很奇怪。特别是,它表明你可能在许多情况下使用线程,而这些情况根本没有意义(但话说再说一次,我可能只是误解了你的热情)。

正如其他人所指出的,线程处理另外非常复杂和复杂。线程的包装器存在,并且只有在极少数情况下才必须明确处理它们。对于大多数应用程序,可以隐含线程。

例如,如果您只想将计算推送到后台同时保持GUI响应,则更好的解决方案通常是使用回调(这使得看起来好像计算是在后台完成而实际执行时在同一个线程上),或使用一个方便的包装器,如BackgroundWorker,它接受​​并隐藏所有显式的线程处理。

最后一点,创建一个线程实际上非常昂贵。使用线程池可以降低此成本,因为在此,运行时会创建许多随后重用的线程。当人们说线程的显式管理是坏的时,这就是他们可能指的所有。

答案 7 :(得分:2)

许多高级GUI应用程序通常由两个线程组成,一个用于UI,一个(或有时更多)用于处理数据(复制文件,进行大量计算,从数据库加载数据等)。

处理线程不应该直接更新UI,UI应该是一个黑盒子(检查维基百科的 Encapsulation )。 他们只是说“我已完成处理”或“我完成了第9个任务中的任务7”并调用了事件或其他回调方法。 UI订阅该事件,检查已更改的内容并相应地更新UI。

如果您从处理主题更新UI,您将无法重复使用代码,如果您想更改部分代码,则会遇到更大的问题。

答案 8 :(得分:2)

我认为你应该尽可能多地使用Threads进行体验,并了解使用它们的好处和缺陷。只有通过实验和使用,您对它们的理解才会增长。尽可能多地阅读这个主题。

说到C#和用户界面(单线程,你只能修改UI线程上执行的代码上的用户界面元素)。我使用以下实用程序让自己保持理智,晚上睡得很香。

 public static class UIThreadSafe {

     public static void Perform(Control c, MethodInvoker inv) {
            if(c == null)
                return;
            if(c.InvokeRequired) {
                c.Invoke(inv, null);
            }
            else {
                inv();
            }
      }
  }

您可以在任何需要更改UI元素的线程中使用它,例如:

UIThreadSafe.Perform(myForm, delegate() {
     myForm.Title = "I Love Threads!";
});

答案 9 :(得分:1)

我认为线程是一件非常好的事情。但是,与他们合作非常困难,需要大量的知识和培训。主要问题是当我们想要从其他两个线程访问共享资源时会产生不良影响。

考虑一下经典的例子:你有两个线程可以从共享列表中获取一些项目,然后在做某事后从列表中删除项目。

定期调用的线程方法如下所示:

void Thread()
{
   if (list.Count > 0)
   {
      /// Do stuff
      list.RemoveAt(0);
   }
}

请记住,理论上,线程可以在代码的任何未同步的行上切换。因此,如果列表只包含一个项目,则一个线程可以传递list.Count条件,就在线程切换list.Remove之前,另一个线程传递list.Count(列表仍然包含一个项目)。现在第一个线程继续list.Remove,然后第二个线程继续list.Remove,但第一个线程已经删除了最后一个项目,所以第二个线程崩溃了。这就是必须使用lock语句进行同步的原因,这样就不会出现if语句中有两个线程的情况。

这就是为什么未同步的UI必须始终在单个线程中运行而其他线程不应该干扰UI的原因。

在.NET的早期版本中,如果要在另一个线程中更新UI,则必须使用Invoke方法进行同步,但由于实现起来很难,因此新版本的.NET附带{{ 1}}类通过包装所有内容并让你在BackgroundWorker事件中执行异步内容并在DoWork事件中更新UI来简化事物。

答案 10 :(得分:1)

尝试保持UI线程和处理线程尽可能独立的一个重要原因是,如果UI线程冻结,用户将注意到并且不高兴。让UI线程快速发展非常重要。如果您开始将UI内容移出UI线程或将处理内容移动到UI线程中,那么您的应用程序无响应的风险会更高。

此外,很多框架代码都是故意编写的,期望您将UI和处理分开;当你将两者分开时,程序会更好地工作,并且当你不这样做时会遇到错误和问题。我不记得由于这个原因我遇到的任何具体问题,尽管我过去模糊的回忆试图设置UI在UI之外负责并且代码拒绝工作的东西的某些属性;我不记得它是不编译还是抛出异常。

答案 11 :(得分:0)

从非UI线程更新UI时需要注意以下几点:

  1. 如果经常使用“调用”,如果其他内容使UI线程运行缓慢,则非UI线程的性能可能会受到严重的不利影响。我宁愿避免使用“调用”,除非非UI线程需要等待UI线程操作在继续之前执行。
  2. 如果对控制更新之类的东西不加考虑地使用“BeginInvoke”,过多的调用委托可能会排队,其中一些在实际发生时可能毫无用处。
  3. < /醇>

    在许多情况下,我首选的样式是将每个控件的状态封装在一个不可变类中,然后有一个标志,指示更新是否需要,挂起或需要但不是挂起(后一种情况可能发生在请求在完全创建之前更新控件。如果需要更新,控件的更新例程应该从清除更新标志,获取状态和绘制控件开始。如果设置了更新标志,则应重新循环。要请求另一个线程,例程应该使用Interlocked.Exchange来设置更新标志以更新挂起,并且 - 如果它没有挂起 - 尝试BeginInvoke更新例程;如果BeginInvoke失败,请将更新标志设置为“required but not pending”。

    如果在控件的更新例程检查并清除其更新标志之后发生控制尝试,则可能会发生第一次更新将反映新值,但无论如何都会设置更新标志,从而强制额外的屏幕重绘。在发生这种情况时,它将是相对无害的。重要的是控件最终会被绘制到正确的状态,而不会有多个BeginInvoke挂起。