【问题标题】:Why does this multi-threaded program work (and not crash)?为什么这个多线程程序可以工作(而不是崩溃)?
【发布时间】:2013-10-08 03:54:12
【问题描述】:

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

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

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

compute palindrome 只是遍历单词的子列表:

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。

【问题讨论】:

  • 没有什么会“抱怨”。如果您在一个线程中写入某个地址并在另一个不同步的情况下读取 - 您可能会读取不正确的数据。在这种情况下,您不会写入任何共享缓冲区 - 只是从中读取。取决于最初用数据填充数组的代码 - 它可能相当不错。
  • 另一点是不保证 MT 安全的代码会失败。希望是。在实践中,糟糕的代码可以通过许多测试而不会出错,但很少会失败。

标签: c multithreading pthreads


【解决方案1】:

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

希望这会有所帮助!

【讨论】:

  • 只有两个+线程需要更改共享数据时才需要同步(加锁)。
  • 啊,谢谢。我来自 Java,无论您是在阅读还是在写作,访问任何非线程安全的数据结构(如数组)都会立即引发 ConcurrentModificationException。
  • 欢迎来到 C 语言,在这里你可以做任何你想做的事,我们喜欢这样。
  • 更进一步(也许我对问题的评论不太清楚) - 写作也不会产生任何错误。您可能(或可能不会)读取不正确的数据。有时。这完全取决于程序员,如果您发现不需要同步(例如,出于性能原因 - 即使同步很便宜,它也会暂停所有其他互斥锁/信号量/等) - 您拥有避免它所需的一切。
【解决方案2】:

只是添加到现有的优秀答案和 cmets,让我们可视化在单核 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,并且您将使用其他程序流控制来确保其中正确的一个是结束的在一个。

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

测试

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

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

链接:

CSP on Wikipedia - CSP的理论解释

Java CSP on Wikipedia - 在程序员层面对 CSP 有很好的解释

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多