为什么这个多线程程序工作(而不是崩溃)?

时间:2013-10-08 03:54:12

标签: c multithreading pthreads

根据我的理解,如果两个或多个线程同时尝试访问同一个内存块,它应该“抱怨”,至少可以说。

我正在为一个计算回文的课程编写一个程序(在列表中显示向后和向前的单词也算在内)。在我的多线程解决方案中,我生成26个线程来处理字母表中的每个字母

int error = pthread_create(&threads[i], NULL, computePalindromes, args);

计算回文只是贯穿单词子列表:

void * computePalindromes(void * arguments) {
    struct arg_struct *args = (struct arg_struct *)arguments;
    int i;

    for (i = args->start; i < args->end; i++) {
        if (quickFind(getReverse(array[i]), 0, size - 1)) {
            printf("%s\n", array[i]);
        }
    }

    return NULL;
}

现在,应该导致程序停止的段。我修改了quickSelect来查找列表中的反向字。

int quickFind(char * string, int lower_bound, int upper_bound) {
    int index = ((upper_bound + lower_bound) / 2);
    //sem_wait(&semaphores[index]);
    if (upper_bound <= lower_bound) return (strcmp(string, array[index]) == 0);

    if (strcmp(string, array[index]) > 0) {
        //sem_post(&semaphores[index]);
        return quickFind(string, (index + 1), upper_bound);
    } else if (strcmp(string, array[index]) < 0) {
        //sem_post(&semaphores[index]);
        return quickFind(string, lower_bound, (index - 1)); 
    } else return 1;
}

你可以看到我注释掉了一堆sem_post / waits。

2 个答案:

答案 0 :(得分:5)

两个线程同时访问同一个内存没有任何问题,只要它们只是读取内存而不是它。您对数据执行的操作都没有实际修改它,因此线程并行执行所有这些操作是完全安全的。

希望这有帮助!

答案 1 :(得分:4)

只需添加现有的优秀答案和评论,就可以看到单核CPU中发生的情况。

现在很明显,在单核CPU中,一次只能运行一个线程/进程。操作系统调度程序,它只是另一个线程(非常特殊的...),选择在接下来的15ms左右运行的内容。但无论运行什么,任何一点都可以单独访问内存。因此,虽然在实践中可能感觉有很多同时运行,但事实并非如此。这只是很多线程在很短的时间内一次运行一次。

但是我们现在都拥有多核CPU。正在发生的事情是核心都能够解决内存问题(无论是哪种方式 - 像Netburst,QPI和Hypertransport这样的事情都会让事情变得复杂)。但是,在电子设备的深处,不可能有两个或多个内核同时访问同一个内存芯片。这样做会开始破坏,所以要特别注意确保一次只有一个核心可以访问任何一块物理内存。因此,存储器访问以非常基本的方式在电子级别进行序列化。

“啊哈”我听到你说,“那会让一切变得非常慢!”而且你是对的,所以硬件人员用缓存来解决这个问题,以帮助改进。

因此结果是,如果不同核心上的两个线程尝试写入同一地址,则内存硬件会强制它们轮流使用。如果访问是真正同步的话,那么首先是哪一个先行,这只是纯粹的运气。你在软件中不知道的是这个仲裁是如何完成的,所以你不能依赖它。在一台32位总线宽度的计算机上,您可以在一次不间断的操作中完成int32赋值,但可能不是int64赋值。但是在64位总线宽度上,您可能会发现int64分配是不间断完成的。

所以如果你在一个线程中有一个语句,比如a = b + c,并且a = 10在另一个线程中并且它们在完全相同的时间运行(彼此小于0.3纳秒,相应地松散地对应于3GHz时钟你不知道一个是10,b + c,还是两个可怕的混合(因为你不知道硬件如何处理两个核心的写入,你不知道如何)无论如何,许多硬件内存事务实际涉及“a =”,并且您不知道调度程序是否已经部分抢占了一个线程!)。无论发生什么事情,电子产品都不会抱怨(为什么要这样?它所关心的一切都不会引起火灾和爆炸,并且硬件的工作并不是猜测软件想要做什么),所以操作系统和你的程序没有注意到事情已经错了。

这就是为什么你需要信号量来序列化对'a'的访问,这样它最终至少要么是b + c或10,你要使用其他程序流程控制来确保其正确的一个是什么结束在一个。

没有信号量你就有机会(0.3纳秒很短),在一台电脑上你可能永远不会看到问题,但在另一台电脑上你可能每次都会看到它。你不能事先告诉会发生什么。因此,为了可靠性,您必须正确编码。

测试

使用信号量控制访问测试程序共享数据是充满困难和困难的。事实上,你无法单独通过测试证明这样的程序是“完美的”。您所能做的就是评估程序设计的正确性,并检查源代码是否正确实现了设计。这是一项很多工作,很难做到这一点。程序员需要严格的纪律才能做到正确。

还有其他编程范例旨在缓解这个问题。沟通顺序流程是我一直在喋喋不休的流程。它需要一个完整的思维转变才能进入它,但回报是测试程序更有可能揭示源代码中的潜在问题。从程序员的角度来看,这是一种乐趣 - 你可以放松地编写多线程软件并享受它,而不是出汗和烦恼,以确保在信号量后面正确访问所有共享数据。它也非常好地扩展:)

链接:

CSP on Wikipedia - 对CSP的理论解释

Java CSP on Wikipedia - 在程序员级别对CSP有一个很好的解释

在C中,你有点自己。您最终将使用pipe()和pselect()或类似方法编写自己的库。但我说得非常值得。