【问题标题】:How memory is shared between threads in CC中的线程之间如何共享内存
【发布时间】:2019-06-02 07:17:11
【问题描述】:

我试图了解线程之间如何共享内存。

我知道每个线程都有自己的堆栈,而堆在每个线程之间共享。每个线程共享公共寻址空间,因此线程内的局部变量可以被另一个线程使用指针看到。这是通过在 Linux 中使用 POSIX 库 pthread 来完成的。

所以,假设它是正确的,如果我创建一个线程并在他的堆栈中分配了一个本地 var,如果包含 var 的堆栈帧被破坏,另一个线程应该读取错误的值。有了这段代码,它就以这种方式工作。

void *_th2(void *args) {

    sleep(1);
    printf("0x%x\n", *(int *)args);
    fflush(stdout);

    pthread_exit(NULL);
}

void *_th1(void *args) {
    pthread_t tid;
    int var = 10;

    pthread_create(&tid, NULL, _th2, (void *)&var);
    pthread_exit(NULL);
}

但是,如果我使用 malloc 创建 var 以在堆中分配它,它不会显示正确的值。为什么?代码如下

void *_th2(void *args) {

    sleep(1);
    printf("0x%x\n", *(int *)args);
    fflush(stdout);

    pthread_exit(NULL);
}

void *_th1(void *args) {
    pthread_t tid;
    int *var = malloc(sizeof *var);

    *var = 10;
    pthread_create(&tid, NULL, _th2, (void *)var);
    pthread_exit(NULL);
}

【问题讨论】:

  • 第一个具有未定义的行为。第二个有内存泄漏,但应该按预期工作。您是如何测试并得出结论的?
  • 在第一个代码块中,没有什么会迫使它读取错误的值。第二个线程可能在第一个线程调用pthread_exit() 之前运行。即使它稍后运行,也不会强制立即覆盖无效变量的内存。未定义的行为意味着任何事情都可能发生,包括成功读取旧变量的内存。
  • 你应该在pthread_join()_th1()中的第二个线程pthread_exit()之前。
  • 请注意,一般情况下,您不应创建以下划线开头的函数、变量、标记或宏名称。 C11 §7.1.3 Reserved identifiers 的一部分说: — 以下划线开头的所有标识符以及大写字母或另一个下划线始终保留用于任何用途。所有以下划线开头的标识符始终保留保留用作普通和标记名称空间中具有文件范围的标识符。 另见What does double underscore (__const) mean in C?
  • 运行第二版代码时看到了什么?你能发布整个程序吗?

标签: c multithreading memory heap-memory stack-memory


【解决方案1】:

根据问题中显示的内容,这是一个不太简单的 MCVE (Minimal, Complete, Verifiable Example) 程序:

#include <assert.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static int join = 1;

static void *th2(void *args)
{
    printf("%s: %d (%p)\n", __func__, *(int *)args, args);
    sleep(1);
    printf("0x%X\n", *(int *)args);
    fflush(stdout);
    pthread_exit(NULL);
}

static void *th1(void *args)
{
    assert(args == NULL);
    pthread_t tid;
    int var = 10;

    printf("%s: %d (%p)\n", __func__, var, (void *)&var);
    pthread_create(&tid, NULL, th2, &var);
    if (join)
        pthread_join(tid, NULL);
    pthread_exit(NULL);
}

/*---*/

static void *th4(void *args)
{
    printf("%s: %d (%p)\n", __func__, *(int *)args, args);
    sleep(1);
    printf("0x%X\n", *(int *)args);
    fflush(stdout);
    pthread_exit(NULL);
}

static void *th3(void *args)
{
    assert(args == NULL);
    pthread_t tid;
    int *var = malloc(sizeof *var);

    *var = 10;
    printf("%s: %d (%p)\n", __func__, *var, (void *)var);
    pthread_create(&tid, NULL, th4, var);
    if (join)
    {
        pthread_join(tid, NULL);
        free(var);
    }
    /* else leak memory for var */
    pthread_exit(NULL);
}

int main(int argc, char **argv)
{
    pthread_t t1;
    pthread_t t3;

    if (argc > 1 && argv[argc] == NULL)
        join = 0;
    printf("%s pthread_join() on sub-threads\n", join ? "Using" : "Not using");

    printf("launch 1\n");
    pthread_create(&t1, NULL, th1, NULL);
    pthread_join(t1, NULL);

    printf("launch 3\n");
    pthread_create(&t3, NULL, th3, NULL);
    pthread_join(t3, NULL);

    printf("finished\n");

    return 0;
}

设置为如果传递了命令行参数,子线程th1()th3()在退出前不执行pthread_join();如果没有传递参数,它们会等待。

当编译为 pth19 并运行(在运行 macOS 10.14.2 Mojave 的 Mac 上,使用 GCC 8.2.0)时,我得到:

$ pth19
Using pthread_join() on sub-threads
launch 1
th1: 10 (0x70000bda2f04)
th2: 10 (0x70000bda2f04)
0xA
launch 3
th3: 10 (0x7fa0a9500000)
th4: 10 (0x7fa0a9500000)
0xA
finished
$ pth19 1
Not using pthread_join() on sub-threads
launch 1
th1: 10 (0x70000690ff04)
Segmentation fault: 11
$

pthread_join() 调用一起使用时,它可以正常工作并按预期工作。

当连接被省略时,代码会崩溃——这是“未定义行为”表现出来的一种方式。当您不加入 th2th4 线程时,th1th3 线程可以让其他线程访问不再有效的数据。 (诚​​然,原来分配的内存并没有被释放,但是在内存分配之前就发生了崩溃。)

注意确保线程只访问有效数据。

不要尝试像这样在线程之间共享数据;你正在从事一项已经很难的工作(正确的线程编程很困难)并且变得更加困难。

【讨论】:

    【解决方案2】:

    我知道每个线程都有自己的堆栈,而堆是共享的 每个线程之间。每个线程共享公共地址空间,所以 另一个线程可以看到线程内的局部变量 指针。这是通过在 Linux 中使用 POSIX 库 pthread 来完成的。

    其中一些细节可能会因操作系统和线程实现而异,但 POSIX 会 specify 这样做

    地址可以由线程确定的任何东西,包括但 不限于静态变量,通过malloc()获得的存储, 通过实现定义获得的直接可寻址存储 函数、和自动变量,所有线程都可以访问 相同的过程。

    (强调)。

    如果我创建一个线程并在他的堆栈中分配了一个本地变量,另一个 如果包含 var 的堆栈帧是,线程应该读取错误的值 销毁。

    不,你几乎倒退了。您可以说的是,任何线程只允许在该变量的生命周期内读取自动变量的值。 C 规范根本没有提到堆栈,但在基于堆栈的实现中,自动变量的生命周期在其所属的堆栈帧被弹出时结束,或更早。在变量的生命周期结束后,尝试通过指针读取其值会产生未定义的行为。在许多可能表现出来的行为中,可能会读取任何值,包括变量在其生命周期结束时所持有的值。

    但是,如果我使用 malloc 创建 var 以在堆中分配它,它不会显示正确的值。为什么?

    你还没有提供一个完整的例子,但是当我把你提供的功能和这个main()结合起来时:

    int main(void) {
        _th1(NULL);
        sleep(3);
        return 0;
    }
    

    ,打印出来的程序

    0xa

    这表明第二个线程确实正确地读取了存储在分配对象中的值,正如预期的那样,它在程序终止之前运行。

    main() 中的sleep() 的存在使得整个程序在第二个线程运行完成之前可能(但不确定)不会终止。在实践中,确实应该加入每个线程,但原始功能无法做到这一点,我选择不修改它们。

    【讨论】:

      【解决方案3】:

      线程堆栈上的变量不能跨线程或进程访问。在线程中,它可以作为参数传递给函数,但是一旦线程退出,它的堆栈就会消失,变量也会消失。在代码中,当指向变量的指针传递给第二个线程时,一旦第一个线程退出,该变量对于第二个线程是未定义的。

      【讨论】:

      • 线程堆栈上的变量可以跨线程访问,只要线程仍在运行并且它还没有从声明变量的函数返回。
      猜你喜欢
      • 2012-07-06
      • 2016-03-01
      • 1970-01-01
      • 1970-01-01
      • 2014-04-19
      • 2017-11-30
      • 2013-05-09
      • 2023-03-31
      • 1970-01-01
      相关资源
      最近更新 更多