#c #multithreading #c 11 #concurrency #thread-safety
#c #многопоточность #c 11 #параллелизм #потокобезопасность
Вопрос:
В докладе о параллелизме и модели памяти C 11 Херб Саттер приводит примеры незаконных оптимизаций.
http://channel9.msdn.com/Shows/Going Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-2-of-2
Из слайда на минуте 17:
void f(vector<widget>amp; v) {
if(v.length()>0) xMutex.lock();
for(int i = 0; i < v.length(); i)
x; // write is conditional
if(v.length()>0) xMutex.unlock();
}
«Очень вероятное (хотя и глубоко ошибочное) преобразование центрального цикла:»
r1 = x;
for(int i = 0; i < v.length(); i)
r1; // oops: write is not conditional
x = r1;
Он объясняет: «…эта запись не является условной, это будет происходить при каждом выполнении, даже если значение doOptionalWork равно false , что приведет к вводу записи, которая не защищена блокировкой мьютекса, которая вводит гонку … «
Почему он говорит, что изобретенная запись не защищена блокировкой мьютекса? Я понял, что полное преобразование описывается следующим образом.
// "optimized" version 1
void f(vector<widget>amp; v) {
if(v.length() > 0) xMutex.lock()
r1 = x;
for(int i = 0; i < v.length(); i)
r1;
x = r1;
if(v.length() > 0) xMutex.unlock();
}
Но это также может быть так.
// "optimized" version 2
void f(vector<widget>amp; v) {
if(v.length() > 0) xMutex.lock()
r1 = x;
for(int i = 0; i < v.length(); i)
r1;
if(v.length() > 0) xMutex.unlock();
x = r1;
}
Очевидно, что версия 2 не является потокобезопасной, но я не уверен в версии 1. Является ли версия 1 потокобезопасной? Что, если в игре нет других строк, которые записывают в x?
Только сейчас я начал вводить «Либо v.length() равно 0, либо нет …» и понял, что даже тавтологии подводят меня в многопоточном мире. Я не знаю, с чего я могу начать рассуждать об этом.
Ответ №1:
Мьютекс используется только в том случае, если внутри вектора что-то есть. Одновременный запуск этого метода над двумя пустыми векторами приводит к гонке данных, потому что мы вообще не блокируем, но записываем в x.
Ответ №2:
Представьте, что есть другой поток, выполняющий следующий код:
xMutex.lock()
x;
xMutex.unlock();
Если приведенный выше код выполняется одновременно с (преобразованной) функцией f
с пустым вектором, то приращение to x
может потеряться, хотя это невозможно на уровне исходного кода.
Ответ №3:
Вызов mutex. lock()
функции — это то, что блокирует мьютекс. Мьютекс — это взаимоисключающая блокировка, которая позволяет одному и только одному вызывающему вызывать lock()
и возвращать. Последующие вызывающие абоненты будут блокироваться, ожидая вызова исходного вызывающего абонента unlock()
.
Это «взаимоисключающий» в том смысле, что только одному потоку разрешено получать блокировку.
Это определение мьютекса.
в опубликованном вами коде второй блок не завершает цикл приращения lock()
unlock()
вызовами and .
Это означает, что два потока, вызывающие функции, которые изменяют переменную, измененную в цикле, будут наступать друг на друга. Можно записать переменную непосредственно перед (или после) другим циклом, считывающим переменную. Код ожидает, что переменная будет согласована с функцией, поэтому поведение функции будет неправильным.
Ответ №4:
Подумав об этом, я собрал демонстрационную версию, чтобы продемонстрировать гонку.
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
// we have very simple widgets
typedef int widget;
// x and its mutex are shared by the threads
static int x = 0;
std::mutex xMutex;
static std::chrono::milliseconds central_loop_dur(10);
static std::chrono::milliseconds before_central_loop(0);
void f(std::vector<widget>amp; v) {
// The first thread acquires the lock.
// The second thread has an empty vector so passes through before the lock is released.
if(v.size() > 0) xMutex.lock();
// The first thread will take about 50ms to write to x.
// The second thread reads x nearly concurrently with the first thread. Both see 0.
int r = x;
// The first thread passes through here.
// The second thread snoozes.
std::this_thread::sleep_for(before_central_loop);
before_central_loop = std::chrono::milliseconds(200);
for(auto i : v) {
std::this_thread::sleep_for(central_loop_dur);
r;
}
// At 50ms, the first thread writes to x.
// At 200ms, the second thread obliviously writes a previous value of x back to x.
x = r;
if(v.size() > 0) xMutex.unlock();
}
void thread_main(size_t vec_size) {
std::vector<widget> v(vec_size);
f(v);
}
int main() {
std::thread t1(thread_main, 5);
std::thread t2(thread_main, 0);
t1.join();
t2.join();
std::cout << "The value of x = " << x << std::endl;
}
Без инструкций ожидания, если бы все происходило по порядку, результат был бы x = 5. Вместо этого программа (с высокой вероятностью) выведет x = 0, даже если поток 1 увеличивается в 5 раз!