You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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"

确实存在两种系统自动维护的隐藏池:

  1. 主线程默认池:主线程启动时创建,每次事件循环结束时自动清空
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 13:01:41