5

I know of course that I can not draw onto the same Graphics object from different threads, but is it also true that I can not draw to different Graphics objects in different threads?

Consider the following console program:

class Program
{
    static ThreadDrawer[] drawers;
    static void Main(string[] args)
    {
        int numThreads = 8;
        drawers = new ThreadDrawer[numThreads];
        for (int i = 0; i < numThreads; i++)
        {
            drawers[i] = new ThreadDrawer();
            drawers[i].Start();
        }
        for (int i = 0; i < numThreads; i++)
        {
            drawers[i].Wait();
        }
        Console.WriteLine("Complete.");
        Console.ReadKey();
    }

    class ThreadDrawer
    {
        private Thread thread;
        private AutoResetEvent resetEvent;

        public ThreadDrawer()
        {
            thread = new Thread(DrawRandomCircles);
            resetEvent = new AutoResetEvent(false);
        }

        public void Start()
        {
            thread.Start();
        }

        public void Wait()
        {
            resetEvent.WaitOne();
        }

        public void DrawRandomCircles()
        {
            Random r = new Random(Environment.TickCount);
            using (Bitmap b = new Bitmap(1000, 1000))
            using (Graphics g = Graphics.FromImage(b))
            {
                for (int i = 0; i < 100000; i++)
                {
                    g.DrawEllipse(Pens.Red, new Rectangle(r.Next(1000), r.Next(1000), 200, 200));
                }
            }
            resetEvent.Set();
        }
    }
}

The program creates a Bitmap in each thread and proceeds to draw random ellipses on it using a Graphics object, also generated per thread from the Bitmap.

Due to a requirement to build for .net2 the multithreading is implemented using Threads and AutoResetEvents instead of TPL.

The program executes without throwing an exception, but it executes serially. Using n threads multiplies execution time by n and it is clear to see using the task manager that only one core is being used.

Important to take note that none of this is tied to any UI element.

What is going on here? Is the Graphics object locking on a static object?

Rotem
  • 21,452
  • 6
  • 62
  • 109

3 Answers3

5

Here's a screen-shot of the concurrency analyzer I used to see what's going on with these threads:

enter image description here

Yes, you can see lots of red (blocking) with flecks of green (execution). The threads are taking turns entering a critical section that's acquired inside the internal GpGraphics::RenderDrawPath() function. The larger blobs of green is where the program actually drew the lines (I replaced DrawEllipse with DrawRectangle and got rid of the Random call).

There is some concurrency, you can for example see the RenderDrawPath() call being overlapped by the code that renders the anti-aliased lines, overall cpu load is around 35%. But there isn't much of it.

Nothing you can do about it of course. You get ahead by overlapping the logic in your own program to decide what to draw with the GDI+ calls. Which will normally happen, the test is too synthetic.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Well that's depressing. I think it might finally be time to start learning WPF. Thanks for the research work :) – Rotem Aug 18 '13 at 14:17
  • This is the tool he used in the screenshot: https://learn.microsoft.com/en-us/visualstudio/profiling/concurrency-visualizer – juFo May 29 '17 at 12:47
3

It seems like locking happens in unmanaged code, inside GDI+ library (unfortunately, this behavior is not mentioned in official docs).

Similar question: Parallelizing GDI+ Image Resizing .net

Community
  • 1
  • 1
max
  • 33,369
  • 7
  • 73
  • 84
2

I'm not 100% sure.. but yes, there is a private static locking object in the Graphics class. It appears to be locked only from GetHalftonePalette, which in turn, is called whenever a Bitmap is initialized within the Graphics object. It would appear that this could be the cause of contention.

(Note: Initial findings after 5 minutes of using ILSpy.. not very in-depth)

Simon Whitehead
  • 63,300
  • 9
  • 114
  • 138
  • Could you please define "whenever a `Bitmap` is initialized within the `Graphics` object"? Do you mean when a `Graphics` object is created from a `Bitmap`? – Rotem Aug 18 '13 at 11:42
  • Apologies. What I mean is when the `Graphics` object attempts to create a `DIBSection` or any other internal allocation of memory for use with the `Bitmap` it will call `GetHalftonePalette`. Which locks. – Simon Whitehead Aug 18 '13 at 11:44
  • Logically, wouldn't that mean it would only lock on the `Graphics` constructor, and not on each call to `DrawEllipse`? – Rotem Aug 18 '13 at 11:54
  • @SimonWhitehead post some disasm-ed code, it is interesting to see why is that. I am using Graphics inside directshow graphs, so far I'm without any big penalties... – Daniel Mošmondor Aug 18 '13 at 12:08