Pthreads в C - pthread_exit

По какой-то причине я думал, что вызов pthread_exit(NULL) в конце основной функции гарантирует, что все запущенные потоки (по крайней мере созданные в основной функции) закончатся до того, как main сможет выйти. Однако, когда я запускаю этот код ниже, не вызывая две функции pthread_join (в конце main), я явно получаю ошибку сегментации, которая, кажется, происходит, потому что функция main была завершена до того, как два потока завершат их и поэтому буфер char недоступен. Однако, когда я включаю эти два вызова функции pthread_join в конце main, он работает так, как должен. Чтобы гарантировать, что main не будет выходить до завершения всех запущенных потоков, необходимо ли явно вызывать pthread_join для всех потоков, инициализированных непосредственно в main?

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <assert.h>
#include <semaphore.h>
#define NUM_CHAR 1024
#define BUFFER_SIZE 8

typedef struct {
    pthread_mutex_t mutex; 
    sem_t full;
    sem_t empty;
    char* buffer;
} Context;

void *Reader(void* arg) {
    Context* context = (Context*) arg;
    for (int i = 0; i < NUM_CHAR; ++i) {
        sem_wait(&context->full);
        pthread_mutex_lock(&(context->mutex));
        char c = context->buffer[i % BUFFER_SIZE];
        pthread_mutex_unlock(&(context->mutex));
        sem_post(&context->empty);

        printf("%c", c);
    }
    printf("\n");
    return NULL;
}

void *Writer(void* arg) {
    Context* context = (Context*) arg;
    for (int i = 0; i < NUM_CHAR; ++i) {
        sem_wait(&context->empty);
        pthread_mutex_lock(&(context->mutex));
        context->buffer[i % BUFFER_SIZE] = 'a' + (rand() % 26);
        float ranFloat = (float) rand() / RAND_MAX;
        if (ranFloat < 0.5) sleep(0.2);
        pthread_mutex_unlock(&(context->mutex));
        sem_post(&context->full);
    }
    return NULL;
}

int main() {
    char buffer[BUFFER_SIZE];
    pthread_t reader, writer;
    Context context;
    srand(time(NULL));
    int status = 0;
    status = pthread_mutex_init(&context.mutex, NULL);
    status = sem_init(&context.full,0,0);
    status = sem_init(&context.empty,0, BUFFER_SIZE);
    context.buffer = buffer;

    status = pthread_create(&reader, NULL, Reader, &context);
    status = pthread_create(&writer, NULL, Writer, &context);

    pthread_join(reader,NULL);   // This line seems to be necessary
    pthread_join(writer,NULL);   // This line seems to be necessary

    pthread_exit(NULL);
    return 0;
}

Если это так, как я могу обработать случай, когда будет создано много идентичных потоков (например, в коде ниже) с использованием одного и того же идентификатора потока? В этом случае, как я могу убедиться, что все потоки будут завершены до выхода main? Должен ли я иметь массив идентификаторов NUM_STUDENTS pthread_t, чтобы иметь возможность сделать это? Думаю, я мог бы сделать это, разрешив потокам Student семафор, а затем позволить функции main ждать на этом семафоре, но действительно ли нет более простого способа сделать это?

int main()
{
    pthread_t thread;
    for (int i = 0; i < NUM_STUDENTS; i++)
        pthread_create(&thread,NULL,Student,NULL);  // Threads 
    // Make sure that all student threads have finished
    exit(0);
}

Ответы

Ответ 1

pthread_exit() - это функция, вызываемая потоком для завершения собственного выполнения. Для ситуации, которую вы дали, ее нельзя вызывать из основного потока программы.

Как вы поняли, pthread_join() - это правильное средство ожидания завершения соединяемого потока из main().

Также, как вы выяснили, вам нужно сохранить значение, возвращаемое из pthread_create(), чтобы перейти к pthread_join().

Это означает, что вы не можете использовать ту же переменную pthread_t для всех создаваемых вами потоков, если вы собираетесь использовать pthread_join().

Скорее, создайте массив pthread_t, чтобы у вас была копия каждого идентификатора потока.

Ответ 2

В стороне от того, должна ли программа или не должна заканчиваться, когда главный поток вызывает pthread_exit, pthread_exit говорит

Функция pthread_exit() завершается вызывающий поток

А также:

После того, как поток завершен, результат доступа к локальному (авто) переменными потока являются undefined.

Поскольку контекст является автоматической переменной main(), ваш код может упасть, прежде чем он даже дойдет до точки тестирования того, что вы хотите проверить...

Ответ 3

Мини-сага

Вы не указываете среду, в которой вы используете исходный код. Я изменил ваш код, чтобы использовать nanosleep() (так как, как я упомянул в комментарии к вопросу, sleep() принимает целое число и, следовательно, sleep(0.2) эквивалентен sleep(0)) и скомпилировал программу на MacOS X 10.6.4.

Без проверки ошибок

Он отлично работает; потребовалось около 100 секунд для запуска с вероятностным коэффициентом 0,5 (как и следовало ожидать, я изменил это на 0,05, чтобы сократить время выполнения до 10 секунд) и некоторое время генерировал случайную строку.

Иногда я ничего не получал, иногда получаю больше, а иногда получаю меньше данных. Но я не видел дампа ядра (даже с "ulimit -c неограниченным", чтобы разрешить сколь угодно большие дампы ядра).

В конце концов, я применил некоторые инструменты и понял, что у меня всегда было 1025 символов (1024 сгенерированных плюс новая строка), но довольно часто у меня было 1024 символа ASCII NUL. Иногда они появлялись посередине, иногда в начале и т.д.:

$  ./pth | tpipe -s "vis | ww -w64" "wc -c"
    1025
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000
\000\000\000\000\000\000\000\000ocriexffwgdvdvyfitjtvlzcoffhusjo
zyacniffpsfswesgrkuxycsubufamxxzkrkqnwvsxcbmktodessyohixsmuhdovt
hhertqjjinzoptcuqzertybicrzaeyqlyublbfgutcdvftwkuvxhouiuduoqrftw
xjkgqutpryelzuaerpsbotwyskaflwofseibfqntecyseufqxvzikcyeeikjzsye
qxhjwrjmunntjwhohqovpwcktolcwrvmfvdfsmkvkrptjvslivbfjqpwgvroafzn
fkjumqxjbarelbrdijfrjbtiwnajeqgnobjbksulvcobjkzwwifpvpmpwyzpwiyi
cdpwalenxmocmtdluzouqemmjdktjtvfqwbityzmronwvulfizpizkiuzapftxay
obwsfajcicvcrrjehjeyzsngrwusbejiovaaatyzouktetcerqxjsdpswixjpege
blxscdebfsptxwvwsllvydipovzmnrvoiopmqotydqaujwdykidmwzitdsropguv
vudyfiaaaqueyllnwudfpplcfbsngqqeyucdawqxqzczuwsnaquofreilzvdwbjq
ksrouwltvaktpdrvjnqahpdqdshmmvntspglexggshqbjrvxceaqlfnukedxzlms
cnapdtgtcoyhnglojbjnplowericrzbfulvrobfn
$

(Программа "tpipe" похожа на "tee", но она записывает в каналы вместо файлов (и на стандартный вывод, если вы не укажете опцию "-s" ); "vis" происходит от "среды программирования UNIX" на Керниган и Пайк: "ww" - это "оболочка слов", но здесь нет слов, поэтому грубая сила обертывает шириной 64.)

Поведение, которое я наблюдал, было очень неопределенным - я получал разные результаты при каждом прогоне. Я даже заменил случайные символы алфавитом в последовательности ('a' + i% 26) и все еще получал странное поведение.

Я добавил некоторый код отладочной печати (и счетчик контекс), и было ясно, что семафор context->full не работал должным образом для читателя - ему разрешалось входить во взаимное исключение, прежде чем писатель написано что-нибудь.

С проверкой ошибок

Когда я добавил проверку ошибок в операции мьютекса и семафора, я обнаружил, что:

sem_init(&context.full) failed (-1)
errno = 78 (Function not implemented)

Итак, странные выходы связаны с тем, что MacOS X не реализует sem_init(). Это странно; сбой функции sem_wait() с errno = 9 (EBADF "Bad file descriptor" ); Я сначала добавил проверки. Затем я проверил инициализацию...

Использование sem_open() вместо sem_init()

Успешно выполняется вызов sem_open(), который выглядит хорошо (имена "/full.sem" и "/empty.sem"), флаги O_CREAT, значения режима 0444, 0600, 0700 в разное время и начальные значения 0 и BUFFER_SIZE, как при sem_init()). К сожалению, первая операция sem_wait() или sem_post() завершилась с ошибкой с errno = 9 (EBADF 'Bad file descriptor').

Мораль

  • Важно проверить условия ошибки из системных вызовов.
  • Вывод, который я вижу, не является детерминированным, поскольку семафоры не работают.
  • Это не изменяет "он не сбой без поведения вызовов pthread_join().
  • MacOS X не имеет рабочей реализации семафора POSIX.

Ответ 4

pthread_join() - это стандартный способ дождаться завершения другого потока, я бы придерживался этого.

В качестве альтернативы вы можете создать счетчик потоков, и все дочерние потоки увеличивают его на 1 при запуске, а затем уменьшают его на 1, когда они заканчивают (с правильной фиксацией курса), а затем main() ждут этого счетчика нажмите 0. (pthread_cond_wait() будет моим выбором).

Ответ 5

Нет необходимости вызывать pthread_join(reader,NULL); вообще, если Context и buffer объявлены со статической продолжительностью хранения (как уже указывал Стив Джессоп, кафе и Дэвид Шварц).

Объявление Context и buffer static также требует изменения Context *context до Context *contextr или Context *contextw соответственно.

Кроме того, следующая запись, называемая pthread_exit.c, заменяет sem_init() на sem_open() и использует nanosleep() (как было предложено Джонатаном Леффлером).

pthread_exit был протестирован в Mac OS X 10.6.8 и не выводил никаких символов ASCII NUL.

/*

cat pthread_exit.c  (sample code to test pthread_exit() in main())

source: 
"pthreads in C - pthread_exit",
http://stackoverflow.com/questions/3330048/pthreads-in-c-pthread-exit

compiled on Mac OS X 10.6.8 with:
gcc -ansi -pedantic -std=gnu99 -Os -Wall -Wextra -Wshadow -Wpointer-arith -Wcast-qual  -Wstrict-prototypes \
    -Wmissing-prototypes -Wformat=2 -l pthread -o pthread_exit pthread_exit.c

test with: 
time -p bash -c './pthread_exit | tee >(od -c 1>&2) | wc -c'

*/


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

#include <time.h>

void *Reader(void* arg);
void *Writer(void* arg);

// #define NUM_CHAR 1024
#define NUM_CHAR 100
#define BUFFER_SIZE 8

typedef struct {
    pthread_mutex_t mutex; 
    sem_t *full;
    sem_t *empty;
    const char *semname1;
    const char *semname2;
    char* buffer;
} Context;


static char buffer[BUFFER_SIZE];
static Context context;

void *Reader(void* arg) {
    Context *contextr = (Context*) arg;
    for (int i = 0; i < NUM_CHAR; ++i) {
        sem_wait(contextr->full);
        pthread_mutex_lock(&(contextr->mutex));
        char c = contextr->buffer[i % BUFFER_SIZE];
        pthread_mutex_unlock(&(contextr->mutex));
        sem_post(contextr->empty);
        printf("%c", c);
    }
    printf("\n");
    return NULL;
}

void *Writer(void* arg) {
    Context *contextw = (Context*) arg;
    for (int i = 0; i < NUM_CHAR; ++i) {
        sem_wait(contextw->empty);
        pthread_mutex_lock(&(contextw->mutex));
        contextw->buffer[i % BUFFER_SIZE] = 'a' + (rand() % 26);
        float ranFloat = (float) rand() / RAND_MAX;
        //if (ranFloat < 0.5) sleep(0.2);
        if (ranFloat < 0.5)
           nanosleep((struct timespec[]){{0, 200000000L}}, NULL);
        pthread_mutex_unlock(&(contextw->mutex));
        sem_post(contextw->full);
    }
    return NULL;
}

int main(void) {
    pthread_t reader, writer;
    srand(time(NULL));
    int status = 0;
    status = pthread_mutex_init(&context.mutex, NULL);
    context.semname1 = "Semaphore1";
    context.semname2 = "Semaphore2";

    context.full = sem_open(context.semname1, O_CREAT, 0777, 0);
    if (context.full == SEM_FAILED)
    {
        fprintf(stderr, "%s\n", "ERROR creating semaphore semname1");
        exit(EXIT_FAILURE);
    }

    context.empty = sem_open(context.semname2, O_CREAT, 0777, BUFFER_SIZE);
    if (context.empty == SEM_FAILED)
    {
        fprintf(stderr, "%s\n", "ERROR creating semaphore semname2");
        exit(EXIT_FAILURE);
    }

    context.buffer = buffer;

    status = pthread_create(&reader, NULL, Reader, &context);
    status = pthread_create(&writer, NULL, Writer, &context);

//    pthread_join(reader,NULL);   // This line seems to be necessary
//    pthread_join(writer,NULL);   // This line seems to be necessary

    sem_unlink(context.semname1);
    sem_unlink(context.semname2);

    pthread_exit(NULL);

    return 0;
}

Ответ 6

pthread_join выполняет следующие действия:

Функция pthread_join() приостанавливает выполнение вызывающего потока до тех пор, пока целевой поток не завершится, если целевой поток уже не завершился. При возврате из успешного вызова pthread_join() с аргументом non-NULL value_ptr значение, переданное в pthread_exit() завершающим потоком, становится доступным в местоположении, на который ссылается value_ptr. Когда a pthread_join() возвращается успешно, целевой поток завершается. Результаты нескольких одновременных вызовов pthread_join() с указанием одного и того же целевого потока: undefined. Если поток, вызывающий pthread_join(), отменяется, то целевой поток не будет отсоединен.

Однако вы можете добиться того же, используя петлю легкого веса, которая предотвратит выход exe. В Glib это достигается путем создания GMainLoop, в Gtk + вы можете использовать gtk_main. После завершения потоков вы должны выйти из основного цикла или вызвать gtk_exit.

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

Ответ 7

В соответствии с семантикой pthread, как описано, например. здесь, ваша оригинальная идея, похоже, подтверждается:

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

Однако я не уверен, что эта часть потоков POSIX стандартная или просто обычная, но не универсальная "приятная иметь" добавочный лакомый кусочек (я знаю, что некоторые реализации не уважают это ограничение - я просто не знаю, должны ли эти реализации считаться стандартными. -). Поэтому мне придется присоединиться к осторожному хору, рекомендующему присоединиться к каждой теме, которую нужно прекратить, просто чтобы быть в безопасности - или, как Jon Postel поместите его в контексте реализации TCP/IP:

Be conservative in what you send; be liberal in what you accept.

a "принцип надежности", который следует использовать гораздо шире, чем просто в TCP/IP; -).

Ответ 8

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

Использовать надлежащую pthread_join(3) технику, как предложено другими, или перемещать общие переменные в статическое хранилище.

Ответ 9

Когда вы передаете поток указателю на переменную, вам нужно убедиться, что время жизни этой переменной не меньше, пока поток попытается получить доступ к этой переменной. Вы передаете указатели потоков на buffer и context, которые выделяются в стеке внутри main. Как только main завершается, эти переменные перестают существовать. Поэтому вы не можете выйти из main, пока не убедитесь, что эти потоки больше не нуждаются в доступе к этим указателям.

В 95% случаев исправление этой проблемы состоит в следующем:

1) Выделите объект для хранения параметров.

2) Заполните объект параметрами.

3) Передайте указатель на объект в новый поток.

4) Разрешить новому потоку освобождать объект.

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

Вам нужно сделать это как для buffer, так и для context. Задайте значение использования 2, а затем передайте указатель на этот объект на оба потока.