Как память распределяется между потоками в C - PullRequest
0 голосов
/ 07 января 2019

Я пытаюсь понять, как память распределяется между потоками.

Я знаю, что каждый поток имеет свой собственный стек, а куча распределяется между всеми потоками. Каждый поток имеет общее адресное пространство, поэтому локальная переменная внутри потока может быть видна другому потоку с помощью указателей. Это делается с помощью библиотеки Pthix POSIX в Linux.

Итак, предполагая, что это правильно, если я создаю поток с локальным 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);
}

Но если я создам var с помощью malloc для размещения его в куче, он не покажет правильное значение. Зачем? Код ниже

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);
}

Ответы [ 3 ]

0 голосов
/ 07 января 2019

Вот не совсем минимальная программа 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 и запуске (на Mac с MacOS 10.14.2 Mojave, с использованием 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() работает правильно и как ожидалось.

Когда объединения не указаны, происходит сбой кода, что является одним из проявлений «неопределенного поведения». Если вы не присоединяетесь к потокам th2 и th4, потоки th1 и th3 могут оставлять другим доступ к данным, которые больше не действительны. (Конечно, выделенная память не была освобождена в оригинале, но сбой происходил до выделения памяти.)

Будьте внимательны, чтобы потоки обращались только к действительным данным.

Не пытайтесь делиться данными между потоками, как это; Вы берете работу, которая уже трудна (правильное программирование потоков трудно), и делаете ее еще сложнее.

0 голосов
/ 07 января 2019

Я знаю, что каждый поток имеет свой собственный стек, а куча - общая между каждым потоком. Каждый поток имеет общее адресное пространство, поэтому локальная переменная внутри потока может быть видна другому потоку, используя указатели. Это делается с помощью библиотеки pthread POSIX в Linux.

Некоторые из этих деталей могут различаться в зависимости от операционной системы и реализации потоков, но POSIX указывает , что

Все, чей адрес может быть определен потоком, в том числе, но не ограничивается статическими переменными, хранилище получено через malloc(), хранилище с прямой адресацией, полученное в результате реализации функции, и автоматические переменные , доступны для всех потоков в тот же процесс.

(выделение добавлено).

если я создам поток с локальной переменной, размещенной в его стеке, другой поток должен прочитать неправильное значение, если кадр стека, содержащий var разрушен.

Нет, у вас это в значительной степени задом наперед. Что вы можете сказать, так это то, что любому потоку разрешено считывать значение автоматической переменной только в течение времени жизни этой переменной. Спецификации C вообще не упоминают стеки, но в реализации на основе стека время жизни автоматической переменной заканчивается, когда кадр стека, к которому она принадлежит, выталкивается, или ранее . После окончания времени жизни переменной попытка прочитать ее значение через указатель приводит к неопределенному поведению. Среди множества возможных вариантов поведения, которые могут проявиться, можно прочитать любое значение вообще, включая значение, которое хранится в переменной в конце срока ее жизни.

Но если я создам var с помощью malloc для размещения его в куче, он не покажет правильное значение. Почему?

Вы не представили полный пример, но когда я объединил функции, которые вы представили с этим main():

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

, результирующая программа напечатана

0xa

, который показывает, что второй поток действительно правильно читает значение, сохраненное в выделенном объекте, как и следовало ожидать, в той степени, в которой он работает до завершения программы.

sleep() в main() присутствует, чтобы вероятно (но не уверен), что общая программа не завершится, пока второй поток не завершится. На практике следует обязательно присоединиться к каждому потоку, но оригинальные функции не позволяют этого сделать, и я решил не изменять их.

0 голосов
/ 07 января 2019

Переменные в стеке потоков недоступны между потоками или процессом. Внутри потока это может быть передано в качестве параметров функции, но как только поток выходит, его стек исчезает, и поэтому переменные. В коде, в то время как указатель на переменную передается во второй поток, после выхода из первого потока эта переменная не определена для второго потока.

...