1

Please see the following Code:-

 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

Is it Possible to use only One ForEach loop instead of Two as shown above? If No, is there a way to Enhance the Speed of Execution by using async and await?

Raky
  • 625
  • 5
  • 19
  • Since Segments and Ellipses seem to be in two different collections, why do you have to test whether they are `OfType(Of Segment)` or `OfType(Of Ellipse)`? It would be possible to use only one loop if both types of objects would be in the same collection typed as the base class of the two. These Draw methods do usually not throw exceptions. – Olivier Jacot-Descombes Feb 02 '22 at 17:39
  • `Segment` and `Ellipse` are Classes to Draw Lines and oval shapes of differing attributes like length, diameter, color, Brightness, etc. – Raky Feb 02 '22 at 17:42
  • Even If I modify the class definitions to put both shapes into same Class will the execution speed enhance? – Raky Feb 02 '22 at 17:43
  • Have you determined that this is the step that needs to be sped up, or does it just look like a candidate for improvement? Why could the DrawLine or DrawEllipse methods fail in such a manner that the failure should be ignored? – Andrew Morton Feb 02 '22 at 18:12
  • @AndrewMorton, Yes I have identified that this needs Speeding up because it takes about 0.12 Seconds to execute these two `ForEach` loops. When I disable one `ForEach` loop, the execution time is about 0.03 seconds. – Raky Feb 02 '22 at 18:15
  • 2
    `System.Drawing.Graphics` instance members are not thread-safe. I'm not sure how this even works in parallel in the first place. And no, `Async/Await` does not add parallelism, which you have already done. – djv Feb 02 '22 at 18:19
  • @djv So, it should be faster as a normal loop instead of trying to Parallel.ForEach it? – Andrew Morton Feb 02 '22 at 18:20
  • @AndrewMorton it's worth a try. The TPL may be doing extra work to invoke on the UI thread anyway. I don't really know. – djv Feb 02 '22 at 18:22
  • The Normal `For Each` is slow. `Parallel.ForEach` is much faster. I had first used the normal `For Each` and then learnt about `Parallel.ForEach` and found the execution was much faster and smoother. – Raky Feb 02 '22 at 18:23
  • 2
    Just in case the code is falling foul of something else: [What is the right way to use OnPaint in .Net applications?](https://stackoverflow.com/a/13652570/1115360) – Andrew Morton Feb 02 '22 at 18:27
  • 1
    I have tested your code with the parallelism, and I get an exception `System.InvalidOperationException HResult=0x80131509 Message=Object is currently in use elsewhere.`, which is what I had expected. Converting to a normal `For Each` makes the code work... – djv Feb 02 '22 at 18:38
  • 1
    Ahhhh! It's the reason for the empty `Try...Catch`! – djv Feb 02 '22 at 18:41
  • 1
    The `For Each` takes the *right* amount of time. You can't parallelize these operations because the Graphics object has affinity to the UI thread, and you can't perform multiple operations on the UI thread at the same time. When you run in parallel you have numerous failures and perhaps a failure takes less time than a successful draw. Hence it seems to take less time, but in reality you aren't drawing all the shapes in your list(s). The accepted answer with inheritance is still a great way to solve the type-checking problem (which was originally the only issue you thought you had). – djv Feb 02 '22 at 18:46
  • @djv, in the `Draw` method of the Class I have used `SyncLock gr` and `End SyncLock` to prevent the error.. `Try...Catch` was to circumvent when the `Pen` or `Bruch` are `null` or `Nothing` – Raky Feb 02 '22 at 18:51
  • Using `Parallel.ForEach` is has actually sped up the execution. – Raky Feb 02 '22 at 18:52
  • 2
    @Raky it depends on how many operations you are performing. The overhead from `Parallel.ForEach` can't be justified if the operations are individually quick, and synchronizing the sole operation defeats the purpose of parallelism. My answer disputes what you have claimed directly above, but perhaps the number of shapes varies enough to change the outcome. Still, be suspicious of a parallel solution with synchronization. The two things are in opposition of each other. – djv Feb 02 '22 at 19:54

2 Answers2

4

Here is a complete solution, using the same idea as @OlivierJacot-Descombes answer. Plus some additional inherited functionality. This solution means to test the speed differences between the methods discussed in the question and existing answer.

First, the 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

The methods in the abstract class, Draw and DrawSyncLock only need to be one method, but they both exist for testing purposes.

To set up, I declare a list of shape and populate it with 10000 Lines and 10000 Ellipses. And set up the bitmap in 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

Then I ran three different methods inside the Paint handler, with a stopwatch to time them

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

What I found is that the Parallel.ForEach with a Try... empty Catch is extremely slow, not even in the same ballpark as the other two methods. About 10x! By the way, this was your original method, so no wonder you were trying to speed things up. Here is one sample:

PForEachTry: 1188
ForEach: 149
PForEachSync: 177

It's not even worth continuing to test that method, so I'll remove it and test just the other two. Here are the results, generated by resizing the form:

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

In case the order in which they are called matters, I reversed the order, and checked again:

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

Order doesn't matter. It's always slower (with 20000 shapes...)

The fact that you have settled on a Parallel.ForEach and SyncLock - the only thing it does is indicate that you shouldn't be running in parallel. Graphics instance methods are not thread-safe, so they won't benefit from multithreading when they are the only operation being performed in the thread. The additional overhead created when using Parallel.ForEach can be ignored when performing long-running tasks, but numerous short-lived operations are so quick that the overhead of delegating 20000 tasks starts to be counter-active. It makes sense when you have a few long-running tasks.

In short, parallel + a single locked operation is code-stink, and it's beneficial to understand why.

Based on the comment that ~500 shapes are used, I changed the code to generate 250 Lines and 250 Ellipses. This is the result:

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

The execution time is so short it probably doesn't matter much. But the parallel loop still takes a bit longer. Even if you find the parallel loop takes a bit less time, the design of synchronizing the sole operation in a parallel loop is counterintuitive.

djv
  • 15,168
  • 7
  • 48
  • 72
  • I have a total of 362 `Line`s and 212 `Ellipse`s which are redrawn after lapse of every 1 Tick. There are `Slider`s and `Button`s which change the attributes of certain Shapes only and Call for `PictureBox1.Refresh()`. The normal `For Each` was slower and the `Parallel.ForEach is much faster. However, every `PictureBox1.Refresh()` takes about `0.12` seconds. – Raky Feb 03 '22 at 01:47
  • The two classes for `Segment` and `Ellipse` already have a `Public Property Name`. Please tell me how to Identify the Segment(Line) and Ellipse(OvalShapes) when we make a collection? – Raky Feb 03 '22 at 03:32
  • 1
    @Raky You can add the property Name to the base class Shape – djv Feb 03 '22 at 04:14
  • 1
    @Raky and I agree it's possible that a lesser number of shapes such as in your case may result in a quicker parallel execution. Especially if you do other things each iteration in addition to the draw. – djv Feb 03 '22 at 04:15
  • 1
    I added `Public Property Name As String` to my code above. – djv Feb 03 '22 at 04:23
  • 2
    Good work. This is what I had suspected. Multithreading is counterproductive in this case. – Olivier Jacot-Descombes Feb 03 '22 at 15:58
2

It would be possible to use only one loop if all types of shapes were in the same collection. To enable this, you would need to have an abstract base class for shapes (or to let all the shapes implement a common interface).

Public MustInherit Class Shape
    Public Property ForeColor As Color

    ... other common properties

    Public MustOverride Sub Draw(g As Graphics)
End Class

Then you can derive the concrete shapes like this (with Line as an example):

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

The you can add all the kinds of shapes to a List(Of Shape) and loop it like this

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

The Draw methods should not throw exceptions. They would, e.g., if the pen was Nothing. But this would be a programming error that has to be fixed and not something that you should be caught at runtime. So, remove the Try-Catch.


As @djv has demonstrated, using parallelism is counterproductive here. So, use the collection in a single loop like this (and of course remove SyncLock and Try Catch):

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

It is important to understand that you don't need to know whether the shape is a Line or an Ellipse. For lines the Line.Draw method will be called, for an ellipse the Ellipse.Draw will be called automatically. This is called Polymorphism.

If you can identify the type of a shape in the collection in different ways. E.g., with If TypeOf shape Is Line Then or by getting its type name with shape.GetType().Name. However, if you must do this, then you are probably doing something wrong. If you follow the OOP principles, you should be able to do everything in a polymorphic way.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • Please tell me if use of `async` would speed up the execution. – Raky Feb 02 '22 at 18:05
  • 1
    No, [async keeps the UI responsive](https://learn.microsoft.com/en-us/dotnet/visual-basic/programming-guide/concepts/async/#BKMK_WhentoUseAsynchrony) while things are done asynchronously. It does not run things in parallel automatically. – Olivier Jacot-Descombes Feb 02 '22 at 18:16
  • 3
    But questions regarding speed can usually only be answered by doing [benchmarking](https://benchmarkdotnet.org/articles/overview.html). Very often code optimizations make the code more complicated but do not yield the expected advantage. I am not even sure if this Parallel.For does even speed up drawing since only one draw operation can runt at a time. – Olivier Jacot-Descombes Feb 02 '22 at 18:25