0

I'm running some Metal code in a thread, but encountering some issues I don't fully understand. Running the following code with USE_THREAD 0 and USE_AUTORELEASEPOOL 0 works fine but setting either one to 1 results in a SIGSEGV in objc_release:

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x20)
  * frame #0: 0x00007fff2020d4af libobjc.A.dylib`objc_release + 31
    frame #1: 0x00007fff2022b20f libobjc.A.dylib`AutoreleasePoolPage::releaseUntil(objc_object**) + 167
    frame #2: 0x00007fff2020de30 libobjc.A.dylib`objc_autoreleasePoolPop + 161
    frame #3: 0x0000000100003d60 a.out`render(void*) + 896
    frame #4: 0x0000000100003dd8 a.out`main + 24
    frame #5: 0x00007fff20388f3d libdyld.dylib`start + 1
    frame #6: 0x00007fff20388f3d libdyld.dylib`start + 1

Using the autoreleasepool I can understand since the objects are already released (since release is called manually on them), but why does the same issue occur when the code is running inside a thread? Is this related to pthreads specifically? Is there a "hidden" autoreleasepool somewhere I am missing?

I understand using an autoreleasepool and not releasing manually will achieve the same result but I am trying to understand what is going on here.

// clang++ main.mm -lobjc -framework Metal
#define USE_THREAD 0
#define USE_AUTORELEASEPOOL 1

#import <Metal/Metal.h>

void * render(void *) {
    #if USE_AUTORELEASEPOOL
    @autoreleasepool {
    #else
    {
    #endif
        NSArray<id<MTLDevice>> * devices = MTLCopyAllDevices();
        id<MTLDevice> device = devices[0];

        id<MTLCommandQueue> command_queue = [device newCommandQueue];

        MTLTextureDescriptor * texture_descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:640 height:480 mipmapped:NO];
        texture_descriptor.usage = MTLTextureUsageRenderTarget;

        id<MTLTexture> texture = [device newTextureWithDescriptor:texture_descriptor];

        [texture_descriptor release];
        texture_descriptor = NULL;

        id<MTLCommandBuffer> command_buffer = [command_queue commandBuffer];

        MTLRenderPassDescriptor * render_pass_descriptor = [MTLRenderPassDescriptor renderPassDescriptor];
        render_pass_descriptor.colorAttachments[0].texture = texture;

        id<MTLRenderCommandEncoder> render_command_encoder = [command_buffer renderCommandEncoderWithDescriptor:render_pass_descriptor];

        [render_pass_descriptor release];
        render_pass_descriptor = NULL;

        [render_command_encoder endEncoding];

        [render_command_encoder release];
        render_command_encoder = nil;

        [command_buffer commit];
        [command_buffer waitUntilCompleted];

        [command_buffer release];
        command_buffer = nil;

        [texture release];
        texture = nil;

        [command_queue release];
        command_queue = nil;
    }

    return 0;
}

#include <pthread.h>

int main() {
    #if USE_THREAD
        pthread_t thread;
        pthread_create(&thread, NULL, render, NULL);

        pthread_join(thread, NULL);
    #else
        render(NULL);
    #endif

    return 0;
}
  • 1
    Is there a reason why you don't use ARC (automatic reference counting)? – Martin R Nov 14 '22 at 14:27
  • 1
    You release some objects (e.g. texture_descriptor) which you don't “own” – compare https://stackoverflow.com/a/16934794/1187415. But with ARC everything becomes so much simpler, and less error-prone. – Martin R Nov 14 '22 at 14:51
  • 1
    Does this answer your question? [does NSThread create autoreleasepool automatically now?](https://stackoverflow.com/questions/24952549/does-nsthread-create-autoreleasepool-automatically-now) – Willeke Nov 14 '22 at 14:56
  • @Willeke: I think that is a different issue. Here the problem is just wrong (manual) reference counting. – Martin R Nov 14 '22 at 15:03
  • That does indeed seem to be the issue. Only releasing `texture` and `command_queue` (the two objects created with a `new...` function) resolves the issue. But suppose we do not have an autoreleasepool in this case, when would e.g. `MTLTextureDescriptor` be released, especially if I would not create a texture using it? And who would be responsible for releasing it? Since there is no autoreleasepool or ARC, I don't see how it would be released. –  Nov 14 '22 at 15:07
  • 1
    There are various situations where autorelease pools are created: In iOS or macOS GUI applications they are created in the main event loop. Apparently pthreads also create an autorelease pool (see the link in @Willeke's comment). – In a simple command line app there is no default autorelease pool. If you don't create one then those objects are never released. – Martin R Nov 14 '22 at 15:12

1 Answers1

0

I'm not sure if Apple has ever updated their documentation in regards to this behaviour, but approximately since macOS 10.7, all POSIX threads were populated with autorelease pool block automatically. You can clearly see it from dealloc of autoreleased objects:

#import <Foundation/Foundation.h>
#import <pthread.h>

@interface TDWObject : NSObject

@end

@implementation TDWObject

- (void)dealloc {
    [super dealloc];
    NSLog(@"%@", [NSThread callStackSymbols]);
}

@end


void *run(void *objPtr) {
    TDWObject *obj = (__bridge TDWObject *)objPtr;
    [obj autorelease];
    return NULL;
}


int main() {
    pthread_t thread;
    TDWObject *obj = [TDWObject new];
    pthread_create(&thread, NULL, run, (__bridge void *)obj);
    pthread_join(thread, NULL);
    return 0;
}

This code (when compiled with MRC) will print the following stacktrace:

-[TDWObject dealloc] + 83
_ZN19AutoreleasePoolPage12releaseUntilEPP11objc_object + 168
objc_autoreleasePoolPop + 227
_ZN20objc_tls_direct_baseIP19AutoreleasePoolPageL7tls_key3ENS0_14HotPageDeallocEE5dtor_EPv + 140
_pthread_tsd_cleanup + 607
_pthread_exit + 70
_pthread_start + 136
thread_start + 15

objc_autoreleasePoolPop function is exactly what gets called in the end of autorelease pool blocks nowadays.

The Dreams Wind
  • 8,416
  • 2
  • 19
  • 49