Здравствуйте, Ikemefula, Вы писали:
I>Кстати, вопрос такой — ты в курсе, что Windows до определенной версии поддерживала только кооперетивную многозадачность ?
I>Ты в курсе, что уже тогда в ней были и семафоры, и муткесы.
Семафоры и мьютексы более использовались для сигналинга м/у процессами, внутри одного процесса обычно пользовали CRITICAL_SECTION.
Ну и еще в Windows 16 работали перывания прямо в юзверском коде ))
Поэтому, если твоя прога сидела на прерываниях, то эти прерывания тоже как-то обыгрывались: либо ресурсы защищались теми же критическими секциями, либо для какого-то участка кода
запрещались прерывания.
I>Как так, ведь в кооперативной многозадачности "гонок нет"
В Windows 16 (если ты о ней) кооперативный шедулер срабатывал на КАЖДЫЙ вызов системного АПИ, наверно в этом дело?
Т.е., ты пытаешься сравнить ситуацию, когда ты вручную расставляешь точки переключений задач с ситуацией, когда у тебя эти точки переключения задач расставлены даже в тех местах, где ты бы НЕ ХОТЕЛ, чтобы переключение произошло.
I>Все очень просто — ты хочешь съехать с кооперативной многозадачности.
Ы?
I>У тебя вытесняющая многозадачность, а я говорю про кооперативную. Представь себе, гонки это свойство любой многозадачности, а не только вытесняющей.
Я думаю, что ты пытаешься впасть в ту не раз наблюдаемую у некоторых коллег ересь, что, используя любой тьюринг-полный язык, на ём можно проэмулировать любые эффекты, достижимые в современных тьюринг-полных выч. системах.
Я же пытаюсь рассуждать о том, что у нас есть в "базе", потому что именно ср-ва из "базы" являются ср-вами шаговой доступности в языке, а всё остальное — только через библиотеки и трюки.
В базе жабаскрипта есть вполне детерминированная однопоточная модель вычислений — ты можешь пользоваться знаниями об этом.
Например, в случае вытесняющей многозадачности, для сериализации доступа к разделяемому ресурсу ты должен писать как-то так:
try {
Monitor.enter(guard1);
resource.access1(); // (1)
resource.access2(); // (2)
} finally {
Monitor.Exit(guard1);
}
Что логически эквивалентно сишному:
try {
RaiiGuard g(criticalSection1);
resource.access1(); // (1)
resource.access2(); // (2)
}
Но в жабаскрипте для обеспечения детерминированности достаточно, чтобы м/у точками (1) и (2) не произошло переключения задач. Т.е. не вставляй м/у точками (1) и (2) никаких await/yield и всех делов.
Если же ты м/у этими точками вставляешь инструкции переключения потоков, то твоя недетерминированность создана искусственно, т.е. является эмуляцией недетерминированности с помощью детерминированного инструмента.
I>У тебя плохой пример. Раздели чтение переменной и запись на две операции. Внезапно, обнаруживаем инвариант — поток затирает ровно то значение, которое перед этим прочёл.
Да, но где здесь недетерминированность?
Вот у меня код на жабаскрипте:
var x = 1;
x = x * 10;
x = x - 5;
В условиях детерминированности ты можешь в своей программе полагаться на значение x. Это фундаментальное св-во, лежащее в основе вcего программирования, что ты можешь полагаться на логику своей собственной программы.
Теперь другой пример:
var x = 1;
async function f1() {
var x1 = x * 10;
await someAsyncOp(); // (1)
x = x1 - 5;
}
Пример тоже полностью детерминированный. Ты можешь полагаться на значение локальной переменной x1, но уже не можешь полагаться на значение x после ручного переключения потоков на await.
Собсно, знания о том, какие значения являются детерминпированными, а какие нет, тоже составляют фундамент многопоточного программирования. ))
В последнем примере x1 — это личный ресурс, а x — разделяемый.
Но это не "само так получилось", не? Это так код был специально написан, чтобы x стал разделяемым ресурсом, да еще чтобы обязательно что-то с ним произошло м/у операциями чтения или записи. В последнем примере достаточно поменять строку (1) со строчкой сверху или снизу, чтобы ты опять вернулся к детерминированным рассуждениям относительно x.
I>Если компилятор не гарантирует атомарность инкремента
Компилятор ничего не может гарантировать. Он просто использует низлежащую модель вычислений.
I>многозадачный код ломает этот инвариант.
I>Отсюда ясно, что не состояние неопределено, а порядок выполнения недетерминирован, ибо может быть реально так:
I>thread1 read i
I>thread2 read i
I>thread1 write i + 1
I>thread2 write i + 1
Слава те хосподя, об этом и пример ))
I>И именно эта последовательность ломает состояние. То есть, гонки налицо. Видишь их?
Для случая С++? Конечно, это же был мой пример.
I>Теперь твой пример на JS, вместо потока — задача. Что читаем i и записываем отдельно.
I>Запускаем один таск — никаких нарушений инварианта нет, всё шоколадно.
I>Запускаем второй таск, опаньки — инвариант ломается.
Ну он же не сам поломался, это ты специально, ручками, сделал так, чтобы переменные i и actual изменялись/проверялись в РАЗНЫХ циклах запуска жабаскрипта. А ты сделай так, чтобы эти переменные изменялись в рамках одного цикла запуска скрипта и всё будет ОК.
I>Здесь, внимание, порядок выполнения зависит исключительно от того, в какой момент стартовал новый таск.
Да похер, сорри.
Ты НЕ можешь полагаться на значение переменных, оставшихся с прошлого запуска, потому что тебе недоступна история этих прошлых хапусков, не предоставляет жабаскрипт такой инфы, ты можешь полагаться на значения переменных, прочитанных в рамках текущего запуска.
Ты вообще смотрел мой пример? Не обратил внимание на такой момент:
// подпишись сюда в браузере на событие с клавиатуры
function onKeyboard(nextChar)
{
keyboardQueue.push(nextChar);
if(keyboardAwaiter != null) {
var awaiter = keyboardAwaiter;
keyboardAwaiter = null;
awaiter(keyboardQueue.take());
}
}
Если бы мне было доступно управление порядком очереди событий к хосту жабаскрипта, мне не нужна была бы очередь событий с клавиатуры, мне достаточно было бы одной переменной, хранящей последнюю нажатую кнопку. Но т.к. я НЕ знаю, какой событие за каким будет выполнено в реальности, я делаю буфер-очередь.
Но зато к самой очереди я обращаюсь БЕЗ каких-либо блокировок, т.к. низлежащая модель вычислений такова, что весь мой код, считай, обложен парой DisableInterruptions/EnableInterruptions.
И ты, именно ты, как программист, должен ЗНАТЬ низлежащую вычислительную модель. Ты должен знать, что твой жабасрипт НЕ прервется ни в какой точке исходника до тех пор, пока ты сам же, явно его не прервёшь. И если ты не обладаешь этими знаниями, то ты еще не готов программировать на этом зыке. Вот и всё кино.
I>ОС следит только за протоколом файловой системы. Что ты запишешь в файл, ты не знаешь, если писателей больше одного и твоей синхронизации нет.
I>И если два потока пишут в один файл, не важно, логических или физических, результат может быть недерминирован.
Ну так открывай файл с исключительным доступом, елки. И не шарь владение хендлом на файл.
Я пока проблему никак не могу уловить.
Не мог бы ты озвучить ТЗ целиком?
I>Это, в частности, проявляется в том, что содержимое файла при одном запуске будет "111222" а во втором случае "121212" а в третьем "122211"
Ну это согласно ТЗ так или из-за ошибок в коде?