如何在不达到全局 GCD 限制的情况下并行化许多(100 多个)任务?

How to parallelize many (100+) tasks without hitting global GCD limit?

问题

当在后台延迟加载 100 多个图标的列表时,我达到了 GCD 线程限制(64 个线程),这导致我的应用程序在主线程上冻结并出现 semaphore_wait_trap。我想重组我的线程代码以防止这种情况发生,同时仍然异步加载图标以防止 UI 阻塞。

上下文

我的应用加载了一个带有 SVG 图标的屏幕。金额平均在 10-200 之间。使用本地 SVG 图像或远程 SVG 图像(如果它有自定义图标)绘制图标,然后对它们进行 post 处理以获得最终图像结果。

因为这需要一些时间,而且它们对用户来说并不重要,所以我想在后台加载并 post- 处理它们,所以它们会随着时间的推移弹出。对于每个图标,我使用以下内容:

dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
    //code to be executed in the background
    SVGKImage *iconImage = [Settings getIconImage:location];
    dispatch_async(dispatch_get_main_queue(), ^{
        //code to be executed on the main thread when background task is finished
        if (iconImage) {
            [iconImgView setImage:iconImage.UIImage];
        }
    });
});

getIconImage 方法处理基本 SVG 的初始加载,如果是本地加载,它会与 [NSInputStream inputStreamWithFileAtPath:path] 同步读取,如果远程加载,则 [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&errorWithNSData] 会同步读取。这一切都是同步发生的。

然后有一些 post 重新着色 SVG 的处理,在它返回并放入主线程的 UIImageView 之前。

问题:

有没有一种方法可以构建我的代码以允许并行后台加载但防止由于线程太多而导致的死锁?

解决方案编辑:

_iconOperationQueue = [[NSOperationQueue alloc]init];
_iconOperationQueue.maxConcurrentOperationCount = 8;    

// Code will be executed on the background
[_iconOperationQueue addOperationWithBlock:^{
    // I/O code
    SVGKImage *baseIcon = [Settings getIconBaseSVG:location];

    // CPU-only code
    SVGKImage *iconImage = [Settings getIconImage:location withBaseSVG:baseIcon];
    UIImage *svgImage = iconImage.UIImage; // Converting SVGKImage to UIImage is expensive, so don't do this on the main thread
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // Code to be executed on the main thread when background task is finished
        if (svgImage) {
            [iconImgView setImage:svgImage];
        }
    }];
}];

不要直接将 GCD 与并发队列一起使用,而是使用 NSOperationQueue。将其 maxConcurrentOperationCount 设置为合理的值,例如 4 或 8。

如果可以,您还应该将I/O与纯计算分开。使用 I/O 的宽度限制操作队列。可以使用不受限制的操作队列或纯 GCD 的纯计算。

原因是I/O块。 GCD 检测到系统空闲并启动另一个工作线程并从队列中启动另一个任务。这也会在 I/O 中阻塞,因此它会继续执行更多操作,直到达到极限。然后,I/O 开始完成,任务解除阻塞。现在您已经超额订阅了系统资源(即 CPU),因为运行中的任务多于核心,突然间它们实际上正在使用 CPU 而不是被 I/O.[=12= 阻塞]

纯计算任务不会引发此问题,因为 GCD 发现系统实际上很忙,并且在较早的任务完成之前不会使更多任务出队。

你可以通过使用像这样的信号量来保持 GCD 运行 整个操作在后台进行,否则等待信号量将停止 UI:

dispatch_semaphore_t throttleSemaphore = dispatch_semaphore_create(8);
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for /* Loop through your images */ {
    dispatch_semaphore_wait(throttleSemaphore, DISPATCH_TIME_FOREVER);
    dispatch_async(concurrentQueue, ^{
        //code to be executed in the background
        SVGKImage *iconImage = [Settings getIconImage:location];
        dispatch_async(dispatch_get_main_queue(), ^{
            //code to be executed on the main thread when background task is finished
            if (iconImage) {
                [iconImgView setImage:iconImage.UIImage];
            }
            dispatch_semaphore_signal(throttleSemaphore);
        });
    });
}