【问题标题】:Crash course in simple threading?简单线程速成课程?
【发布时间】:2011-09-08 21:32:29
【问题描述】:

我正在测试线程的想法,但现在只在非常关键的地方。线程几乎为任何事情增加了相当迷人的复杂性,但在 .NET 中,System.Threading 中的线程似乎有很多选择。我想知道哪个最适合处理字符串操作。

考虑将 complex 字符串提供给自定义对象。该对象当前在某个点拆分字符串,并将第一部分提供给函数,然后当该函数完成时,将字符串的另一半提供给第二个函数。这两个函数相互之间没有依赖关系,因此应该是线程的良好候选者,以便两个函数可以在字符串的每一段上同时工作。

添加前的示例:

Public Sub ParseString(ByVal SomeStr As String)
    If String.IsNullOrWhitespace(SomeStr) Then
        Throw New ArgumentNullException("SomeStr")
    End If

    ' Assume that ParsedFirstString is a boolean that is set to
    ' True if the call to ParseFirstString completes successfully.
    ' Ditto for ParsedSecondString.

    Dim MyDelimiter As Char = "|"c
    Dim SomeStrArr As String() = SomeStr.Split({MyDelimiter}, 2)

    Call Me.ParseFirstString(SomeStrArr(0))

    If Me.ParsedFirstString = False Then
        Throw New ArgumentException("Failed to parse the first part of the string.")
    End If

    Call Me.ParseSecondString(SomeStrArr(1))

    If Me.ParsedSecondString = False Then
        Throw New ArgumentException("Failed to parse the second part of the string.")
    End If
End Sub




这工作正常,并且在我的多核系统上的时序循环中进行测试,我可以在 ~140ms-170ms 内执行 1,000 次(如果 10,000 次,则平均 ~1,200ms+)。这是一个可以接受的速度,如果我不能让线程发挥得很好,那么我会继续前进。但是我在查看了一个threading example 和一个关于调用thread with parameters 的SO 问题后尝试了一种线程方法,并最终得到类似于以下的代码:

Public Sub ParseString(ByVal SomeStr As String)
    If String.IsNullOrWhitespace(SomeStr) Then
        Throw New ArgumentNullException("SomeStr")
    End If

    Dim MyDelimiter As Char = "|"c
    Dim SomeStrArr As String() = SomeStr.Split({MyDelimiter}, 2)

    Dim FirstThread As New Thread(Sub() Me.ParseFirstString(SomeStrArr(0))
    Dim SecondThread As New Thread(Sub() Me.ParseSecondString(SomeStrArr(1))

    FirstThread.Priority = ThreadPriority.Highest
    SecondThread.Priority = ThreadPriority.Highest

    Call FirstThread.Start()
    Call SecondThread.Start()

    If Me.ParsedFirstString = False Then
        Throw New ArgumentException("Failed to parse the first part of the string.")
    End If

    If Me.ParsedSecondString = False Then
        Throw New ArgumentException("Failed to parse the second part of the string.")
    End IF
End Sub




这样做的问题是,字符串的第一部分或第二部分的解析可以在 两者 完成之前完成,这会导致两个异常之一.于是我进一步环顾四周,发现我可以使用Join 方法来等待两个线程都完成。这解决了异常的触发,但它大大增加了执行时间。执行上述函数 1,000 次并对其计时,现在平均运行时间可达约 3,700 毫秒。线程似乎不适合这种任务。

但似乎还有其他线程机制,包括 ThreadPools 和 BackgroundWorkers。可能其他人我还没有查过(我几个小时前才开始搞砸这个)。

社区对此类任务的线程有何看法?我第一次尝试线程有什么问题?

仅供参考,我不会更新任何 UI 组件,也不会将结果写入任何类型的存储介质。





结论:

看来我的字符串解析功能比我想象的要好得多。在尝试了Parallel ClassTask Class 之后,如果我测试一个 10,000 次迭代循环,然后是单线程,我的测试数据大约为 ~1,220ms-1,260ms。如果我什至实现基本的Parallel.Invoke() 以将解析拆分为两个并行线程,我会将计时循环填充到额外的〜300ms(可能是由于匿名委托的开销,但似乎没有办法解决这个问题)。这是在 Core2 Q9550 Yorkfield,未超频的 95W 处理器上进行比较。

成功的选择是在这个特定的代码区域保持单线程。感谢所有参与的人!

【问题讨论】:

  • 第一课:线程并不简单,尤其是看起来很简单的时候。
  • @John:我知道。我之前曾尝试为 SMP 系统编写 IRQ 处理程序。最终结果(到目前为止)并没有取得太大的成功,所以我暂时休息一下并切换回 .NET 开发。
  • 在这种情况下,你已经准备好学习线程的第二课了:除非你必须这样做,否则不要这样做。让您的程序在没有线程的情况下工作。如果您随后需要线程来使其表现良好,那么请使用 .NET 的新 TPL 功能来重构您的代码 - 这样它就可以继续工作,但也表现良好。
  • @John:实际上,该项目的框架基本上是完整的。我只是在整理和重新审视我 6 个多月前写的东西。这也允许尝试新事物以查看它们是否有效。如果没有,那很好。仔细阅读我的问题——我说我只是在试验,如果线程不适合我,也没有坏处,没有犯规:)
  • 在 Fx4 中,使用任务 (TPL)。 TPL 使用线程池。 Backgroundworker 只能在 GUI (Win/WPF) 中有限使用。

标签: .net vb.net multithreading parallel-processing task-parallel-library


【解决方案1】:

我建议使用 TPL 类,例如 ParallelTask

您的代码是否受益于并行执行,您需要在特定机器上进行基准测试并找出答案。这是最好的方法。相同的代码在一台机器上会减慢执行速度,但在另一台机器上会加快很多。基本上取决于CPU(核数、超线程等)、算法和并行任务数。

如果您使用 TPL,您的代码将如下所示:

    Call Parallel.Invoke(
        Sub()
            Me.ParseFirstString(SomeStrArr(0))
        End Sub,
        Sub()
            Me.ParseFirstString(SomeStrArr(1))
        End Sub)

对不起,我不擅长 VB.NET 语法。可能有办法让它更短。

【讨论】:

  • 有趣。 Invoke 的使用是否完全涉及反射,或者该函数只是共享反射命名空间中所谓函数的名称?
  • @Kumba - 此代码与反射无关。该方法只是具有相似的名称。
  • 感谢您的来信。您的答案需要稍作调整才能使用匿名方法,因为Invoke 需要这样(上面的示例解析方法实际上是子方法,如果不清楚,请见谅)。
  • @kumba,Parallel.Invoke 将是为 2 个 ParseString 方法使用任务的最佳选择。请注意,除了较低的开销之外,您还可以免费获得同步(加入)和异常管理。
  • 我的结果表明,即使使用Parallel 也没有任何好处。我怀疑线程初始化中有太多开销,这严重否定了任何感知到的好处。剩下的单线程是我要走的路。也就是说,您的答案就是成功的答案,因为它为 .NET 4.0 的用户展示了一种非常简单的方法,可以在他们的项目中实现安全而有效的线程。谢谢!
【解决方案2】:

当正在执行的工作占用的 CPU 周期多于创建/管理/加入多个线程的开销时,线程化(或一般的并行化)是有益的。

如果您要解析代码并且字符串相对简单,那么多线程实际上会使事情变慢。

线程创建是一项相对昂贵的操作。 .NET 4 引入了“任务”的概念。任务是您希望与其他代码并行执行的代码块。 .NET Framework 内置了许多智能功能,可以将所有任务拆分到理想数量的线程中(通常与您拥有的 CPU 内核数量相同),并为多个任务重用相同的线程。

任务仍然有不小的开销,但比原始线程少得多。因此,在许多情况下,Tasks 仍然会使事情比串行代码慢,但这种情况会更少。在没有看到您的输入字符串和解析方法的情况下,我们无法说出您的特定场景在此范围内的位置。

【讨论】:

  • 这就是为什么我说我正在处理复杂的字符串,而不是简单的字符串。假设字符串的每一半在结构上与另一半完全不同,因此我使用两个相互独立的独立解析函数。我认为这是线程的可行候选者,但我不确定。这里关于 SO 的许多线程问题涉及更新 UI 组件或将值写入存储(平面文件、数据库等)。我还没有这样做。
  • UI 和 I/O 是线程的常见场景,因为您不希望 UI 在 I/O 发生时“冻结”。线程仍然有许多有效的纯计算场景,你的很可能就是其中之一。请参阅我的编辑回复:尝试使用开销较低的任务
  • @Robert:我目前正在测试 Alex 的建议,即使用 Parallel。接下来将尝试任务。它们似乎都是 .NET 4.0 的新手。
  • @Kumba - Alex 对 Parallel.Invoke 的建议很好。在幕后,Parellel.Invoke 使用任务。您最好了解有关任务的更多信息,但无需单独进行基准测试。
  • Parallel 确实是一个漂亮的类。 .NET 4.0 用户的另一个好处。目前正在查看ForForEach 方法。但是,我发现 MS 的示例有点缺乏细节,比如没有在变量声明中指定类型或使用未定义的变量。当 IDE 开始抱怨时,尝试理解示例变得更加困难。
【解决方案3】:

对我来说,从 UI 线程中卸载任何计算是最好的选择。特别是如果出于任何原因它可能会很耗时,无论是在复杂性的数量上,还是两者兼而有之。

如您所见,有很多方法可以做到这一点。很大程度上取决于您在之后/期间想要什么样的信息。

虽然我的 .net 选择是 c#,但前提是相同的。

您可以使用任务,这似乎工作得很好,因为您可以轻松地为它们提供选项 Parallel.Invoke 允许选项

对我来说,我是后台工作人员,除非您确切知道要拥有多少个,否则我发现它们不太适合使用,因为如果您使用工具箱中的组件,则必须预先制作它们,还需要先弄清楚他们是否忙,然后再让他们去做。就个人而言,我想要一些我可以说“来,去做”的事情,就像一群仆从在排队等待工作。

线程也可以工作,但是,如果(像我一样)您要发送大约 10k 封电子邮件并要求它解析它们,我发现它会涉及更多。

从表面上看,你有最好的想法,获取你想要的代码,然后尝试每种方法,看看哪一种适合你,你的思维方式和速度。如果您发现一个明显慢于其他任何一个,或者执行它的 UI 线程,很可能有办法改进它。

如果您遇到示例,MS 当然会为每种类型提供大量示例。

【讨论】:

  • 目前还没有 UI 组件。离那还有一段距离。我现在只是在纯代码中工作。我查看了 MS 的示例,但有时,他们的示例展示了难以归结为我想要完成的事情的重要事情。这就是为什么我最近在很多事情上都转向 java2s 和 SO。
  • 如你所说,有时MS的例子太简单了,对不起,线程中的“睡眠”不是现实线程的例子。我想要更多的东西。到目前为止,我一直避免使用 java :) 我真的不喜欢 UI 似乎响应速度慢得多并且需要更长的时间才能启动。
  • 我仍然在 delphi 世界中工作,在那里我有一个名为 TPerfectThread 的类,我可以将其用作工厂来提交更多作业。爱它。我想我需要为.net 做类似的事情
  • @Kumba,也关注您的评论,我不是在谈论更新 UI,我没有错过重点,我说的是卸载任何东西,即使它根本没有直接影响 UI 是一件好事。它允许以后轻松。
  • 是的,当我曾经涉足 VBA 时,我很快就学会了在进行一些后台处理时定期调用 DoEvents,这样我的 UI 就不会出现挂断用户。
猜你喜欢
  • 2011-01-20
  • 2023-03-16
  • 1970-01-01
  • 2014-03-11
  • 1970-01-01
  • 1970-01-01
  • 2010-09-08
  • 2021-08-28
  • 1970-01-01
相关资源
最近更新 更多