只使用一个 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
循环而不是上面所示的两个循环?如果否,有没有办法通过使用 async
和 await
来提高执行速度?
如果所有类型的形状都在同一个集合中,则可以只使用一个循环。要启用此功能,您需要为形状创建一个抽象基础 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、Draw
和DrawSyncLock
中的方法只需要是一个方法,但它们都是为了测试目的而存在的。
为了设置,我声明了一个形状列表并用 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.ForEach
和 Try... 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.ForEach
和 SyncLock
- 它唯一做的就是表明您不应该 运行 并行。图形实例方法不是 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
执行时间很短,可能无关紧要。但是并行循环仍然需要更长的时间。即使您发现并行循环花费的时间少了一点,但在并行循环中同步唯一操作的设计是违反直觉的。
请看下面代码:-
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
循环而不是上面所示的两个循环?如果否,有没有办法通过使用 async
和 await
来提高执行速度?
如果所有类型的形状都在同一个集合中,则可以只使用一个循环。要启用此功能,您需要为形状创建一个抽象基础 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、Draw
和DrawSyncLock
中的方法只需要是一个方法,但它们都是为了测试目的而存在的。
为了设置,我声明了一个形状列表并用 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.ForEach
和 Try... 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.ForEach
和 SyncLock
- 它唯一做的就是表明您不应该 运行 并行。图形实例方法不是 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
执行时间很短,可能无关紧要。但是并行循环仍然需要更长的时间。即使您发现并行循环花费的时间少了一点,但在并行循环中同步唯一操作的设计是违反直觉的。