dispatch_apply 留下一个线程 "hanging"

dispatch_apply leaves one thread "hanging"

我正在按照 Apple 的并发编程指南试验多线程。替换 for 循环的多线程函数 (dispatch_apply) 看起来很简单,并且可以通过简单的 printf 语句正常工作。但是,如果块调用更 cpu 密集的计算,程序永远不会结束或执行超过 dispatch_apply,并且一个线程(主线程?)似乎停滞在 100%。

#import <Foundation/Foundation.h>

#define THREAD_COUNT 16

unsigned long long longest = 0;
unsigned long long highest = 0;

void three_n_plus_one(unsigned long step);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_apply(THREAD_COUNT, queue, ^(size_t i) {
            three_n_plus_one(i);
        });
    }
    return 0;
}

void three_n_plus_one(unsigned long step) {
    
    unsigned long long start = step;
    unsigned long long end = 1000000;
    unsigned long long identifier = 0;

    while (start <= end) {

        unsigned long long current = start;
        unsigned long long sequence = 1;

        while (current != 1) {
        
            sequence += 1;
            if(current % 2 == 0)
                current = current / 2;
            else {
                current = (current * 3) + 1;
            }

            if (current > highest) highest = current;
        }

        if (sequence > longest) {
            longest = sequence;
            identifier = start;
            printf("thread %lu, number %llu with %llu steps to one\n", step, identifier, longest);
        }

        start += (unsigned long long)THREAD_COUNT;
    }
}

不过,循环似乎已经结束了。据我了解,这应该是相当简单的,但我仍然对我在这里做错了什么一无所知。

您调用的 step 是循环的索引。它在您的代码中从 0 到 THREAD_COUNT-1。由于您将 start 指定为 step,这意味着您的第一次迭代尝试计算从零开始的 Colatz 序列。计算 0/2 == 0 无限循环也是如此。

你想写的是:

unsigned long long start = step + 1;

将您的块大小称为“THREAD_COUNT”具有误导性。问题不在于创建了多少线程(可能不创建线程;这取决于系统)。问题是把工作分成多少块。

请注意,在没有同步的情况下在多个线程上读取和写入 longesthighest 是未定义的行为,因此这段代码可能会做出令人惊讶的事情(特别是在优化时)。不要假设它仅限于在最长和最高中获得错误的值。允许优化器假定在它运行时没有其他线程触及这些值,并且可以基于此显着地重新排列代码。但这不是这个特定问题的原因。

(+1) 一样,一个线程“挂起”的原因是因为在提供零时出现无限循环,而不是因为 dispatch_apply 有任何问题(称为 concurrentPerform 在 Swift).

但是,更微妙的问题(以及使并发代码不那么“直截了当”的原因)是此代码不是线程安全的。存在“数据竞赛”。您正在从多个线程同时访问和更改 highestlongest。我鼓励在测试并发代码时使用 Thread Sanitizer (TSAN),它非常适合识别这些数据竞争。

例如,编辑您的方案并暂时打开线程清理程序:

然后,当您 运行 时,它会警告您数据竞争:

您可以通过同步对这些变量的访问来修复这些竞争。锁是一种简单的机制。如果可以的话,我也会避免在内部 while 循环中进行同步。在这种情况下,您甚至可以将其从外部 while 循环中删除。在这种情况下,我可能会建议使用局部变量来跟踪当前“最长”序列、“最高”值以及该最高值的标识符,然后仅在完成后才与共享变量进行比较和更新,在两个循环之外。

例如也许:

- (void)three_n_plus_one:(unsigned long) step {
    unsigned long long start = step + 1;
    unsigned long long end = 1000000;
    unsigned long long tempHighest = start;
    unsigned long long tempLongest = 1;
    unsigned long long tempIdentifier = start;

    while (start <= end) {
        unsigned long long current = start;
        unsigned long long sequence = 1;

        while (current != 1) {
            sequence += 1;

            if (current % 2 == 0)
                current = current / 2;
            else {
                current = (current * 3) + 1;
            }

            if (current > tempHighest) tempHighest = current;
        }

        if (sequence > tempLongest) {
            tempLongest = sequence;
            tempIdentifier = start;
        }

        start += (unsigned long long)THREAD_COUNT;
    }

    [lock lock];   // synchronize updating of shared memory

    if (tempHighest > highest) {
        highest = tempHighest;
    }

    if (tempLongest > longest) {
        longest = tempLongest;
        identifier = tempIdentifier;
    }

    [lock unlock];
}

我使用了 NSLock,但使用任何你想要的同步机制。但想法是(a)确保将所有交互与共享内存同步; (b) 将必要的同步次数减少到最低限度。 (在这种情况下,天真的同步方法比上面的方法慢 200 倍,这将同步次数降至最低。)

完成数据争用修复后,您可以关闭 TSAN。