LINUX.ORG.RU

Использование блокировки для синхронизации блока данных между pthread'ами


0

1

Вообщем, есть примерно такой код:

// Блок данных из двух целых чисел:
int dx = 0;
int dy = 0;

int init_keys(WINDOW *window) {
  keypad(window,TRUE); // enable keyboard mapping (process function keys in KEY_ symbols)
  notimeout(window,TRUE);
  wtimeout(window,20); // ms
  cbreak(); // no wait for \n
  noecho();
  return 0;
}

...

// первая нить (поток)
void *funcKeyPress(void *p) {
  int ch;
  dx = 0;
  dy = 0;
  do {
    ch = wgetch(windowGame);
    if (ch == 0x1B) {
      fQuit = TRUE;
      break;
    }

    // adxw
    if (ch == 'a') {dx = -1; dy = 0;}
    if (ch == 'd') {dx = +1; dy = 0;}
    if (ch == 'x') {dx = 0; dy = +1;}
    if (ch == 'w') {dx = 0; dy = -1;}
    // arrows
    if (ch == KEY_LEFT) {dx = -1; dy = 0;}
    if (ch == KEY_RIGHT) {dx = +1; dy = 0;}
    if (ch == KEY_DOWN) {dx = 0; dy = +1;}
    if (ch == KEY_UP) {dx = 0; dy = -1;}

    // drawing

  } while(TRUE);
  return NULL;
}
Здесь dx,dy используются (вторая нить/поток):
void *funcDrawMap(void *p) {
  do {
    ...
    x_old = _p_x[iu];
    y_old = _p_y[iu];
    x_new = _p_x[iu]+dx; // вот здесь надо "защитить" пару переменных от записи первой нитью
    y_new = _p_y[iu]+dy;
    ...
    usleep(1000*200);
  } while(TRUE);
  ...
  return NULL;
}
Как лучше выполнить блокировку? Приветствуется пример в виде пары строчек исходного кода.

Прочитал http://randu.org/tutorials/threads/ - там подходящего примера нет.

Я так понимаю, mutex'ы здесь не нужны (они - для больших задержек)?

★★★★★

В первом варианте - write lock, во втором варианте - read lock. А в целом это называется RWLock.

Если у тебя один писатель и один читатель, думаю, можно будет не заморачиваться и заюзать спинлоки

yoghurt ★★★★★
()
Ответ на: комментарий от yoghurt

> Если у тебя один писатель и один читатель

Один писатель - и двое/трое читателей (всего будет 3-4 треда).

pacify ★★★★★
() автор топика
Ответ на: комментарий от pacify

Ну тогда rwlock'ы - то, что доктор прописал. Читатели смогут параллельно юзать переменные, не блокируя друг друга.

yoghurt ★★★★★
()
Ответ на: комментарий от yoghurt

> Если у тебя один писатель и один читатель, думаю, можно будет не заморачиваться и заюзать спинлоки

Спинлоки в юзер-спейсе использовать не следует, за исключением крайне специфических случаев.

Relan ★★★★★
()
Ответ на: комментарий от const86

>> Спинлоки в юзер-спейсе использовать не следует

Поделись секретом, почему?

Поясню на примере. Пусть у нас двухпроцессорная система и параллельно работают два треда. Тред А взял спинлок и что-то делает. В это же время тред Б на другом процессоре тоже хочет взять этот же лок и начинает спиниться, ожидая когда тред А отпустит спинлок. И тут ядро решает дать поисполняться треду Ц и вытесняет тред А. При этом тред Б продолжает спиниться, впустую расходуя выделенный ему квант процессорного времени! Эта проблема происходит оттого, что ядро не в курсе, занят тред какой-то полезной работой или спинится ожидая освобождения лока. Если же использовать мютекс, то в данном случае ядро заблокировало бы тред Б и запустило бы вместо него на том же процессоре тред Ц, в то время как тред А продолжил бы заниматься своими делами под локом на другом процессоре. Таким образом, полезной работы за единицу времени было бы выполнено больше, чем в случае со спинлоком.

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

Relan ★★★★★
()
Ответ на: комментарий от Relan

> И тут ядро решает дать поисполняться треду Ц и вытесняет тред А. При этом тред Б продолжает спиниться, впустую расходуя выделенный ему квант процессорного времени!

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

Если хочется совсем хорошо, можно дёргать sched_yield(), когда при попытке лока случится облом. Глибцевый спинлок так не делает, надо вручную делать цикл с trylock.

const86 ★★★★★
()
Ответ на: комментарий от const86

> Глибцевый спинлок так не делает, надо вручную делать цикл с trylock.

А за чем переизобретать pthreads?

LamerOk ★★★★★
()

Нашел про RWLock. Читаю. http://www.linux.org.ru/forum/development/311269

Думаю, будет где-то так:

// Блок данных из двух целых чисел:
int dx = 0;
int dy = 0;

int init_keys(WINDOW *window) {
  keypad(window,TRUE); // enable keyboard mapping (process function keys in KEY_ symbols)
  notimeout(window,TRUE);
  wtimeout(window,20); // ms
  cbreak(); // no wait for \n
  noecho();
  return 0;
}

...

// первая нить (поток)
void *funcKeyPress(void *p) {
  int ch;
  dx = 0;
  dy = 0;
  do {
    ch = wgetch(windowGame);
    if (ch == 0x1B) {
      fQuit = TRUE;
      break;
    }

    pthread_rwlock_wrlock ...
    // adxw
    if (ch == 'a') {dx = -1; dy = 0;}
    if (ch == 'd') {dx = +1; dy = 0;}
    if (ch == 'x') {dx = 0; dy = +1;}
    if (ch == 'w') {dx = 0; dy = -1;}
    // arrows
    if (ch == KEY_LEFT) {dx = -1; dy = 0;}
    if (ch == KEY_RIGHT) {dx = +1; dy = 0;}
    if (ch == KEY_DOWN) {dx = 0; dy = +1;}
    if (ch == KEY_UP) {dx = 0; dy = -1;}
    pthread_rwlock_unlock ...

    // drawing

  } while(TRUE);
  return NULL;
}

void *funcDrawMap(void *p) {
  do {
    ...
    pthread_rwlock_rdlock ...
    x_old = _p_x[iu];
    y_old = _p_y[iu];
    x_new = _p_x[iu]+dx; // вот здесь надо "защитить" пару переменных от записи первой нитью
    y_new = _p_y[iu]+dy;
    pthread_rwlock_unlock ...
    ...

    usleep(1000*200);
  } while(TRUE);
  ...
  return NULL;
}

Правильно?

pacify ★★★★★
() автор топика
Ответ на: комментарий от const86

> можно дёргать sched_yield(), когда при попытке лока случится облом

Эффект от sched_yield() малопредсказуем в общем случае. Тред уйдет в конец очереди планировщика и когда он снова будет спланирован на исполнение — ХЗ, возможно что очень нескоро. А в случае с мютексом ядро разбудит его как только мютекс будет освобожден (когда тред будет спланирован на исполнение — уже другой вопрос).

В общем, я за то, чтобы не извращаться, а просто использовать мютекс. :)

Relan ★★★★★
()

Не надо тут использовать rwlock... Он оправдан только при действительно большом числе reader'ов (high contention) и при длительных операциях с данными. Накладные расходы на него намного больше, чем на обычных мьютекс.

ratatosk
()
Ответ на: комментарий от Relan

> А в случае с мютексом

IMHO, поток встанет в тот же конец очереди, когда проснётся.

Для сферической двухпоточной проги, в вечном цикле инкрементирующей один защищённый счётчик: мутекс оказывается самым тормозным. Спинлок существенно быстрее. Ещё быстрее - тот же спинлок с trylock + sched_yield.

В общем, я за то, чтобы не извращаться, а просто использовать мютекс. :)

Я из тех же соображений голосую за спинлок. Разумеется, с озвученной выше оговоркой, что критическая секция короткая и без сисвызовов.

const86 ★★★★★
()
Ответ на: комментарий от ratatosk

http://hpaste.org/46210/locks_benchmark

druid@druid-desktop:~/tmp$ time ./rlocks
Testing rwlock implementation

real	0m9.045s
user	0m9.030s
sys	0m0.000s

druid@druid-desktop:~/tmp$ time ./mlocks
Testing mutex implementation

real	0m5.420s
user	0m5.380s
sys	0m0.010s

druid@druid-desktop:~/tmp$ time ./slocks
Testing spinlock implementation

real	0m1.692s
user	0m1.670s
sys	0m0.010s

ratatosk
()
Ответ на: комментарий от ratatosk

p.s. это -DNUM_ITERATIONS=100000000, Ubuntu x64, не самый свежый кор2-дуо.

ratatosk
()
Ответ на: комментарий от ratatosk

> http://hpaste.org/46210/locks_benchmark

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

const86 ★★★★★
()
Ответ на: комментарий от const86

> добавить в сравнение посиксный спинлок.

Насколько кроссплатформенны спинлоки?
Интересует Ubuntu, Centos 4, FreeBSD.

pacify ★★★★★
() автор топика

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

надо делать примерно как здесь:

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int a = 0;

//поток1
void *T1(void*) 
{
	while(true) 
	{
		pthread_mutex_lock(&mutex);
			printf("T1 %i\n", a);
			a++;
		pthread_mutex_unlock(&mutex);
		sleep(1);
	}
	return 0;
}

//поток 2
void *T2(void*)
{
	while(true) 
	{
		pthread_mutex_lock(&mutex);
			printf("T2 %i\n", a);
			a++;
		pthread_mutex_unlock(&mutex);
		sleep(2);
	}
	return 0;
}

спины сделаны для другого.

не надо читать статьи по этой теме на русском, в английском то информации мало, даже в переведенных книжках почему-то ошибки в примерах :(

wal
()
Ответ на: комментарий от wal

Спасибо за пример. Я так и сделал. Только вот это пропустил:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pacify ★★★★★
() автор топика
Ответ на: комментарий от pacify

PTHREAD_MUTEX_INITIALIZER это макрос и позволяет инициализировать статический mutex и не использовать pthread_mutex_init

если все равно используется pthread_mutex_init то PTHREAD_MUTEX_INITIALIZER писать не обязательно

wal
()
Ответ на: комментарий от wal

> здесь не надо извращаться и использовать спины

Так говоришь, будто со спинами код становится втрое длиннее и запутаннее.

так как блокировка все равно необходима, а конкуренция между потоками никакая (вероятность попадания очень низкая)

Блокировка в каком смысле? Синхронизация нужна. А ожидание не нужно. У ТС чтение и инкремент+запись двух переменных. Ему и atomic хватило бы, но лучше не надо: можно словить глюк, если вдруг захочется изменить атомарно обе переменные.

спины сделаны для другого.

Неубедительно, поясни.

const86 ★★★★★
()
Ответ на: комментарий от const86

>Так говоришь, будто со спинами код становится втрое длиннее и запутаннее.

если код правильный, то он будет и длиннее и запутанней, т.е. как минимум надо написать if (количество процессоров>1) используем спинлок else блокируем mutex;

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

Блокировка в каком смысле? Синхронизация нужна. А ожидание не нужно.

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

wal
()
Ответ на: комментарий от wal

Отвечу цитатами себя любимого:

если код правильный, то он будет и длиннее и запутанней, т.е. как минимум надо написать if (количество процессоров>1) используем спинлок else блокируем mutex;

Трёхстрочная обёртка вокруг trylock+sched_yield вместо голого pthread_spin_lock.

использовать spinlock неразумно, например при одном процессоре (ядре), ведь такая блокировка не отпускает процессор, а циклически его гнобит

Вероятность напороться на переключение процессов внутри критической секции обычно крайне низка. И проблема решается трайлоком, если всё-таки надо.

const86 ★★★★★
()
Ответ на: комментарий от wal

> философ? а программы писать пробовал?

Пробовал. Всё написанное мной в этом треде проверял прогами. Приведёшь конкретный сценарий, в котором мои варианты покажут себя плохо?

const86 ★★★★★
()
Ответ на: комментарий от const86

>Пробовал. Всё написанное мной в этом треде проверял прогами. Приведёшь конкретный сценарий, в котором мои варианты покажут себя плохо?

Да в ненагруженных средах это может тормозить незаметно, конечно все зависит от алгоритма, и блокировки могут появляться раз в год :)

обычные mutex-ы работают надежней, хотя в некоторых случаях надо много думать (текущий случай - не тот)

wal
()
Ответ на: комментарий от wal

> обычные mutex-ы работают надежней, хотя в некоторых случаях надо много думать (текущий случай - не тот)

Согласен в том, что мутекс работает предсказуемо, как его ни крути. Но медленнее.

const86 ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.