Паралелни записи на една и съща стойност

Имам програма, която ражда множество нишки, които могат да запишат точно същата стойност в точно същото място в паметта:

std::vector<int> vec(32, 1); // Initialize vec with 32 times 1
std::vector<std::thread> threads;

for (int i = 0 ; i < 8 ; ++i) {
    threads.emplace_back([&vec]() {
        for (std::size_t j = 0 ; j < vec.size() ; ++j) {
            vec[j] = 0;
        }
    });
}

for (auto& thrd: threads) {
    thrd.join();
}

В този опростен код всички нишки може да се опитат да запишат точно същата стойност в едно и също място в паметта в vec. Има ли вероятност това състезание за данни да предизвика недефинирано поведение или е безопасно, тъй като стойностите никога не се четат, преди всички нишки да бъдат обединени отново?

Ако има потенциално опасна надпревара за данни, ще бъде ли достатъчно използването на std::vector<std::atomic<int>> вместо с std::memory_order_relaxed магазини, за да се предотврати надпреварата с данни?


person Morwenn    schedule 13.04.2014    source източник
comment
Всъщност е сравнително лесно да се определи дали нещо е data-race-UB или не: Ако повече от един запис, но не и четене, могат да бъдат изпълнени едновременно, вие сте в беда. Ако повече от едно чете, но не се случва запис, вие сте добре. Ако едно писане и поне едно четене се случат едновременно, пак си прецакан. Кратко: (›1 запис) ИЛИ (запис+четене) е проблем.   -  person stefan    schedule 13.04.2014
comment
Използвайте атоми и сте в безопасност. Не виждам причина да въвеждаме UB тук.   -  person usr    schedule 13.04.2014
comment
свързани   -  person user28667    schedule 22.07.2020


Отговори (3)


Отговор на езиков адвокат, [intro.multithread] n3485

21 Изпълнението на програма съдържа надпревара за данни, ако съдържа две конфликтни действия в различни нишки, поне едното от които не е атомарно и нито едно не се случва преди другото. Всяка такава надпревара за данни води до недефинирано поведение.

4 Две оценки на изрази конфликт, ако едната от тях модифицира място в паметта, а другата има достъп или модифицира същото място в паметта.


ще бъде ли достатъчно използването на std::vector<std::atomic<int>> вместо с std::memory_order_relaxed магазини, за да се предотвратят състезания за данни?

да Тези достъпи са атомарни и има връзка случва се преди, въведена чрез съединяването на нишките. Всяко последващо четене от нишката, раждаща тези работници (която е синхронизирана чрез .join), е безопасно и дефинирано.

person dyp    schedule 13.04.2014

Това е надпревара с данни и компилаторите в крайна сметка ще станат достатъчно умни, за да компилират неправилно кода, ако вече не са. Вижте Как да компилираме неправилно програми с „доброкачествени“ състезания за данни раздел 2.4 защо записи на същата стойност нарушават кода.

person nwp    schedule 14.04.2014
comment
Това беше замислено като коментар към отговора на Language-lawyer от dyp, за да покаже, че всъщност има последствия, но ми липсва необходимата репутация. - person nwp; 14.04.2014

Подробен отговор на внедряването:

Докато езиковият стандарт класифицира това като недефинирано поведение, всъщност можете да се чувствате доста сигурни, стига наистина да пишете едни и същи данни.

Защо? Хардуерът последователно осъществява достъпа до една и съща клетка от паметта. Единственото нещо, което може да се обърка е, когато няколко клетки от паметта се записват едновременно, защото тогава нямате гаранция от хардуера, че достъпите до няколко клетки са последователни по един и същи начин. Например, ако един процес запише 0x0000000000000000, а друг запише 0xffffffffffffffff, вашият хардуер може да реши да последователно подреди достъпите до различните байтове по различен начин, което води до нещо като 0x00000000ffffffff.

Ако обаче данните, записани от двата процеса, са еднакви, тогава няма забележима разлика между двете възможни сериализации, резултатът е детерминистичен.

Съвременният хардуер не обработва достъп до паметта по байт по байт начин, вместо това процесорите комуникират с основната памет по отношение на кеш линии, а ядрата обикновено могат да комуникират с техните кешове по отношение на 8-байтови думи. Като такова, настройването на правилно подравнен указател е атомарна операция, на която може да се разчита за прилагане на алгоритми без блокиране. Това е използвано в ядрото на Linux, преди да станат достъпни по-мощни атомарни операции. C++ формализира това под формата на atomic<> типове, добавяйки поддръжка за хардуерни функции на по-високо ниво като запис след четене, атомни увеличения и други.

Но, разбира се, ако разчитате на вашите хардуерни детайли, наистина трябва да знаете какво правите, преди да го направите. В противен случай се придържайте към езикови функции като типовете atomic<>, за да осигурите правилни операции и да избегнете UB.


@Downvoters:

Въпросът не е маркиран [language-lawyer] и отговорът изрично посочва „подробен отговор за внедряване“. Целта беше да се обясни как ще изглежда UB в програмата в реалния живот. Този отговор е написан, за да допълни приетия отговор (който има моето одобрение) с различна гледна точка към въпроса.

person cmaster - reinstate monica    schedule 13.04.2014
comment
Също така не можете да разчитате на компилатора да не използва UB. Цикълът по същество е memzero. Кой знае какво прави компилаторът с това? - person usr; 13.04.2014
comment
@usr Е, обикновено може да се разчита на оптимизатора, че няма да разбере проблемите с паралелността. Това би направило оптимизацията твърде сложна и единственият ефект би бил разбиването на многонишков код. И няма UB поведение в цикъла, когато приемете, че има само една нишка. - person cmaster - reinstate monica; 13.04.2014
comment
Защо? Хардуерът последователно осъществява достъпа до една и съща клетка от паметта. Имате ли оракул, който познава всеки вид хардуер, върху който програмата OPs някога ще работи? - person Casey; 13.04.2014
comment
@Casey Не, не знам, но знам как се прехвърлят байтове в основната памет; и дори имам малко представа за това кое е осъществимо в хардуера и кое не. Следователно съм напълно сигурен, че програмата на OP никога няма да работи на хардуер, където две едновременни записи на едни и същи данни водят до недефинирано поведение. Но, разбира се, това е подробният отговор на изпълнението :-) - person cmaster - reinstate monica; 13.04.2014