расширение Python c: многопоточность и случайные числа - PullRequest
0 голосов
/ 29 ноября 2011

Я реализовал шаблон рабочей очереди в C (в расширении Python), и я разочарован производительностью.

У меня есть симуляция со списком частиц («элементов»), и я измеряю время, затрачиваемое на выполнение всех вычислений, необходимых для временного шага, и записываю это вместе с количеством вовлеченных частиц. Я выполняю код на четырехъядерном Hyper-Threading i7, поэтому я ожидал, что производительность возрастет (время, необходимое для падения) с числом потоков до 8, но вместо этого в самой быстрой реализации нет рабочих потоков (функции просто выполняется вместо добавления в очередь,) и с каждым рабочим потоком код становится все медленнее и медленнее (на шаг больше, чем время для реализации без потоков для каждого нового потока!) Я быстро взглянул на использование моего процессора приложение, и кажется, что Python никогда не превышает 130% загрузки процессора, независимо от того, сколько потоков работает. Машина имеет достаточный запас мощности выше, общее использование системы составляет около 200%.

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

Теперь я прочитал, что моя первоначальная попытка с rand() будет медленной, потому что мои случайные числа не были потокобезопасными (имеет ли это предложение смысл? Не уверен ...)

Я пробовал реализацию как с random(), так и с drand48_r (хотя, к сожалению, последний, кажется, недоступен на OS X), но без статистики.

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

void* worker_thread_function(void* untyped_queue) {

  queue_t* queue = (queue_t*)untyped_queue;
  int success = 0;
  int rand_id;
  long int temp;
  work_item_t* work_to_do = NULL;
  int work_items_completed = 0;

  while (1) {
    if (pthread_mutex_lock(queue->mutex)) {

      // error case, try again:
      continue;
    }

    while (!success) {

      if (queue->queue->count == 0) {

        pthread_mutex_unlock(queue->mutex);
        break;
      }

      // choose a random item from the work queue, in order to avoid clashing element mutexes.
      rand_id = random() % queue->queue->count;

      if (!pthread_mutex_trylock(((work_item_t*)queue->queue->items[rand_id])->mutex)) {

        // obtain mutex locks on both elements for the work item.
        work_to_do = (work_item_t*)queue->queue->items[rand_id];

        if (!pthread_mutex_trylock(((element_t*)work_to_do->element_1)->mutex)){ 
          if (!pthread_mutex_trylock(((element_t*)work_to_do->element_2)->mutex)) {

            success = 1;
          } else {

            // only locked element_1 and work item:
            pthread_mutex_unlock(((element_t*)work_to_do->element_1)->mutex);
            pthread_mutex_unlock(work_to_do->mutex);
            work_to_do = NULL;
          }
        } else {

          // couldn't lock element_1, didn't even try 2:
          pthread_mutex_unlock(work_to_do->mutex);
          work_to_do = NULL;
        }
      }
    }

    if (work_to_do == NULL) {
       if (queue->queue->count == 0 && queue->exit_flag) {

        break;
      } else {

        continue;
      }
    }

    queue_remove_work_item(queue, rand_id, NULL, 1);
    pthread_mutex_unlock(work_to_do->mutex);

    pthread_mutex_unlock(queue->mutex);

    // At this point, we have mutex locks for the two elements in question, and a
    // work item no longer visible to any other threads. we have also unlocked the main
    // shared queue, and are free to perform the work on the elements.
    execute_function(
      work_to_do->interaction_function,
      (element_t*)work_to_do->element_1,
      (element_t*)work_to_do->element_2,
      (simulation_parameters_t*)work_to_do->params
    );

    // now finished, we should unlock both the elements:
    pthread_mutex_unlock(((element_t*)work_to_do->element_1)->mutex);
    pthread_mutex_unlock(((element_t*)work_to_do->element_2)->mutex);

    // and release the work_item RAM:
    work_item_destroy((void*)work_to_do);
    work_to_do = NULL;

    work_items_completed++;
    success = 0;
  }
  return NULL;
}

Ответы [ 3 ]

0 голосов
/ 29 ноября 2011

Это не похоже на random (), это ваша проблема, так как это один и тот же код независимо от количества потоков. Так как производительность падает с количеством потоков, вероятно, вы будете убиты блокировкой накладных расходов. Вам действительно нужно несколько потоков? Сколько времени занимает работа, и какова ваша средняя глубина очереди? Выбор предметов случайным образом кажется плохой идеей. Определенно, если число очередей <= 2, вам не нужно делать рандовый расчет. Кроме того, вместо случайного выбора индекса очереди было бы лучше просто использовать разные очереди для каждого рабочего потока и вставлять циклически. Или, по крайней мере, что-то простое, например, запоминание последнего индекса, заявленного предыдущим потоком, и просто не выбор этого. </p>

0 голосов
/ 29 ноября 2011

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

random() и друзья, которые имеют скрытую переменную состояния, могут стать серьезным препятствием для параллельного программирования. Если они сделаны поточно-ориентированными, это обычно делается путем мьютекса доступа, поэтому все замедляется.

Портативный выбор для поточно-ориентированного генератора случайных чисел в системах POSIX: erand48. В отличие от drand48 он получает переменную состояния в качестве аргумента. Вам просто нужно сохранить переменную состояния в стеке каждого потока (это unsigned short[3]) и вызвать erand48 с этим.

Также имейте в виду, что это псевдо случайные генераторы. Если вы используете одну и ту же переменную состояния между разными потоками, ваши случайные числа не являются независимыми.

0 голосов
/ 29 ноября 2011

Нити Python не являются реальными. Все потоки Python выполняются в одном потоке уровня ОС и выполняются по одному за один раз благодаря GIL - Global Interpreter Lock. Переписывание вашего кода с процессами может помочь, если работники относительно долго живут в контексте.

Страница Википедии на GIL

---- Изменить ----

Правильно, это было в с. Но GIL по-прежнему имеет значение. Информация о темах в расширениях c

...