Linux - форсирует одноядерное выполнение и отлаживает многопоточность с помощью pthread - PullRequest
2 голосов
/ 22 ноября 2010

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

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

Я профилировал выполнение программы, используя Valgrind:

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

Я получаю пару конфликтов в сегменте BSS, которые вызваны моими глобальными структурами и переменными счетчика потоков. Однако я мог бы смягчить эти конфликты с помощью принудительного выполнения Signle-Core, потому что я думаю, что одновременное планирование моих 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);
}

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

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

my_data-> nr иногда разрешает большие целые значения:

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
[...] 

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

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

Ответы [ 3 ]

3 голосов
/ 22 ноября 2010

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

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

В этой программе у вас есть переменные, которые используются совместно и изменяются разными потоками.Если переменная читается только потоками (либо const, либо эффективно const после того, как потоки, которые смотрят на нее) создаются, вам не о чем беспокоиться (и в "чтение по потокам" я включаю основной original thread), поскольку переменная не изменяется, если компилятор генерирует код для чтения этой переменной только один раз (запоминая его в локальной временной переменной) или если он генерирует код для чтения этого значения снова и снова.всегда один и тот же, так что вычисления, основанные на нем, всегда одинаковы.

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

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

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

x = x + 1;

, требует, чтобы вы знали старое значение для генерации нового значения.Если x является глобальным, то вы должны концептуально загрузить x в регистр, добавить 1 в этот регистр, а затем сохранить это значение обратно вx.На процессоре RISC вы фактически должны выполнить все 3 из этих инструкций, и, будучи 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 должен использоваться в качестве индекса в массиве потоков. Обратите внимание, что я создал не один поток в этом коде, а ваш должен был создать несколько. Я сделал это, чтобы сделать пример более понятным, но не должно быть слишком сложно изменить его, чтобы продолжить и добавить NEW_THR к num_threads, но если NEW_THR равно 2 и MAX_THR - num_threads равно 1 (каким-то образом), то Вы должны как-то справиться с этим правильно.

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

man 3 semaphore.h

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

1 голос
/ 22 ноября 2010

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

0 голосов
/ 22 ноября 2010

https://computing.llnl.gov/tutorials/pthreads/#PassingArguments

, что, кажется, проблема.вам нужно malloc структура thread_data.

...