3

据我了解,如果两个或多个线程试图同时访问同一个内存块,它至少应该“抱怨”。

我正在为一个计算回文的类编写一个程序(在列表中前后出现的单词也算在内)。在我的多线程解决方案中,我生成了 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。

4

2 回答 2

5

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

希望这可以帮助!

于 2013-10-08T03:57:49.543 回答
4

只是为了添加现有的优秀答案和评论,让我们可视化单核 CPU 中发生的事情。

现在显然在单核 CPU 中一次只能运行一个线程/进程。操作系统调度程序实际上只是另一个线程(一个非常特殊的线程......),它选择在接下来的 15 毫秒左右运行什么。但是在任何时候,任何正在运行的东西都只能访问内存。因此,尽管在实践中可能会感觉一次同时运行很多东西,但实际上并没有。只是在很短的时间内一次运行一个线程。

但是,我们现在都有多核 CPU。那里发生的情况是内核都能够寻址内存(一种或另一种方式 - 诸如 Netburst、QPI 和 Hypertransport 之类的东西使事情变得复杂)。然而,在电子设备的深处,不可能有两个或多个内核同时访问同一个存储芯片。这样做会开始破坏事情,因此需要非常小心以确保一次只有一个内核可以访问任何一块物理内存。因此,内存访问以一种非常基本的方式在电子级别进行了序列化。

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

所以结果是,如果不同内核上的两个线程尝试写入同一个地址,内存硬件会强制它们轮流执行。如果访问确实是同时进行的,那么哪个先走只是纯粹的运气。在软件中你不知道这种仲裁是如何完成的,所以你不能依赖它。在一台具有 32 位总线宽度的计算机上,您可能会在一次不间断的操作中完成 int32 分配,但可能不是 int64 分配。但是在 64 位总线宽度上,您可能会发现 int64 分配已不间断地完成。

因此,如果您在一个线程中有一条语句,例如 a = b + c,而在另一个线程中有 a = 10,并且它们在完全相同的时间运行(彼此相差不到 0.3 纳秒,大致对应于 3GHz 时钟速率),您不知道 a 是 10、b+c 还是两者的可怕混合(因为您不知道硬件如何处理两个内核的写入,也不知道有多少硬件内存事务实际上无论如何都包含在“a =”中,并且您不知道线程是否已被调度程序在中途抢占!)。不管发生了什么,电子设备都不会抱怨(为什么会抱怨呢?它只关心不着火和爆炸,第二次猜测软件想要做什么不是硬件的工作),

这就是为什么您需要信号量来序列化对“a”的访问,以便它最终至少是 b+c 或 10,并且您将使用其他程序流控制来确保其中正确的一个是最终在 a .

如果没有信号量,您就是在冒险(0.3 纳秒非常短),在一台计算机上您可能永远看不到问题,但在另一台计算机上您可能每次都会遇到问题。你无法提前知道会发生什么。因此,为了可靠性,您必须正确编码。

测试

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

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

链接:

维基百科上的 CSP——CSP 的理论解释

Wikipedia 上的 Java CSP - 对程序员级别的 CSP 有很好的解释

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

于 2013-10-08T06:39:33.687 回答