Multiple threads to draw in NSView

≯℡__Kan透↙ 提交于 2019-12-05 00:21:51


In my code, I subclassed NSView and in its drawRect method, I am spawning three threads to perform the drawing.

    [[self window] setAllowsConcurrentViewDrawing:YES];
    [self setCanDrawConcurrently:YES];

    [NSThread detachNewThreadSelector:@selector(DrawText) toTarget:self withObject:nil];
    [NSThread detachNewThreadSelector:@selector(DrawRectangle) toTarget:self withObject:nil];
    [NSThread detachNewThreadSelector:@selector(DrawGradient) toTarget:self withObject:nil];

    //Wherease these functions DrawText, DrawRectangle and DrawGradient performs their task as suggested by name.

    //In DrawText, DrawRectangle, and DrawGradient lockFocus and unlockFocus is being
    // used for multithreaded drawing.


When I run the same program from Xcode, it is running fine. Output is shown below.

But when I run it from the outside, there is problem and output is shown below.

First, I would like to know is it right way to draw from a secondary thread? Or what is another way to draw from a secondary thread?

What is the reason behind this problem?


Ken Aspeslagh is somewhat incorrect about drawing from secondary threads (he is correct it is generally a bad idea). From what I can see of your code you don't have a good use case for drawing on a secondary thread. Can you explain why you want to do this?

you yourself have already discovered setCanDrawConcurrently: which explicitly talks of invoking drawRect: from a background thread. Note that the views window must have allowsConcurrentViewDrawing set to YES for this to work (it is the default).

Apples own Cocoa Drawing Guide has a section on drawing from secondary threads. I have highlighted some portions I think are relevant to you.

The Application Kit maintains a unique graphics context for each window and thread combination. Because each thread has its own graphics context object for a given window, it is possible to use secondary threads to draw to that window. There are some caveats, however.

During the normal update cycle for windows, all drawing requests are sent to your application’s main thread for processing. The normal update cycle happens when a user event triggers a change in your user interface. In this situation, you would call the setNeedsDisplay: or setNeedsDisplayInRect: method (or the display family of methods) from your application’s main thread to invalidate the portions of your view that require redrawing. You should not call these methods from any secondary threads.

If you want to update a window or view from a secondary thread, you must manually lock focus on the window or view and initiate drawing yourself. Locking focus configures the drawing environment for that window's graphics context. Once locked, you can configure the drawing environment, issue your drawing commands as usual, and then flush the contents of the graphics context to the window buffer.

In order to draw regularly on a secondary thread, you must notify the thread yourself. The simplest way to send regular notifications is using an NSTimer or NSAnimation object. For more information on how to animate content, see “Advanced Drawing Techniques.”

The Cocoa Threading Programming Guide also says this:

If you want to use a thread to draw to a view, bracket all drawing code between the lockFocusIfCanDraw and unlockFocus methods of NSView

Just an aside, GCD block invocation is probably a much nicer method for performing small sets of operations in the background than NSThread.

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    // you can put each of these calls in their own queue if you want
    [self DrawText];
    [self DrawRectangle];
    [self DrawGradient];

However, that likely has nothing to do with your problem; I mention it only because I think it will serve you better to use GCD queues.


You should only be drawing to the screen from the main thread.

Edit: It is apparently really complicated so you're better off drawing to the screen from the main thread. ;)

If you need to render something that takes too much time to avoid blocking the main thread, consider using a thread to do the drawing to an offscreen context, and then copy that context to the screen on the main thread.


I read about NSGraphicsContext Restriction at Thread guide.

Here, I found the following line:

If you do any drawing from a secondary thread, you must flush your drawing calls manually. Cocoa does not automatically update views with content drawn from secondary threads, so you need to call the flushGraphics method of NSGraphicsContext when you finish your drawing. If your application draws content from the main thread only, you do not need to flush your drawing calls.

After calling flushGraphics, it works fine.