LINUX.ORG.RU

Синхронизация потоков

 ,


0

1

Есть код такого вида:

#include <stdatomic.h>

struct { long l; char s[8]; } queue[1024 * 1024];
_Atomic int qsize;

void process_queue() {
  extern void process(typeof(*queue));
#ifdef X86
  for (int i; (i = atomic_fetch_sub_explicit(&qsize, 1, memory_order_acq_rel)) > 0; process(queue[i - 1]));
#else
  int i = atomic_fetch_sub_explicit(&qsize, 1, memory_order_acquire);
  while (i > 0) { process(queue[i - 1]); i = atomic_fetch_sub_explicit(&qsize, 1, memory_order_relaxed); }
  atomic_fetch_sub_explicit(&qsize, 1, memory_order_release);
#endif
}
void extra_thread() {
  while (1) { while (atomic_load_explicit(&qsize, memory_order_relaxed) <= 0); process_queue(); }
}
void main_thread() {
  while (1) {
    extern int thread_count;
    extern int get_data(typeof(queue), int);
    atomic_store_explicit(&qsize, get_data(queue, sizeof(queue) / sizeof(*queue)), memory_order_release);
    process_queue();
    while (atomic_load_explicit(&qsize, memory_order_relaxed) !=
#ifdef X86
      -thread_count + 1
#else
      -thread_count + 1 << 1
#endif
    );
  }
}

С х86 вопросов нет. Хоть как пиши - оно будет работать/тормозить одинаково. С чем-то другим не совсем понятно. Можно ли сделать лучше, чем мой вариант под #else?

В get_data сисколы и в реальности можно просто писать relaxed везде и всё будет работать. Но интересно как сделать «по правилам»(из стандарта).

Что ты пытаешься добиться?

Ну и исправь стиль кода - не надо писать extern-ы внутри функций, не надо абузить аргументы for для объявления переменных и для описания тела цикла. Код сложно читать.

firkax ★★★★★
()

С х86 вопросов нет

У меня один вопрос, что это за дерьмо такое?

#ifdef X86

что ты тут такое платформозависимое пытаешься сделать? Если что кресты достаточно высокоуровневые, чтоб мультитред делать не привязываясь к архитектуре процессора.

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

Что ты пытаешься добиться?

main_thread/extra_thread - специально эти дописал. Кратко: приходит пачка данных и нужно их обработать неким образом. Обработка одного элемента с остальными не пересекается. Прожевать пришедшую пачку поскорее и пойти за следующей. Всё.

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

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

Вот, это совсем идеально было бы, лишние ifdef’ы не хочу. Знаешь как сделать?

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

А тебе как надо? Вариантов то много есть как мультипоток устроить и на чём именно, может ты вообще хочешь GPU юзать. И разберись какие ОС нужны, какие платформы и какой метод синхронизации.

peregrine ★★★★★
()

У тебя уже есть готовое приложение с очередью на мутексе, ты его отпрофайлил и понял что узкое место именно в нём?

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

Дружок, это у тебя проблемы, а не у меня. Это ты не знаешь для чего в крестах есть следующие заголовки

#include <iostream>
#include <thread>
#include <chrono>
#include <semaphore>

Да и про thread.join ты тоже не слышал и потому изобретаешь велосипеды. А уж про чуть более сложные штуки, вроде пула потоков (которые в свою очередь надо делить на 3 типа и в зависимости от того что там узкое cpu/блокирующее io/неблокирующее io и работать с ними по-разному в зависимости от того что тебе надо) или даже сраного openmp и упаси бог opencl с cuda ты вообще ничего не слышал кроме новостей. Так что почитай про эти термины и напиши что именно из всех этих вариантов тебе надо.

ЗЫ

Тут далеко не все варианты перечислены, но мне лень более редкие технологии и техники описывать.

peregrine ★★★★★
()

Для начала отформатировать нормально код и запихнуть в каждую ветку ифдефа свои реализации. Эту портянку читать невозможно. Во вторых — подумать и написать понятно <<что конкретно хочется сделать>>.

thunar ★★★★★
()
Последнее исправление: thunar (всего исправлений: 3)
Ответ на: комментарий от peregrine

Для низкоуровневой синхронизации есть stdatomic и тред о нём
Для чего-то более высокоуровневого - c++ треды - лишь обёртка над posix/winapi, которая нередко работает хуже чем нижестоящие API.
Однако тригернула меня первая строчка: iostream совершенно не место в коде.
Остальное - код chrono тяжело читать, а stl'овские треды просто не удобные, серьёзных проблем как у какого-нибудь std::string конечно они не вызовут и это просто вкусовщина

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

Только стиль кода там будет приятнее, а API ближе к задаче.
Есть кстати ещё вариант - писать под c11threads, но он не везде поддерживается и для неподдерживаемых платформ придётся вендорить обёртку, как это например делается в mesa

mittorn ★★★★★
()

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

anonymous
()

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

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

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

Ну например тут неплохо расписано:
https://stackoverflow.com/a/3176640
В целом для меня лично кажется неудобным и сложно читаемым оператор сдвига для вывода.
Это конечно хорошая демонстрация возможности перегрузки операторов, но не очень хорошая практика. Если в этой же строчке появятся параметры шаблонов и операторы сравнения, она станет совершенно нечитаемой (вобще использование <> для шаблонов наверно было ошибкой, но что есть - то есть)

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

Ну например тут неплохо расписано

Фигня там какая-то написана. 1 - исключения стд должна кидать по минимуму, это универсальный код, нельзя там играться в исключения, может у меня коды возврата осознанно на вызывающей стороне и портянку TRY-catch мне даром не нужны. Исключения из стд - крайний случай, когда вообще иначе никак - например, надо вернуть ссылку, но валидного объекта под неё нет, или юзер явно попросил кидать эксемшены, такой должна быть логика. Более того исключения - сложная штука, очень многие её даже не осилят правильно, и имеют смысл только в сложных проектах и прямых руках.

  1. Спорно, а мне может хочется один раз настроить hex вывод, например, и писать 100500 раз. К тому же есть std::format для желающих «stateless iostream». «Затраты на сохранение состаяния», это серьёзно вообще? Пара десятков байт для десктопа - проблема? Никто не пихает iostream в мк, хотя и для них данный аргумент смешон. МК размером с ноготь и стоимостью в доллар мизинца умеет современный wifi с норм скоростями, dhcp, стек tcp/ip вплоть до всяких https, а ты пугаешь людей на десктопных суперкомпьюетерах десятой лишних байт на хранение состояния? Это сказки лишь для ТС’а, который считает каждый такт цп (ошибается, конечно, на деле даже мьютекс не сильно проиграет атомику, а если гонять не голый for с полезной нагрузкой, то разница будет в районе погрешности).

  2. Есть std::format. Знаете ли вы, что iostream быстрее сишного printf? Не потому ли это, что последнему нужно парсить формат сринг?

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

В тегах кресты, значит имеем с++ челика, который сишные либы юзает из крестов. Но даже если ему чистый си надо, то помним что этот файл в 11 стандарте опционален и у меня нет уверенности что всякие ардуинки умеют в 17 стандарт и выше, уж если про покрытие платформ как ТС хочет говорить. Но даже если у него всё есть, то openmp работает и с чистым си, а вообще в си надо мазаться pthreads в случае линукса и POSIX-а, да и в оффтопике можно, вариантов как его там заставить работать масса, хотя там есть тонкости. Ещё есть более современный вариант с threads.h если стандарт старше 11, там как раз тот велосипед что ТС городит вокруг атомика уже сделан и баги отловлены и можно мазаться абстракциями чуть выше тупого атомика.

anonymous
()

Атомики - сложная вещь, и не всегда они лучше и быстрее тех же мьютексов. А мьютексы сильно проще и в понимании, и в поддержке. И хорошо ведут себя даже под нагрузкой.

Присоединяюсь к отписавшимся выше. Вам точно атомики нужны, чтобы усложнить код на ровном месте?

anonymous
()

оно будет работать/тормозить одинаково.

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

Но интересно как сделать «по правилам»(из стандарта).

Параллельное программирование на C++ в действии

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

Знаете ли вы, что iostream быстрее сишного printf?

// cat test_iostream.cpp 
#include <iostream>

int main() {
  for (int i = 0; i < 10000000; ++i)
    std::cout << "num: " << 42.42 << '\n';
  return 0;
}
cat test_printf.cpp 
#include <cstdio>

int main() {
  for (int i = 0; i < 10000000; ++i)
    printf("num: %0.2f\n", 42.42);
  return 0;
}
$ time ./test_iostream > /dev/null

real	0m2.965s
user	0m2.953s
sys	0m0.010s

$ time ./test_printf > /dev/null

real	0m1.754s
user	0m1.753s
sys	0m0.000s
anonymous
()
Ответ на: комментарий от anonymous

И ещё - сделай cout. sync_with_stdio(false)

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

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

-O3

Самому проверить слабо с разными оптимизациаями? (И вряд ли существенно повлияет, так как вызываются системные so-шки)

И да, «iostream» - это обертка над «stdio.h»

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

Это читы.

// cat test_raw.cpp
#include <unistd.h>

#define MSG "num: 42.42\n"

int
main ()
{
  for (int i = 0; i < 10000000; ++i)
    {
      write (STDOUT_FILENO, MSG, sizeof (MSG));
    }
  return 0;
}
$ time ./test_iostream > /dev/null

real    0m1,815s
user    0m1,809s
sys     0m0,003s
$ time ./test_printf > /dev/null

real    0m1,066s
user    0m1,058s
sys     0m0,007s
$ time ./test_raw > /dev/null

real    0m1,598s
user    0m0,437s
sys     0m1,160s

В итоге printf быстрее чем write. Читы. Попробуйте g++ --no-printf-cheats.

thegoldone ★★
()
Последнее исправление: thegoldone (всего исправлений: 1)
Ответ на: комментарий от anonymous

Дополню свои слова. Чуть по-строже.

Если вы не студент, то вы, верно, хотите того, чтобы вас разбудили по среди ночи и заставили исправить ваш код, из-за которого упал весь прод на миллионы денег? Или у вас просто других задач нет. На месте вашего тимлида я бы крепко задумался.

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

anonymous
()

оно будет работать/тормозить одинаково

оно будет работать

Жесть какая. И как только кому-то в голову пришло вот это вот гордо именовать очередью? Колёса квадратные, а мы за градиент цвета боковых зеркал переживаем…

bugfixer ★★★★★
()
Ответ на: комментарий от anonymous
// c
#include <stdio.h>
int main()
{
	const char *s1 = "dkjfdkjfkdfjkd";
	const char *s2 = "dkjfdkjfkdfjkd";
	const char *s3 = "dkjfdkjfkdfjkd";

	FILE* fp = fopen("/tmp/rr", "w");
	for (int i = 0;  i < 10000000;  ++ i)
		fprintf(fp, "%s %s %s", s1, s2, s3);
}


// cpp
#include <fstream>
using namespace std;
int main()
{
	const char *s1 = "dkjfdkjfkdfjkd";
	const char *s2 = "dkjfdkjfkdfjkd";
	const char *s3 = "dkjfdkjfkdfjkd";

	ofstream f("/tmp/rr2");
	for (int i = 0;  i < 10000000;  ++ i)
		f << s1 << ' ' << s2 << ' ' << s3;
}

$ time ./c
real	0m1.872s

$ time ./cpp
real	0m1.651s

Такой тест я делал, правда на другом компе разница была более существенной, раза в полтора в пользу цпп

Про скорость iostream может говорить только тот, кто никогда не интересовался этой скоростью

Ой пля сколько пафоса. Ты погоди из штанишок выпрыгивать. Реальность в том, что это не какие-то там тяжелые цпп абстракции, а в том, что operator<< не инлайнится, он уходит в libstdc++ (я посмотрел отладчиком). Т.е. твой сишный пример обгоняет лишь потому, что он делает лишь один вызов вместо нескольких.

А вывод напрашивается всего один цппшные ио потоки настолько круты, что узким местом становятся накладные расходы на вызов функций.

Ты никуда не уйдешь от того простого факта, что парсить строку в рантайме - это дороже, чем её не парсить, это очевидно

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

Как обстоят дела с вывовдом не строк, например, чисел с плавющей точкой?

Ты никуда не уйдешь от того простого факта, что парсить строку в рантайме - это дороже, чем её не парсить, это очевидно

Проблема iostream в том, что это виртуальное наследование (косвенные вызовы), тормозные манипуляторы (форматирование, преобразование (чисел)).

Парсинг форматной строки - не самая тяжелая часть.

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

anonymous
()
Ответ на: комментарий от anonymous
// c
#include <stdio.h>

int
main ()
{
  double s01 = 15.15;
  double s02 = 25.25;
  double s03 = 75.75;
  double s04 = 15.15;
  double s05 = 25.25;
  double s06 = 75.75;
  double s07 = 15.15;
  double s08 = 25.25;
  double s09 = 75.75;
  double s10 = 15.15;
  double s11 = 25.25;
  double s12 = 75.75;

  for (int i = 0; i < 10000000; ++i)
    {
      printf ("%f %f %f %f %f %f %f %f %f %f %f %f", s01, s02, s03, s04, s05,
              s06, s07, s08, s09, s10, s11, s12);
    }
}

// cpp
#include <iostream>

using namespace std;

int
main ()
{
  cout.sync_with_stdio (false);

  double s01 = 15.15;
  double s02 = 25.25;
  double s03 = 75.75;
  double s04 = 15.15;
  double s05 = 25.25;
  double s06 = 75.75;
  double s07 = 15.15;
  double s08 = 25.25;
  double s09 = 75.75;
  double s10 = 15.15;
  double s11 = 25.25;
  double s12 = 75.75;

  for (int i = 0; i < 10000000; ++i)
    {
      std::cout << s01 << ' ' << s02 << ' ' << s03 << ' ' << s04 << ' ' << s05
                << ' ' << s06 << ' ' << s07 << ' ' << s08 << ' ' << s09 << ' '
                << s10 << ' ' << s11 << ' ' << s12;
    }
}


$ time ./outc > /dev/null

real    0m13,423s
user    0m13,365s
sys     0m0,040s

$ time ./outcpp > /dev/null

real    0m17,406s
user    0m17,369s
sys     0m0,017s
thegoldone ★★
()
Последнее исправление: thegoldone (всего исправлений: 1)
Ответ на: комментарий от anonymous

Проблема iostream в том, что это виртуальное наследование (косвенные вызовы), тормозные манипуляторы (форматирование, преобразование (чисел)).

Ну и в си нужно преобразовывать и числа. Знаешь, сам факт того, что теорема вида «есть случаи, когда цпп потоки рвут си аналоги» доказана уже говорит о том, что они годные и не тормозные. Со всей своей виртуальностью и абстракциями. А всё это ко-ко-ко по поводу «да оно пипец какое тормозное» полная брехня

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

Ты заниаешься хней. Если ты перепишешь сишный цикл так:

printf ("%f", s01);
printf ("%f", s02);
printf ("%f", s03);
printf ("%f", s04);
printf ("%f", s05);
printf ("%f", s06);
printf ("%f", s07);
printf ("%f", s08);
printf ("%f", s09);
printf ("%f", s10);
printf ("%f", s11);
printf ("%f", s12);

то разница будет около секунды. Кол-во вызовов сильно влияет на результат, это говорит, что скорость цппшных ИО потоков очень высока, что тормоза не на всем том, о чем ты говоришь, а на организации вызовов. Ты берешь голый for (условно), и пытаешься там что-то замерять, вот какой вывод, без всякой полезной нагрузки.

Похоже, что если нет возможности заинлайнить, то парсинг аргументов в компайл тайме не даёт того, что я от него ожидал, лишь увеличивает кол-во вызовов

anonymous
()