大型 POD 结构复制到堆栈在工作线程中崩溃

问题描述 投票:0回答:2

混合了 Obj-C++ 和 C++ 的 iOS 项目。我有一个大小约为 1 MB 的 POD 结构。它有一个全球实例。如果我在工作线程上调用的函数中创建相同的本地实例,则复制操作会在模拟器上的调试构建中崩溃(当从工作线程调用时)。发布版本不会崩溃。

这闻起来像堆栈大小用完了。

有问题的工作线程不是手动创建的 - 是一个

NSOperationQueue
worker.

问题是双重的:

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

重现:

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

S gs;

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

好吧,我知道重写

NSOperationQueue
来增加堆栈。也就是说 - **编译器 (Clang) 肯定有某种解决方法。我会检查反汇编以防万一,但相同的代码不会在发布版本中崩溃。怎么不搞这个案子?顺便说一句,在 Android 上检查了相同的代码,看到了同样的崩溃。


PS:激励案例不是来自我的代码,而是来自第三方算法库。原始库并不需要深拷贝;局部变量永远不会写入,只能读取;引用(甚至是 const 引用)也可以。我的猜测是作者想要参考但搞砸了。在发布版本中,编译器优化了一个引用,因此没有崩溃。在调试版本中,它没有。

ios objective-c objective-c++
2个回答
2
投票

我不知道这个文档是如何更新的,但是根据 Apple 除非在线程创建期间另外配置,否则非主线程的堆栈大小最高为 512kiB。

我很难找到将这种数据结构存储在堆栈上的充分理由,特别是对于 (Obj-)C++,您可以轻松地将其包装在

std::unique_ptr
之类的东西中,它可以自动管理堆分配和释放。 (或者实际上任何其他基于 RAII 的抽象,或者如果你愿意的话,甚至可以将它作为一个 ivar 存储在支持 ARC 的 Objective-C 类中。)

选择非常大的堆栈大小的一个缺点是该内存可能会一直驻留但在线程终止之前未被使用,特别是在 iOS 上,该内存甚至不会交换到磁盘。如果您显式启动线程并在完成巨型堆栈需求算法后将其关闭,这很好。但是,如果您在池线程上运行一次性作业,您现在实际上已经泄漏了 1MB 的内存。也许这是我的嵌入式开发人员(或者我记得当 iPhone 只有 128MB RAM 时)但我不想编写那样的代码。 (或者有人可以提出低内存警告机制清除未使用堆栈空间的证据吗?)


2
投票

在底层,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(

GCD
NSOperation
)都没有公开其线程池的分配部分,更不用说
NSThread
不允许您明确指定自己的
pthread
用于底层执行。如果你想依赖那些 API,你将不得不“人工”实现它。

您可以在要点中找到此类操作类的示例实现。这是如何使用它的最小示例:

#import <Foundation/Foundation.h>
#import <type_traits>
#import "TDWOpeartion.h"

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSOperationQueue *queue = [NSOperationQueue new];
        constexpr auto elementsNum = static_cast<std::size_t>(16);

        [queue addOperations:@[
            [[TDWOperation alloc] initWithExecutionBlock:^{
                using elemType = std::remove_all_extents_t<decltype(S::s)>;
                S arr[elementsNum];
                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");

            } stackSize:sizeof(S) * elementsNum * 2]
        ] waitUntilFinished:YES];
    }
    return 0;
}
© www.soinside.com 2019 - 2024. All rights reserved.