ios 在工作线程中将较大POD结构复制到堆栈时崩溃

uqjltbpv  于 2023-02-06  发布在  iOS
关注(0)|答案(2)|浏览(119)

一个混合了Obj-C和C的iOS项目。我有一个大约1 MB大小的POD结构。它有一个全局示例。如果我在一个工作线程上调用的函数中创建一个相同的本地示例,复制操作在模拟器上的调试构建中崩溃(从工作线程调用时)。发布构建不会崩溃。
这感觉像是用完了堆栈大小。
有问题的工作线程不是手动创建的-是NSOperationQueue工作线程。
问题有两个方面:

  • 为什么自动堆栈增长失败?
  • 如何增加NSOperationQueue线程上的堆栈大小?

录音如下:

struct S
{
     char s[1024*1024];
};

S gs;

-(void)f
{
    S ls;
    ls = gs; //Crash!
}

好的,我看到了重写NSOperationQueue来增加堆栈。也就是说-**编译器(Clang)肯定有某种解决方案。我将检查反汇编以防万一,但相同的代码在发布版本中不会崩溃。为什么它在这种情况下不起作用呢?OBTW,在Android上检查了相同的代码,看到了相同的崩溃。

3phpmpom

3phpmpom1#

我不知道这个文档有多新,但是根据苹果公司的说法,除非在线程创建过程中另有配置,否则非主线程的栈顶大小为512 kB。
我很难找到将这样的数据结构存储在堆栈上的好理由,特别是在(Obj-)C++中,您可以轻松地将其 Package 在类似std::unique_ptr的东西中,它可以自动管理堆分配和释放。(或者任何其他基于RAII的抽象,或者甚至将其存储为支持ARC的Objective-C类中的一个ivar,如果您愿意的话)。
选择非常大的堆栈大小的一个缺点是,这些内存可能会一直驻留,但在线程终止之前不会被使用,特别是在iOS上,这些内存甚至不会被交换到磁盘。如果你显式地启动一个线程,并在完成需要巨大堆栈的算法后关闭它,这是很好的。但如果你在一个池线程上运行一次性作业,你现在实际上已经泄漏了1 MB的内存。也许是因为我是嵌入式开发人员(或者我记得iPhone只有128 MB内存的时候),但我不想写这样的代码。(或者有人能拿出证据证明低内存警告机制会清除未使用的堆栈空间吗?)

zaq34kh6

zaq34kh62#

Cocoa的线程机制使用unix POSIX线程,其堆栈大小遵循以下规则:

  • 默认堆栈大小,如果没有明确指定(例如,在macOS中,您可以通过运行ulimit -s命令找到此值,对于我的机器是8192 KiB,但对于iOS,很可能要小几倍)
  • 任意堆栈大小(如果在创建线程期间指定)

回答你的第一个问题:

  • 为什么自动堆栈增长失败?

它"失败"是因为它不允许增长到超过为给定线程分配的大小。在这种情况下更有趣的问题是-为什么它在发布版本中没有失败?坦白地说,我在这里没有答案。我假设它很可能与优化有关,在优化中允许编译器绕过某些内存流例程或完全丢弃某些代码部分。
对于第二个问题:

  • 如何增加NSOperationQueue线程上的堆栈大小?

应用程序的主线程始终具有默认的系统堆栈大小,并且只能在macOS(或root iOS设备)中使用ulimit进行更改(大小以KiB为单位):

# Sets 32 MiB default stack size to a thread
% ulimit -s 32768

iOS和macOS下的所有其他线程(据我所知)都有明确指定的大小,它等于512 KiB。您必须以某种方式将堆栈大小转发给pthread_create(3)函数,如下所示:

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

struct S {
    char s[1024 * 1024];
};

void *func(void *context) {
    // 16 MiB stack variable
    S s[16];
    NSLog(@"Working thread is finished");
    auto* result = new int{};
    return result;
}

int main(int argc, const char * argv[]) {
    pthread_attr_t attrs;
    auto s = pthread_attr_init(&attrs);
    // Allocates 32 MiB stack size
    s = pthread_attr_setstacksize(&attrs, 1024 * 1024 * 32);

    pthread_t thread;
    s = pthread_create(&thread, &attrs, &func, nullptr);
    s = pthread_attr_destroy(&attrs);
    void* result;
    s = pthread_join(thread, &result);
    if (s) {
        NSLog(@"Error code: %d", s);
    } else {
        NSLog(@"Main is finished with result: %d", *(int *)result);
        delete (int *)result;
    }

    @autoreleasepool {
    }
    return 0;
}

不幸的是,队列API(GCDNSOperation)都没有公开线程池的分配部分,更不用说NSThread不允许您显式地指定自己的pthread用于底层执行。如果您想依赖这些API,您将不得不"人工"实现它。

具有任意堆栈大小线程的NSOperation子类示例

这样一个类的接口可能看起来像这样(假设线程的堆栈大小是恒定的,并且不应该是一个注入的依赖项):

//  TDWOpeartion.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void(^TDWOperationBlock)(void);

__attribute__((__objc_direct_members__))
@interface TDWOperation: NSOperation

@property (copy, nonatomic, readonly) TDWOperationBlock executionBlock;
- (instancetype)initWithExecutionBlock:(TDWOperationBlock)block NS_DESIGNATED_INITIALIZER;

@end

NS_ASSUME_NONNULL_END

实现文件:

//  TDWOpeartion.mm

#import "TDWOpeartion.h"
#import <pthread.h>

#define EXECUTE_WITH_ERROR(codeVar, execution) if((codeVar = execution)) {\
    NSLog(@"Failed to execute " #execution " with error code: %d", codeVar);\
    return;\
}

NS_ASSUME_NONNULL_BEGIN

__attribute__((__objc_direct_members__))
@interface TDWOperation ()

@property (assign, getter=tdw_p_isThreadStarted) BOOL tdw_p_threadStarted;
@property (assign, nonatomic) pthread_t tdw_p_underlyingThread;
@property (strong, nonatomic, readonly) dispatch_queue_t tdw_p_productsSyncQueue;

@end

NS_ASSUME_NONNULL_END

@implementation TDWOperation

@synthesize tdw_p_threadStarted = _tdw_p_threadStarted;

#pragma mark Lifecycle

- (instancetype)initWithExecutionBlock:(TDWOperationBlock)block {
    if (self = [super init]) {
        _executionBlock = block;
        _tdw_p_threadStarted = NO;
        _tdw_p_productsSyncQueue = dispatch_queue_create("the.dreams.wind.property_access.TDWOperation.isThreadStarted",
                                                   DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (instancetype)init {
    return [self initWithExecutionBlock:^{}];
}

#pragma mark NSOperation

- (void)main {
    pthread_attr_t attrs;
    int statusCode;
    EXECUTE_WITH_ERROR(statusCode, pthread_attr_init(&attrs))
    // Allocates 32 MiB stack size
    EXECUTE_WITH_ERROR(statusCode, pthread_attr_setstacksize(&attrs, 1024 * 1024 * 32))

    pthread_t thread;
    EXECUTE_WITH_ERROR(statusCode, pthread_create(&thread, &attrs, &tdw_p_runExecutionBlock, (__bridge_retained void *)self))
    EXECUTE_WITH_ERROR(statusCode, pthread_attr_destroy(&attrs))

    void* result = nullptr;
    if (!self.cancelled) {
        self.tdw_p_threadStarted = YES;
        EXECUTE_WITH_ERROR(statusCode, pthread_join(thread, &result));
        self.tdw_p_threadStarted = NO;
    }
    NSLog(@"Main is finished with result: %d", *(int *)result);
    delete (int *)result;
}

#pragma mark Properties

- (void)setExecutionBlock:(TDWOperationBlock)executionBlock {
    if (self.tdw_p_isThreadStarted) {
        [NSException raise:NSInternalInconsistencyException
                    format:@"Cannot change execution block when execution is already started"];
    }
    _executionBlock = executionBlock;
}

- (BOOL)tdw_p_isThreadStarted {
    __block BOOL result;
    dispatch_sync(_tdw_p_productsSyncQueue, ^{
        result = _tdw_p_threadStarted;
    });
    return result;
}

- (void)setTdw_p_threadStarted:(BOOL)threadStarted {
    dispatch_barrier_async(_tdw_p_productsSyncQueue, ^{
        self->_tdw_p_threadStarted = threadStarted;
    });
}

#pragma mark Private

void *tdw_p_runExecutionBlock(void *args) {
    TDWOperation *self = (__bridge_transfer TDWOperation *)args;
    if (self.executionBlock) {
        self.executionBlock();
    }
    int *result = new int{};
    return result;
}

@end

现在,您可以像使用常规NSOperation示例一样使用它:

#import "TDWOpeartion.h"
#include <type_traits>

struct S {
    unsigned char s[1024 * 1024];
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSOperationQueue *queue = [NSOperationQueue new];
        [queue addOperations:@[
            [[TDWOperation alloc] initWithExecutionBlock:^{
                using elemType = std::remove_all_extents_t<decltype(S::s)>;
                S arr[16];
                auto numOfElems = sizeof(S::s) / sizeof(elemType);
                for(decltype(numOfElems) i = 0; i < numOfElems; ++i) {
                    for (auto val: arr) {
                        val.s[i] = i % sizeof(elemType);
                    }
                }
                NSLog(@"Sixteen MiB were initialized");

            }]
        ] waitUntilFinished:YES];
    }
    return 0;
}

相关问题