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;
}
CodePudding user response:
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.