Блокировка openmp возобновлена ​​тем же потоком, в то время как другой поток ожидает его - PullRequest
0 голосов
/ 04 июня 2018

Я использую инвертированную блокировку в качестве семафора для оповещения об обновлении очереди (обратите внимание на закомментированный Sleep(1), он будет использован позже):

#include <stdio.h>
#include <omp.h>
#include <queue>
#include <stdint.h>
#include <windows.h>

class ThreadLock
{
protected:
  omp_lock_t lock;

public:
  ThreadLock() {
    omp_init_lock(&lock);
  }
  ~ThreadLock() {
    omp_destroy_lock(&lock);
  }

  void acquire() {
    omp_set_lock(&lock);
  }

  void release() {
    omp_unset_lock(&lock);
  }
};

std::queue< uint32_t > g_queue;
ThreadLock g_lock;

void producer()
{
  uint32_t seq = 0;
  g_lock.acquire();
  while (true) {
    Sleep(200);
    #pragma omp critical
      g_queue.push(++seq);
    printf("Produced %u\n", seq);

    g_lock.release();
    //Sleep(1);
    g_lock.acquire();
  }
  g_lock.release();
}

void consumer()
{
  while (true) {
    // Lock if empty
    if (g_queue.empty()) {
      printf("[Consumer] Acquiring lock\n");
      g_lock.acquire();
      g_lock.release();
      printf("[Consumer] Released lock\n");
      if (g_queue.empty()) {
        printf("Still empty\n");
        Sleep(100);
        continue;
      }
    }

    #pragma omp critical
    {
      printf("Consumed %u\n", g_queue.front());
      g_queue.pop();
    }
  }
}

int main(int argc, char* argv[])
{
  #pragma omp parallel sections
  {
    #pragma omp section
      consumer();
    #pragma omp section
      producer();
  }

  return 0;
}

Этот код содержит условие гонки,который через некоторое время останавливает потребителя следующим образом:

[Consumer] Acquiring lock
Produced 1
Produced 2
[Consumer] Released lock
Consumed 1
Consumed 2
[Consumer] Acquiring lock
Produced 3
Produced 4
Produced 5
Produced 6
Produced 7
Produced 8
Produced 9
Produced 10
Produced 11
Produced 12
Produced 13
Produced 14
Produced 15
Produced 16
Produced 17
Produced 18
Produced 19

Похоже, что поток производителя проходит через освобождение / получение без переключения контекста.Хорошо.Давайте заставим его, раскомментировав Sleep(1):

[Consumer] Acquiring lock
Produced 1
[Consumer] Released lock
Consumed 1
[Consumer] Acquiring lock
[Consumer] Released lock
Still empty
[Consumer] Acquiring lock
Produced 2
[Consumer] Released lock
Consumed 2
[Consumer] Acquiring lock
[Consumer] Released lock
Still empty
[Consumer] Acquiring lock
Produced 3
[Consumer] Released lock
Consumed 3

Заметили эти Still empty строки?Похоже, что покупателю удается вставить дополнительный цикл обработки между линиями выпуска / приобретения производителя.

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

Как это можно сделать правильно, с OpenMP и без версий OpenMPвыше 2,0?

1 Ответ

0 голосов
/ 04 июня 2018

В вашем коде есть несколько проблем.Вы смешиваете #pragma omp critical и замок - что не имеет особого смысла.На самом деле вам нужна комбинация блокировки - для защиты всех операций в очереди - и условной переменной - для получения уведомлений о вставке элементов.К сожалению, OpenMP не предоставляет примитивы для условных переменных.Вы также можете использовать подсчитанный семафор для количества элементов в очереди - что также недоступно в OpenMP.

Тогда возникает проблема с голоданием, которую вы пытаетесь решить с помощью sleep - любой советдля ОС для переключения задач она не будет идеальной.Вы можете рассмотреть возможность использования задач OpenMP + taskyield (но это не OpenMP 2.0).

В конце концов, OpenMP не очень подходит для такой работы.OpenMP больше ориентирован на отображение 1 потока - 1 ядра и распределение параллельных циклов.Вы можете комбинировать потоки OpenMP с C ++ 11 std::lock / std::condition_variable.Хотя он, вероятно, будет работать на практике, он официально не поддерживается стандартом.

Примечание. При защите операций в очереди необходимо защитить все вызовы, включая g_queue.empty().

...