只使用一个 ForEach 循环

Using Only One ForEach Loop

请看下面代码:-

 Private Sub PictureBox1_Paint(sender As Object, e As PaintEventArgs) Handles PictureBox1.Paint
           Parallel.ForEach(Segments.OfType(Of Segment),
                             Sub(segment)
                                 Try
                                      segment.DrawLine(e.Graphics)
                                 Catch ex As Exception
                                 End Try
                             End Sub
                             )
        
          Parallel.ForEach(Ellipses.OfType(Of Ellipse),
                             Sub(ellipse)
                                 Try
                                      ellipse.DrawEllipse(e.Graphics)
                                 Catch ex As Exception
                                 End Try
                             End Sub
                             )
      End Sub

是否可以只使用 一个 ForEach 循环而不是上面所示的两个循环?如果否,有没有办法通过使用 asyncawait 来提高执行速度?

如果所有类型的形状都在同一个集合中,则可以只使用一个循环。要启用此功能,您需要为形状创建一个抽象基础 class(或者让所有形状实现一个通用接口)。

Public MustInherit Class Shape
    Public Property ForeColor As Color

    ... other common properties

    Public MustOverride Sub Draw(g As Graphics)
End Class

然后可以这样推导出具体的形状(以Line为例):

Public Class Line
    Inherits Shape

    Public Property StartPoint As PointF
    Public Property EndPoint As PointF

    Public Overrides Sub Draw(g As Graphics)
        Using pen As New Pen(ForeColor)
            g.DrawLine(pen, StartPoint, EndPoint)
        End Using
    End Sub
End Class

您可以将各种形状添加到 List(Of Shape) 并像这样循环它

Parallel.ForEach(Shapes,
    Sub(shape)
        shape.Draw(e.Graphics)
    End Sub
)

Draw 方法不应抛出异常。他们会,例如,如果笔是 Nothing。但这将是一个必须修复的编程错误,而不是您应该在运行时捕获的错误。因此,删除 Try-Catch.


正如@djv 所展示的那样,使用并行性在这里会适得其反。因此,像这样在单个循环中使用集合(当然删除 SyncLock 和 Try Catch):

For Each shape In Shapes
    shape.Draw(e.Graphics)
Next

请务必了解,您无需知道形状是直线还是椭圆。对于直线,将调用 Line.Draw 方法,对于椭圆,将自动调用 Ellipse.Draw 方法。这叫做Polymorphism.

如果你能以不同的方式识别集合中形状的类型。例如,使用 If TypeOf shape Is Line Then 或使用 shape.GetType().Name 获取其类型名称。但是,如果您必须这样做,那么您可能做错了什么。如果你遵循 OOP 原则,你应该能够以多态的方式做任何事情。

这是一个完整的解决方案,使用与@OlivierJacot-Descombes答案相同的思路。加上一些额外的继承功能。该方案是为了测试问题中讨论的方法与现有答案之间的速度差异。

首先,classes

Public MustInherit Class Shape
    Public Property Name As String
    Public Property ForeColor As Color
    Public Sub Draw(g As Graphics)
        drawShape(g)
    End Sub
    Public Sub DrawSyncLock(g As Graphics)
        SyncLock g
            drawShape(g)
        End SyncLock
    End Sub
    Protected MustOverride Sub drawShape(g As Graphics)
    Public Sub New(foreColor As Color)
        Me.ForeColor = foreColor
    End Sub
End Class

Public Class Line
    Inherits Shape
    Public Sub New(foreColor As Color, startPoint As PointF, endPoint As PointF)
        MyBase.New(foreColor)
        Me.StartPoint = startPoint
        Me.EndPoint = endPoint
    End Sub
    Public Property StartPoint As PointF
    Public Property EndPoint As PointF
    Protected Overrides Sub Drawshape(g As Graphics)
        Using pen As New Drawing.Pen(ForeColor)
            g.DrawLine(pen, StartPoint, EndPoint)
        End Using
    End Sub
End Class

Public Class Ellipse
    Inherits Shape
    Public Sub New(foreColor As Color, rect As RectangleF)
        MyBase.New(foreColor)
        Me.Rect = rect
    End Sub
    Public Property Rect As RectangleF
    Protected Overrides Sub Drawshape(g As Graphics)
        Using pen As New Drawing.Pen(ForeColor)
            g.DrawEllipse(pen, Rect)
        End Using
    End Sub
End Class

摘要class、DrawDrawSyncLock中的方法只需要是一个方法,但它们都是为了测试目的而存在的。

为了设置,我声明了一个形状列表并用 10000 条线和 10000 个椭圆填充它。并在PictureBox1

中设置位图
Public Class Form1

    Private shapes As New List(Of Shape)()

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        PictureBox1.SizeMode = PictureBoxSizeMode.StretchImage
        PictureBox1.Dock = DockStyle.Fill

        For i As Integer = 0 To 99
            For j As Integer = 0 To 99
                shapes.Add(New Line(Color.White, New PointF(i + j, i + j), New PointF(j + 10 + 4, j + 10 + 4)))
            Next
        Next
        For i As Integer = 0 To 99
            For j As Integer = 0 To 99
                shapes.Add(New Ellipse(Color.White, New RectangleF(i + j, i + j, 10, 10)))
            Next
        Next
        PictureBox1.Image = New Bitmap(500, 500)
        Using g = Graphics.FromImage(PictureBox1.Image)
            Dim r As New Rectangle(0, 0, 500, 500)
            g.FillRectangle(New SolidBrush(Color.Black), r)
        End Using
    End Sub

然后我 运行 Paint 处理程序中的三种不同方法,用秒表为它们计时

Private Sub PictureBox1_Paint(sender As Object, e As PaintEventArgs) Handles PictureBox1.Paint
    Dim sw As New Stopwatch()

    sw.Restart()
    Parallel.ForEach(shapes,
                        Sub(s)
                            Try
                                s.Draw(e.Graphics)
                            Catch
                            End Try
                        End Sub)
    sw.Stop()
    Console.WriteLine($"PForEachTry: {sw.ElapsedMilliseconds}")

    sw.Restart()
    For Each s In shapes
        s.Draw(e.Graphics)
    Next
    sw.Stop()
    Console.WriteLine($"ForEach: {sw.ElapsedMilliseconds}")

    sw.Restart()
    Parallel.ForEach(shapes, Sub(s) s.DrawSyncLock(e.Graphics))
    sw.Stop()
    Console.WriteLine($"PForEachSync: {sw.ElapsedMilliseconds}")

End Sub

我发现 Parallel.ForEachTry... empty Catch 非常慢,甚至不在与其他两种方法相同的范围内。大约 10 倍!顺便说一句,这是你原来的方法,所以难怪你试图加快速度。这是一个示例:

PForEachTry: 1188
ForEach: 149
PForEachSync: 177

甚至不值得继续测试那个方法,所以我将删除它并只测试另外两个。以下是通过调整表单大小生成的结果:

sw.Restart()
For Each s In shapes
    s.Draw(e.Graphics)
Next
sw.Stop()
Console.WriteLine($"ForEach: {sw.ElapsedMilliseconds}")

sw.Restart()
Parallel.ForEach(shapes, Sub(s) s.DrawSyncLock(e.Graphics))
sw.Stop()
Console.WriteLine($"PForEachSync: {sw.ElapsedMilliseconds}")

ForEach: 68
PForEachSync: 229
ForEach: 75
PForEachSync: 121
ForEach: 89
PForEachSync: 139
ForEach: 79
PForEachSync: 140
ForEach: 74
PForEachSync: 140
ForEach: 83
PForEachSync: 138
ForEach: 75
PForEachSync: 137
ForEach: 79
PForEachSync: 124
ForEach: 128
PForEachSync: 164
ForEach: 63
PForEachSync: 127

如果它们的调用顺序很重要,我颠倒了顺序,然后再次检查:

sw.Restart()
Parallel.ForEach(shapes, Sub(s) s.DrawSyncLock(e.Graphics))
sw.Stop()
Console.WriteLine($"PForEachSync: {sw.ElapsedMilliseconds}")

sw.Restart()
For Each s In shapes
    s.Draw(e.Graphics)
Next
sw.Stop()
Console.WriteLine($"ForEach: {sw.ElapsedMilliseconds}")

PForEachSync: 303
ForEach: 189
PForEachSync: 149
ForEach: 89
PForEachSync: 241
ForEach: 79
PForEachSync: 138
ForEach: 86
PForEachSync: 140
ForEach: 77
PForEachSync: 146
ForEach: 75
PForEachSync: 237
ForEach: 159
PForEachSync: 143
ForEach: 97
PForEachSync: 128
ForEach: 69
PForEachSync: 141
ForEach: 86

顺序无关紧要。它总是比较慢(有 20000 个形状...)

事实上您已经选择了 Parallel.ForEachSyncLock - 它唯一做的就是表明您不应该 运行 并行。图形实例方法不是 thread-safe,因此当它们是线程中执行的唯一操作时,它们不会从多线程中受益。在执行 long-running 任务时可以忽略使用 Parallel.ForEach 时产生的额外开销,但是许多 short-lived 操作是如此之快以至于委派 20000 个任务的开销开始是 counter-active。当您有几个 long-running 任务时,这是有意义的。

简而言之,并行+单锁操作就是code-stink,理解为什么会有好处。

根据使用了约 500 个形状的评论,我更改了代码以生成 250 条直线和 250 个椭圆。这是结果:

PForEachSync: 3
ForEach: 4
PForEachSync: 3
ForEach: 1
PForEachSync: 4
ForEach: 2
PForEachSync: 4
ForEach: 1
PForEachSync: 3
ForEach: 2
PForEachSync: 3
ForEach: 2
PForEachSync: 4
ForEach: 1
PForEachSync: 3
ForEach: 1
PForEachSync: 3
ForEach: 2
PForEachSync: 4
ForEach: 2

执行时间很短,可能无关紧要。但是并行循环仍然需要更长的时间。即使您发现并行循环花费的时间少了一点,但在并行循环中同步唯一操作的设计是违反直觉的。