LINUX.ORG.RU

Насколько bool thread-safe

 ,


2

4
class BlaBla
{
public:
    BlaBla() :
        mbValue(false)
    {}

    bool value() const {
        return mbValue;

    void compute(...) {
        mbValue = true / false;
    }

private:
    bool mbValue;
};

Опустим здесь подробности расчёта. Интересен только bool, насколько он thread-safe здесь?

★★★★★

Ты точно понимаешь, что такое thread safe?

anonymous ()

Может ты хотел сказать atomic? Нет, bool не atomic. В зависимости от архитектуры и имплементации, за кулисами может происходить много всего.

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

так ты ж его никуда из объекта не передаёшь. будет в том же потоке, что и объект

Вызвал compute() из одного потока, value() - из другого.

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

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

UVV ★★★★★ ()

When an evaluation of an expression writes to a memory location and another evaluation reads or modifies the same memory location, the expressions are said to conflict. A program that has two conflicting evaluations has a data race unless

  • both evaluations execute on the same thread or in the same signal handler, or
  • both conflicting evaluations are atomic operations (see std::atomic), or
  • one of the conflicting evaluations happens-before another (see std::memory_order)

If a data race occurs, the behavior of the program is undefined.

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

Это с cppreference, ну да не важно. По стандарту это UB. На разных архитектурах оно ведёт себя по-разному, например, на x86_64 чтение и запись атомарны вплоть до 64-битных чисел, а переупорядочивания почти нет. Но к c++ это уже не относится.

Laz ★★★★★ ()

С точки зрения компилятора он не thread-safe в том смысле, что компилятор будет пытаться закешировать mbValue по возможности и делать другие оптимизации, которые исходят из предположения однопоточности. А это может сломать всю логику. Может прийти в голову использовать volatile, но тогда уж сразу atomic.

xaizek ★★★★★ ()

Ещё одна тема в пользу Rust, у растоманов такая тема бы не возникла :(

Моя вера в безопасный С++ ещё немного подорвана.

Ладно, вот тебе лайфхак как проверять свои гипотезы:

Написать какую-нибудь чушь для теста:

#include <stdio.h>
#include <thread>
#include <atomic>

class BlaBla
{
public:
    BlaBla() :
        mbValue(false)
    {}

    bool value() const {
        return mbValue;
    }

    void compute() {
        mbValue = !mbValue;
    }

private:
    bool mbValue;
};

std::atomic<bool> run;
long res = 0;

void foo(const BlaBla& a)
{
	while(run)
	{
		if (a.value())
			++res;
		else
			--res;
	}
}

void bar(BlaBla& a)
{
	while(run)
	{
		a.compute();
	}
}

int main()
{
	run = true;
	BlaBla bla;
	std::thread t1(foo, std::ref(bla));
	std::thread t2(bar, std::ref(bla));
	puts("Press enter to exit");
	getchar();
	run = false;
	t1.join();
	t2.join();
	printf("%ld\n", res);
}

скомпилировать

g++ -O2 -fsanitize=thread -pthread main.cpp -o test

Запустить:

./test 
Press enter to exit
==================
WARNING: ThreadSanitizer: data race (pid=2456)
  Write of size 1 at 0xfffffffff0d0 by thread T2:
    #0 bar(BlaBla&) <null> (test+0x13bb)
    #1 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(BlaBla&), std::reference_wrapper<BlaBla> > > >::_M_run() <null> (test+0x1423)
    #2 <null> <null> (libstdc++.so.6+0xba1f3)

  Previous read of size 1 at 0xfffffffff0d0 by thread T1:
    #0 foo(BlaBla const&) <null> (test+0x132f)
    #1 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(BlaBla const&), std::reference_wrapper<BlaBla> > > >::_M_run() <null> (test+0x1473)
    #2 <null> <null> (libstdc++.so.6+0xba1f3)

  Location is stack of main thread.

  Location is global '<null>' at 0x000000000000 ([stack]+0x0000000200d0)

  Thread T2 (tid=2459, running) created by main thread at:
    #0 pthread_create <null> (libtsan.so.0+0x2ee37)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xba4cb)
    #2 main <null> (test+0x110f)

  Thread T1 (tid=2458, running) created by main thread at:
    #0 pthread_create <null> (libtsan.so.0+0x2ee37)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xba4cb)
    #2 main <null> (test+0x10eb)

SUMMARY: ThreadSanitizer: data race (/home/fsb4000/test/test+0x13bb) in bar(BlaBla&)
==================

-11947583
ThreadSanitizer: reported 1 warnings

Убедиться, что хоть bool и 1 байт, это не атомик тип, и что не зря есть std::atomic<bool>

fsb4000 ★★★★★ ()

Архитектура современных процессоров такова, что без примитивов синхронизации ты сначала в одном потоке может записать одно значение, а в другом потоке уже после прочитать не новое, а старое значение, как бы это нелогично ни звучало. Не знаю, насколько спасает в C++ volatile в таких случаях. В java помог бы, но там все чуточку сложнее (там volatile превратится по факту в atomic).

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

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

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

Архитектура современных процессоров такова, что без примитивов синхронизации ты сначала в одном потоке может записать одно значение, а в другом потоке уже после прочитать не новое, а старое значение

Не знаю, насколько спасает в C++ volatile в таких случаях

Причем тут процессоры если без volatile компилятор просто выкидывает из программы бессмысленные по его мнению действия (присвоение переменной значения в твоем первом потоке).

anonymous ()

Это небезопасно просто потому, что компилятор может соптимизировать и не сохранять это значение в памяти вообще. Будет в EAX его держать каком-нибудь. Законом не запрещено. Понятно, что другой тред тогда никогда не увидит изменение этого значения. Чтобы такого не происходило, попробуй вкурить volatile. Вроде это оно.

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

Там не только бессмысленные действия. Доступ к памяти - вообще, штука сложная.

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

там volatile превратится по факту в atomic

В Java есть настоящие atomic, не понимаю только почему все пытаются использовать volatile вместо атомиков для синхронизации...

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package...

Жалко что thread sanitizer лишь в драфте пока, а то можно было бы проверить atomic и volotile в Java.

https://openjdk.java.net/jeps/8208520

fsb4000 ★★★★★ ()
Последнее исправление: fsb4000 (всего исправлений: 2)
Ответ на: комментарий от xaizek

компилятор будет пытаться закешировать mbValue по возможности

Э… Насколько я понимаю, если mbValue является членом класса, то никакого кеширования не будет. Будет всегда происходить чтение из памяти. Поэтому цикл в foo() медленнее цикла в bar() - на каждой итерации будет происходить чтение m_val из памяти.

class A {
   int m_val = 10;
   void foo() {
      for (int i = 0; i < m_val; i++) {
      }
   }    
   void bar(int val = 10) {
      for (int i = 0; i < val; i++) {
      }
   }    
};
andreyu ★★★★★ ()
Ответ на: комментарий от fsb4000
TSan is supported on:

    Linux: x86_64, mips64 (40-bit VMA), aarch64 (39/42-bit VMA), powerpc64 (44/46/47-bit VMA)

Arm’a нет… (

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

Насколько я понимаю, если mbValue является членом класса, то никакого кеширования не будет

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

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

Не знаю, насколько спасает в C++ volatile в таких случаях

Не спасает. Только атомики. volatile в джаве - не то же самое, а в крестах он вообще на грани deprecated.

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

Зачем ты пытаешься угадать детали реализации? Есть стандарт языка, в нем есть модель памяти, и она четко говорит: пиши std::atomic_bool.

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

Да, я понял, что в общем случае так будет лучше. Но сами атомики нифига недешёвые, поэтому и пытаюсь понять, где он точно нужен, а где можно и обойтись. На какой-то конфе (вроде cppcon) недавно кто-то проводил тесты и получается, что атомики отрабатывают в 1000 раз дольше почти.

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

Вот я тоже точно усвоил, что с потоками volatile не нужен. Об этом много раз уже писали. Удивлён, что столько людей его сюда предлагают воткнуть.

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

Если m_var - член класса, а var - локальная переменная, то:

  1. Если m_val не обернута в mutex или не является atomic и ее меняют из другого потока, то там может быть что угодно.
  2. Чтение из m_val будет всегда медленнее, чем чтение из локальной var.
andreyu ★★★★★ ()
Ответ на: комментарий от UVV

что атомики отрабатывают в 1000 раз дольше почти.

Но при этом они быстрее, чем оборачивание в мьютекс.

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

Вот я тоже точно усвоил, что с потоками volatile не нужен

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

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

Если ты уперся по скорости в атомики, то лучше оптимизировать их загрузки/сохранения, по типу

   void foo() {
      auto val = m_val.load();
      for (int i = 0; i < val; i++) {
      }
   }    

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

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

volatile в джаве - не то же самое

Да тоже самое, volatile лишь говорит что переменная может измениться где-то в другом месте, то есть компилятор не будет её кешировать, но это не atomic, везде об этом и сразу пишут:

In short – a volatile variable is a variable, which can be changed from anywhere at any time, and thus cannot be optimized away. One consequence is that all the threads see the changes in them at once, but don’t actually set a lock on them. Atomic variables, on the other hand, are guaranteed to update all the threads and don’t allow to change their values asynchronously.

https://www.javamex.com/tutorials/synchronization_volatile.shtml

https://stackoverflow.com/questions/19744508/volatile-vs-atomic

https://stackoverflow.com/questions/7805192/is-a-volatile-int-in-java-thread-...

https://studiofreya.com/java/java-volatile-vs-atomic-variables/

Как бы наличие в языке двух сущностей говорит о том что это разные вещи...

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

В java плаваю, но емнип там volatile имеет эффект барьера, а в крестах - нет. И в принципе там его разрабатывали для многопоточности, и он для нее в итоге работает, пусть более ограниченно чем атомики.

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

Ещё одна тема в пользу Rust, у растоманов такая тема бы не возникла :(

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

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

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

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

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

поэтому и пытаюсь понять, где он точно нужен, а где можно и обойтись.

Очевидно для общих данных без синхронизации не обойтись. Или мьютексы, или атомики

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

Ещё одна тема в пользу Rust, у растоманов такая тема бы не возникла :( С чего вдруг? Если даже принять твой тезис, то не возникла она потому, что запрещён шаринг. Но здесь растоман уже пожрал говна, но что тебе мешает его пожрать? Запрети себе шарить данные.

Убедиться, что хоть bool и 1 байт, это не атомик тип Только в общем случае, который мало кому интересен.

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

Не может. Тебе же выше написали, что никого не интересует согласованность пошаренных данных между потоками. Интересует то - будут ли эти данные поломаны/порваны. Свой там не свой l1 у ядер - неважно.

anonymous ()

Достаточно threadsafe для оптимистичной синхронизации, при условии, что корректно выровнен. Ну, по крайней мере на x86[_64]. Т.е. не консистентных данных там оказаться не может, другое дело, что разные потоки могут увидеть разную «версию» данных. Поэтому точка линеаризации - всё равно нужна, но может быть организованна эффективно с помощью интристиков, барьеров или тупо локов(если оптимистичное предположение сбылось).

Использовать для синхронизации нельзя в общем, но и испортить довольно сложно.

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

Насколько я понимаю, если mbValue является членом класса, то никакого кеширования не будет.

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

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

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

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

Что это за магия такая?

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

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

как-то проходил собеседование в одной крупной компании и сказал об этом проводившему его сеньору. Человек сделал большие глаза. Так что миф крайне распространенный.

на самом деле volatile придумали, чтобы правильно обращаться с memory-mapped files, и ни для чего другого. Ну еще пароли обнулять при выходе из функции.

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

Компилятор должен будет каждый раз перечитать значение из памяти

Так я спрашиваю с чего бы это? Что такого магического в this указателе? Обычные указатели не блокируют оптимизиции, так с чего бы this это делать?

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

это верно, но тс хочет получить thread-safety вообще без ничего. модификатор volatile работает так же как atomic::load(memory_order_relaxed). и даже иногда встречаются задачи, где это имеет смысл:

[code] while (!ready/.load(memory_order_relaxed)/) { sleep(1); } [/code]

и это даже будет работать, но только в том случае, если ready может поменяться с false на true и не может обратно.

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

поправил форматирование

while (!ready/*.load(memory_order_relaxed)*/) {
    sleep(1);
}
anonymous ()
Ответ на: комментарий от xaizek

Так я спрашиваю с чего бы это?

Потому, что компилятор не может предполагать, что член класса не будет изменен где-то снаружи.

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

компилятор не может предполагать, что член класса не будет изменен где-то снаружи.

А ему и не нужно предполагать. Он может это знать. Если на участке кода всё было встроенно и никакой неизвестный код не вызываеся, то незаметное изменение значения поля может быть исключено, хоть и не всегда. Например, с полем bool и наличием указателя на bool в аргументах и записью в этот bool strict-aliasing убъёт эту оптимизацию. Но в общем случае никаких гарантий на отсутствие оптимизаций из-за косвенности через указатель на агрегат нет.

xaizek ★★★★★ ()
Последнее исправление: xaizek (всего исправлений: 1)

Ребята, вам срочно нужно прочитать «What every programmer should know about memory»! Если вы не имеете представления, как в компьюторе работает память, то ваша ценность, как (около-)системных программистов сильно преувеличена.

Продвинутые системные программисты уже мыслят в категориях процессорных багов: как организовать свои данные и паттерны доступа так, чтобы данные нельзя было вытянуть из неположенного места. А вы тут: bool thread-safe, не thread-safe…

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

Вот вроде правильные вещи говоришь, но не в этот раз

мыслят в категориях процессорных багов

Неплохо бы пример привести, зачем мыслить в таких категориях при разработке ПО

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

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

P. S.: ТС-а и волатильщиков это не оправдывает, конечно

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