在pthreads线程中运行Metal代码时触发objc_release的SIGSEGV错误
问题描述
在线程中运行Metal代码时出现崩溃:
- 当
USE_THREAD和USE_AUTORELEASEPOOL均设为0时,代码运行正常 - 将其中任意一个设为1时,会在
objc_release处触发SIGSEGV错误
崩溃栈信息:
* 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
核心疑问:
- 理解显式使用autoreleasepool时手动释放对象会导致重复释放崩溃,但不解为何单独开启pthreads线程也会触发同样问题
- 是否与pthreads机制相关?是否存在未注意到的隐藏autoreleasepool?
- 已知使用autoreleasepool且不手动释放可正常运行,但想了解问题背后的原理
相关代码:
// 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; }
问题分析与解决
核心原因:重复释放对象
你的代码违反了Objective-C内存管理规则,导致同一对象被释放两次:
- 对于
+[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:...、+[MTLRenderPassDescriptor renderPassDescriptor]这类类方法创建的对象,默认会被加入当前线程的AutoreleasePool - 你手动调用了
[texture_descriptor release]和[render_pass_descriptor release],提前销毁了这些对象;当AutoreleasePool销毁时(无论是显式@autoreleasepool块结束,还是线程退出时系统销毁的隐藏池),会再次对这些对象执行release,触发SIGSEGV
为何pthread线程单独开启也会崩溃?
系统会为每个pthread线程自动创建一个默认AutoreleasePool:
- 线程创建后自动生成,线程退出时自动销毁
- 当你在pthread线程中手动释放了已加入默认池的对象,线程退出时池销毁就会触发重复释放,和显式使用
@autoreleasepool的情况本质完全一致
关于"隐藏AutoreleasePool"
确实存在两种系统自动维护的隐藏池:
- 主线程默认池:主线程启动时创建,每次事件循环结束时自动清空
- pthread子线程默认池:线程创建时生成,线程退出时销毁
你之前在主线程且不开启显式池时运行正常,是因为主线程默认池在程序退出时才销毁,此时程序已接近结束,未触发明显崩溃,但这属于未定义行为,并非真正安全。
修复方案
严格遵循Objective-C内存管理的"谁创建谁释放"规则:
- 对于通过
new/alloc/copy/mutableCopy创建的对象(如[device newCommandQueue]),需要手动调用release - 对于通过其他类方法/实例方法创建的对象(如
texture_descriptor、command_buffer),默认被加入AutoreleasePool,不要手动调用release,交给池自动管理
修改后的关键代码片段:
NSArray<id<MTLDevice>> * devices = MTLCopyAllDevices(); // copy开头,需手动release id<MTLDevice> device = devices[0]; id<MTLCommandQueue> command_queue = [device newCommandQueue]; // new开头,需手动release MTLTextureDescriptor * texture_descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:640 height:480 mipmapped:NO]; texture_descriptor.usage = MTLTextureUsageRenderTarget; id<MTLTexture> texture = [device newTextureWithDescriptor:texture_descriptor]; // new开头,需手动release // 移除[texture_descriptor release]; id<MTLCommandBuffer> command_buffer = [command_queue commandBuffer]; // 自动加入AutoreleasePool,无需手动release MTLRenderPassDescriptor * render_pass_descriptor = [MTLRenderPassDescriptor renderPassDescriptor]; // 自动加入池,无需手动release render_pass_descriptor.colorAttachments[0].texture = texture; id<MTLRenderCommandEncoder> render_command_encoder = [command_buffer renderCommandEncoderWithDescriptor:render_pass_descriptor]; // 自动加入池,无需手动release // 移除[render_pass_descriptor release]; [render_command_encoder endEncoding]; // 移除[render_command_encoder release]; [command_buffer commit]; [command_buffer waitUntilCompleted]; // 移除[command_buffer release]; [texture release]; texture = nil; [command_queue release]; command_queue = nil; [devices release]; // 补充手动release,因为是copy开头
内容的提问来源于stack exchange,提问作者user925885
相关产品推荐
相关产品推荐

