2010-11-22 2 views
2

Я отлаживаю многопоточную проблему с C, pthread и Linux. На моем MacOS 10.5.8 C2D работает нормально, на моих компьютерах Linux (2-4 ядра) он вызывает нежелательные выходы.Linux - принудительное одноядерное выполнение и отладка многопоточности с pthread

У меня нет опыта, поэтому я прикрепил свой код. Это довольно просто: каждый новый поток создает еще два потока до достижения максимума. Так что ничего страшного ... как я думал, пока пару дней назад. Могу ли я принудительно выполнить одноядерное выполнение, чтобы предотвратить появление ошибок?

Я профилированное выполнение программки, инструментирование с Valgrind:

valgrind --tool=drd --read-var-info=yes --trace-mutex=no ./threads 

Я получаю пару конфликтов в сегменте BSS - которые вызваны моими глобальных структурами и счетчик нити variales. Однако я мог бы смягчить эти конфликты с принудительным выполнением сигн-ядро, потому что, по-моему, параллельное планирование моих 2-4-ядерных тестовых систем отвечает за мои ошибки.

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

#define MAX_THR  12 
#define NEW_THR  2 

int wait_time = 0; // log global wait time 
int num_threads = 0; // how many threads there are 
pthread_t threads[MAX_THR]; // global array to collect threads 
pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER; // sync 

struct thread_data 
{ 
    int nr;    // nr of thread, serves as id 
    int time;   // wait time from rand() 
}; 
struct thread_data thread_data_array[MAX_THR+1]; 


void 
*PrintHello(void *threadarg) 
{ 

    if(num_threads < MAX_THR){ 
    // using the argument 

    pthread_mutex_lock(&mut); 
    struct thread_data *my_data; 
    my_data = (struct thread_data *) threadarg; 

    // updates 
    my_data->nr = num_threads; 
    my_data->time= rand() % 10 + 1; 

    printf("Hello World! It's me, thread #%d and sleep time is %d!\n", 
       my_data->nr, 
       my_data->time); 

    pthread_mutex_unlock(&mut); 

    // counter 
    long t = 0; 

    for(t = 0; t < NEW_THR; t++){ 
     pthread_mutex_lock(&mut); 
      num_threads++; 
      wait_time += my_data->time; 
      pthread_mutex_unlock(&mut); 
     pthread_create(&threads[num_threads], NULL, PrintHello, &thread_data_array[num_threads]); 
     sleep(1); 
    } 
    printf("Bye from %d thread\n", my_data->nr); 

    pthread_exit(NULL); 
    } 
    return 0; 
} 

int 
main (int argc, char *argv[]) 
{ 

    long t = 0; 
    // srand(time(NULL)); 
    if(num_threads < MAX_THR){ 
     for(t = 0; t < NEW_THR; t++){ 
      // -> 2 threads entry point 
      pthread_mutex_lock(&mut); 
       // rand time 
       thread_data_array[num_threads].time = rand() % 10 + 1; 
      // update global wait time variable 
       wait_time += thread_data_array[num_threads].time; 
       num_threads++; 
     pthread_mutex_unlock(&mut); 
     pthread_create(&threads[num_threads], NULL, PrintHello, &thread_data_array[num_threads]); 
     pthread_mutex_lock(&mut); 
     printf("In main: creating initial thread #%ld\n", t); 
     pthread_mutex_unlock(&mut); 

     } 
    } 

    for(t = 0; t < MAX_THR; t++){ 
     pthread_join(threads[t], NULL); 
    } 

    printf("Bye from program, wait was %d\n", wait_time); 
    pthread_exit(NULL); 
} 

Надеюсь, что код не так уж плох. Я долгое время не делал слишком много С. :) Проблема заключается в том:

printf("Bye from %d thread\n", my_data->nr); 

my_data-> Н.Р. иногда решает высокие целые значения:

In main: creating initial thread #0 
Hello World! It's me, thread #2 and sleep time is 8! 
In main: creating initial thread #1 
[...] 
Hello World! It's me, thread #11 and sleep time is 8! 
Bye from 9 thread 
Bye from 5 thread 
Bye from -1376900240 thread 
[...] 

Я сейчас не больше способов профиля и отлаживать это. Если я отлаживаю это, он работает - иногда. Иногда это не так :(

Спасибо, что прочитал этот длинный вопрос. :) Надеюсь, я не разделял слишком много моей неуловимой неразберихи.

+0

В такие моменты мне жаль, что у меня нет хорошего основного учебника по многопоточности - возможно, некоторые из других могут указать на хороший? Извините, но в настоящее время мне не хватает времени, чтобы ответить на вопрос сам ... – gimpf 2010-11-22 15:30:21

+0

Возможно, он не производит то, что вы ищете, но вы можете использовать «taskset», чтобы установить близость запущенного (или начального) процесса. .. – 2010-11-22 17:27:40

+1

Мне интересно, если ваш массив `threads` переполнен в вашу переменную` num_threads`. – ninjalj 2010-11-22 22:55:41

ответ

3

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

Компиляторы C часто делают много предположений, когда делают оптимизации. Одно из предположений заключается в том, что если текущий рассматриваемый код не выглядит так, как будто он может изменить некоторую переменную, переменная не изменяется (это очень приблизительное приближение к этому, и более точное объяснение займет очень много времени).

В этой программе есть переменные, которые совместно используются и изменяются разными потоками. Если переменная считывается только потоками (либо const, либо эффективно const после того, как потоки, которые смотрят на нее, создаются), вам не о чем беспокоиться (и в «read by threads» я включаю в себя основной оригинал thread), потому что поскольку переменная не изменяется, если компилятор только генерирует код для чтения этой переменной один раз (запоминание ее в локальной временной переменной), или если она генерирует код для его чтения, то значение всегда всегда одинаково, так что вычисления основанные на нем всегда выходят одинаково.

Чтобы заставить компилятор не делать этого, вы можете использовать ключевое слово volatile. Он прикрепляется к объявлениям переменных точно так же, как и к ключевому слову const, и сообщает компилятору, что значение этой переменной может измениться в любой момент времени, поэтому перечитывайте его каждый раз, когда это необходимо, и переписывайте его каждый раз, когда назначается новое значение для него ,

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

Я уверен, что теперь вы думаете, что знаете, как исправить вашу программу, но это не так просто. Вы выполняете математику по общей переменной. Выполнение математики с переменной, используя код:

x = x + 1; 

требует, чтобы вы знали старое значение для генерации нового значения. Если x является глобальным, то вы должны концептуально нагрузкиx в регистр, добавить 1 в этот регистр, а затем магазин что значение обратно в x. На RISC-процессоре вам действительно нужно выполнить все три из этих инструкций, и, будучи 3-мя инструкциями, я уверен, что вы можете видеть, как другой поток, обращающийся к одной и той же переменной, почти в одно и то же время может закончиться , сохраняя новое значение в x просто после того, как мы прочтем нашу ценность - сделаем нашу ценность старой, поэтому наш расчет и ценность, которую мы храним, будут ошибочными.

Если вы знаете какую-либо сборку x86, то вы, вероятно, знаете, что она имеет инструкции, которые могут выполнять математику по значениям в ОЗУ (как получение, так и сохранение результата в том же месте в ОЗУ в одной инструкции). Вы можете подумать, что эта инструкция может быть использована для этой операции в системах x86, и вы почти правы. Проблема в том, что эта команда все еще выполняется в тех шагах, которые выполняются в команде RISC, и есть несколько возможностей для другого процессора изменить эту переменную одновременно с тем, как мы делаем нашу математику. Чтобы обойти это на x86, есть префикс lock, который может быть применен к некоторым инструкциям x86, и я считаю, что заголовочные файлы glibc включают в себя атомные макрофункции для этого на архитектурах, которые могут его поддерживать, но это невозможно сделать на всех архитектуры.

Чтобы правильно работать на всех архитектурах, которые вы собираетесь нужно:

int local_thread_count; 
int create_a_thread; 

pthread_mutex_lock(&count_lock); 
local_thread_count = num_threads; 
if (local_thread_count < MAX_THR) { 
    num_threads = local_thread_count + 1; 
    pthread_mutex_unlock(&count_lock); 

    thread_data_array[local_thread_count].nr = local_thread_count; 
              /* moved this into the creator 
              * since getting it in the 
              * child will likely get the 
              * wrong value. */ 

    pthread_create(&threads[local_thread_count], NULL, PrintHello, 
             &thread_data_array[local_thread_count]); 

} else { 
    pthread_mutex_unlock(&count_lock); 
} 

Теперь, так как вы изменили бы num_threads к volatile вы можете атомарно тест и увеличивать количество потоков во всех потоках. В конце этого local_thread_count должен использоваться как индекс в массиве потоков. Обратите внимание, что я не создал в этом коде только 1 поток, в то время как ваш должен был создать несколько. Я сделал это, чтобы сделать этот пример более понятным, но не нужно слишком сложно его изменить, чтобы добавить NEW_THR в num_threads, но если NEW_THR равно 2, а MAX_THR - num_threads - это 1 (каким-то образом), тогда вам нужно как-то правильно это обработать.

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

man 3 semaphore.h 

расскажет вам немного об этом.

1

num_threads должны, по крайней мере, быть маркированы volatile, и, предпочтительно, помечены атомным тоже (хотя я считаю, что int практически нормально), так что по крайней мере, существует более высокая вероятность того, что различные потоки видят одни и те же значения. Возможно, вам захочется просмотреть выход ассемблера, чтобы увидеть, когда на самом деле якобы происходят записи num_thread в память.