Форум: "Прочее";
Текущий архив: 2015.09.10;
Скачать: [xml.tar.bz2];
ВнизМногопоточное программирование: низкий старт Найти похожие ветки
← →
L_G © (2014-09-29 18:33) [0](это набросок незаконченной статьи, когда-то написанной и отложенной "до лучших времен", но, поскольку времена лучше не становятся, выкладываю как есть. кретикуйте! :)
Для ускорения понимания вами целесообразности чтения этого текста, сразу резюмирую всё нижеописанное.
Проблемы многопоточного программирования - это проблемы разграничения доступа кода потоков исполнения к используемым ими данным. Для лучшего понимания этих проблем и методов их решения будут подробно рассмотрены наиболее простые и прозрачные средства - interlocked-функции и циклы захвата/ожидания ресурсов с их использованием, в том числе для пула ресурсов. Поняв их суть, можно будет более осознанно использовать примитивы синхронизации Win32 API и их обертки из Delphi VCL.
Отдельный раздел посвящен прояснению заблуждений о фундаментальных принципах работы потоков, часто встречающихся среди начинающих Delphi-программистов.
Предложено простое решение для синхронизации нескольких функциональных групп потоков.
При чтении не следует пропускать непонятные места - следует разобраться в смысле каждого абзаца до полного его понимания, иначе написанное далее может оказаться еще более непонятным.
Основы: что такое поток исполнения
Поток исполнения кода (thread) - это последовательность инструкций процессора, исполняющихся в определенном самими этими инструкциями порядке. (Другой вариант перевода термина thread - нить. Стоит упомянуть, что термин stream - последовательность байт - тоже иногда переводят как "поток". Но не давайте себя запутать!)
Непрерывность исполнения потока не гарантируется: он может быть в любой момент прерван планировщиком процессов (частью ядра ОС) с сохранением состояния процессора и передачей управления другому потоку. Позже исполнение потока будет возобновлено с восстановлением сохраненного ранее состояния процессора. Такие прерывания исполнения потоков часто происходят на машинах с одним процессорным ядром и несколько реже, если ядер несколько и несколько потоков исполняются одновременно. Того, что поток от начала и до конца будет исполнен на одном ядре, тоже не гарантируется. Иногда исполнение прерванного потока продолжается на другом ядре, но это происходит незаметно для программы (или практически незаметно, google "проблемы использования инструкции rtdsc").
Поток исполнения кода явно привязан к определенному участку кода: именно конкретная функция/метод запускается в контексте потока, когда он стартует. К данным поток не привязан настолько же явно. Можно сказать, что поток привязан к данным, размещенным на стеке, то есть к локальным переменным, так как стек у каждого потока свой собственный. (При создании каждого экземпляра потока выделяется отдельная область памяти для размещения его стека.) К любым переменным, расположенным в куче (heap) и к любым глобальным (статическим) переменным любой из потоков может обратиться в любой момент. Их адреса для всех потоков одного процесса одинаковы. Ссылки на такие переменные, размещенные на стеке, как бы привязывают конкретные данные к конкретному потоку, но нет никаких гарантий, что к этим данным не сможет обратиться другой поток.
Один и тот же участок кода может исполняться одновременно несколькими потоками. Обычно при этом каждый из таких потоков через размещенные в его стеке ссылки получает доступ к своей специально для него выделенной области памяти (например, к полям объекта потока). Если каждый поток обрабатывает данные только в своей области памяти и обращается к специально для него выделенным ресурсам ОС, то никаких средств для разграничения доступа потоков к памяти и ресурсам ОС не требуется.
Необходимость разграничения доступа
То что поток может в любой момент обратиться к любой переменной, размещенной в куче или глобальной (статической), не означает того, что это можно делать без всяких предосторожностей. Разграничение доступа к памяти в случаях, когда одновременный доступ может привести к нежелательным результатам - забота программиста. Такое разграничение обычно называют сериализацией, то есть выстраиванием желающих получить доступ к ресурсу в очередь. (Не путать с сериализацией данных - созданием в одном непрерывном участке памяти копии или функционального эквивалента данных, размещенных в несмежных участках памяти.)
Другое название для разграничения доступа потоков к памяти и ресурсам ОС - синхронизация. Часто архитектура приложения такова, что одним потокам поручают ввод информацию, другим - обработку, а третьим - вывод. Синхронизация - это обеспечение того, чтобы потоки второй и третьей очередей не "забегали вперед", то есть не обращались к данным, еще не подготовленным для них потоками предыдущей очереди. Каждый поток должен сначала дождаться готовности очередной порции данных для него. Эти моменты ожидания и обеспечивают синхронность работы всех потоков (одновременно: вводящие - вводят, обрабатывающие - обрабатывают, выводящие - выводят) без ошибок и конфликтов.
Применение термина "синхронизация" к потокам может сбить с толку, так как доступ к памяти и ресурсам ОС потоки должны получать вовсе не синхронно (одновременно), а наоборот - последовательно, по очереди. Еще раз, синхронизацию потоков следует понимать только как обеспечение корректности их одновременной работы.
Синхронизация потоков не имеет ничего общего с синхронизацией нескольких копий данных, находящихся в разных местах - распространением изменений от одной копии данных ко всем остальным (например, синхронизацией кэш-памяти различных ядер многоядерного процессора или синхронизацией данных в распределенных базах данных).
← →
L_G © (2014-09-29 18:34) [1]Суть основной проблемы (могущей возникнуть при одновременном доступе нескольких потоков к одной области памяти)
При одновременном доступе двух или нескольких потоков к одной области памяти могут возникнуть эффекты, из-за которых дальнейшее исполнение кода может пойти по пути, который нельзя определить заранее. При этом каждый поток корректно считывает и записывает конкретные ячейки памяти. (Механизм кэширования памяти, даже если у каждого ядра кэш-память своя, работает прозрачно для программиста, обеспечивая корректную синхронизацию кэшей различных ядер.) Нежелательные эффекты возникают из-за того, что операции, которые в понимании программиста должны быть атомарными, оказываются состоящими из нескольких стадий, между которыми могут вклиниться операции с этой же областью памяти на другом ядре. Например, инкремент (операция увеличения значения переменной на единицу) на уровне команд процессоров Intel кодируется как одна инструкция Inc, но при её исполнении происходит три отдельных операции:
1) считывание ячейки памяти в специальный безымянный регистр,
2) увеличение этого регистра на 1
и 3) запись регистра обратно в память.
Если это действие с одной переменной (ячейкой памяти) попытаются исполнить 2 потока (ядра) одновременно, может получиться так:
1) первое ядро считывает значение в регистр (допустим, 0);
2) второе ядро считывает значение в регистр (0);
3) первое ядро увеличивает значение регистра (0 -> 1);
4) второе ядро увеличивает значение регистра (0 -> 1);
5) первое ядро записывает значение обратно (1);
6) второе ядро записывает значение обратно (1).
То есть, 0 было увеличено на 1 два раза, но вместо 2 в результате получилось 1.
На одноядерных процессорах такой проблемы нет, но есть другая (так же присутствующая и на многоядерных): прерывания. В любой непредсказуемый момент времени поток исполнения кода может быть остановлен и процессор может быть переключён на другой поток. Через какое-то время состояние первого потока восстанавливается и управление возвращается к месту кода, на котором первый поток был прерван.
Например, может произойти такое:
1) первый поток считывает значение в регистр (допустим, 0);
2) первый поток увеличивает значение регистра (0 -> 1);
3) процессор переключается на второй поток;
4) второй поток считывает значение в регистр (0);
5) второй поток увеличивает значение регистра (0 -> 1);
6) второй поток записывает значение обратно (1);
7) процессор переключается обратно на первый поток;
8) первый поток записывает значение обратно (1).
Результат тот же - вместо 2 получилось 1.
Вероятность прерывания в конкретном месте кода очень мала, но последствия те же самые, что и на многоядерных процессорах. Программист, желающий написать на 100% надежную программу, в любом случае должен исключить возможность возникновения таких ситуаций.
Та же самая проблема проявляется при операции обмена значениями между двумя переменными. И, разумеется, она же проявится с ещё большей вероятностью в более сложных, чем инкремент/декремент вычислениях, при которых переменная считывается из ячейки памяти, меняет каким-то образом своё значение и записывается обратно.
Казалось бы, должные всегда быть атомарными операции над примитивными переменнымии - простые чтение и запись - тоже могут преподнести сюрприз. В случае, если переменная расположена по адресу без выравнивания (нечетному для word, не кратному четырем для integer, cardinal или pointer и т.д.), её считывание и запись оказываются для процессора составными операциями и возможны проявления нежелательных эффектов.
Если же программа модифицирует не переменную, занимающую одну-единственную ячейку памяти, а более сложную структуру, то к этой проблеме добавляется еще и риск потерять целостность данных, то есть получить в результате двух одновременно проведенных со структурой операций некорректное заполнение этой структуры данными. Иногда эта некорректность такова, что последующая обработка таких данных вызывает исключение или иной неожиданный и катастрофический сбой программы.
Обнаружение и устранение подобных проблем часто оказываются очень трудными задачами по той причине, что ошибочное поведение программы проявляется далеко не всегда. Такую ошибку нельзя гарантированно воспроизвести, проделав определенную последовательность действий пользователя программы. Поэтому при программировании многопоточных приложений важно сразу применять методы и приемы программирования, исключающие саму возможность одновременного доступа разных потоков к одной области памяти.
Решения проблемы
Задача разграничения доступа кода потоков к переменным, объектам и структурам в оперативной памяти может решаться как посредством примитивов синхронизации, предоставляемыми операционной системой или библиотекой языка, так с помощью более простых средств, таких, как атомарные операции.
Описанные проблемы, могущие возникнуть при доступе к одной ячейке памяти (то есть к переменным примитивных типов - integer, byte, boolean, ...), относительно просто и элегантно решаются с помощью вызова функций атомарных операций Win32 API - InterlockedIncrement() вместо Inc(), InterlockedDecrement() вместо Dec(), InterlockedExchange() вместо "tmp:=a; a:=b; b:=tmp;", InterlockedCompareExchange() и так далее. Использование этих функций гарантирует, что одновременно исполняемые операции с доступом к одной переменной из разных потоков будут выстроены в очередь и не произведут никаких нежелательных эффектов.
Изменение более сложных структур данных (записей, объектов) происходит поэтапно, и в промежутке между началом и концом изменения одни части данных временно могут не соответствовать другим. Иногда нужно гарантировать, что данные в таком рассогласованном состоянии никем не будут прочитаны. Обычно для таких случаев рекомендуют использовать критическую секцию.
Но далее будет рассмотрен способ разграничения доступа с использованием более простых средств - только InterlockedExchange() и Sleep().
← →
L_G © (2014-09-29 18:35) [2]Простейший цикл захвата/ожидания
Есть простой и универсальный принцип разграничения доступа потоков к данным. Вот он: перед тем, как начать использовать объект потоко-опасным образом (например, записывать данные в область памяти, принадлежащую объекту), поток должен занять этот объект, предварительно убедившись, что он пока свободен. Занятый объект остальные потоки ни читать, ни изменять не должны. Завершив использование, поток освобождает занятый им объект.
В качестве признака свободы/занятости объекта можно использовать обычную переменную типа integer. Код потока может считать такую переменную и при равенстве значения условному "свободно" попытаться занять объект, изменив значение на "занято". Но если два потока сделают это одновременно, то оба прочитают значение "свободно", оба запишут "занято" и оба останутся в уверенности, что объект успешно занят именно им. Поэтому после проверки значения на "свободно" следует вызвать функцию атомарной замены (InterlockedExchange) для смены значения на "занято". Если результатом вызова (предыдущим значением переменной) окажется "свободно", то это гарантирует, что именно текущий поток занял объект, но если вернется "занято", то это означает, что между чтением, вернувшим "свободно" и обменом его на "занято" другой поток ухитрился обменять "свободно" на своё "занято" первым, то есть попытка текущего потока занять объект не удалась. В случае неудачи поток обычно ненадолго усыпляется в ожидании освобождения объекта. Всё это обернуто в цикл, задающий число попыток или таймаут занятия (время, в течение которого нужно продолжать попытки).const
FREE = 0; BUSY = 1;
_ATTEMPTS = 100; // сделаем не более 100 попыток (* 15 миллисекунд = полторы секунды)
var
BusyFlag: integer; // обычно поле объекта; если локальная переменная - нужно очищать перед использованием!
attempts: integer;
attempts := _ATTEMPTS;
repeat
if (BusyFlag = FREE) and (InterLockedExchange(BusyFlag, BUSY) = FREE) then // пытаемся занять
// ^^^^^^^^^^^^^^^^^^^^^ первая часть условия не обязательна, но с ней немного быстрее
begin // да - удалось занять!
// осуществляем потоко-опасные действия с объектом/структурой/ресурсом
// ...
BusyFlag := FREE; // освобождаем объект
attempts := -1; // выйдем из цикла repeat
end
else begin // нет - занять не удалось
Sleep(15); // подождем 15 миллисекунд
dec(attempts); // уменьшаем число оставшихся попыток
end;
until attempts <= 0;
if attempts = 0 then // так и не удалось занять объект за целых полторы секунды - нужно что-то сделать
То же плюс SpinLock
Бывает, что объекты занимаются совсем ненадолго, и в таких случаях "засыпание" на целых 15 миллисекунд при занятости объекта может заметно замедлить программу (а меньшее время "сна" Windows не всегда может обеспечить). В таких случаях помогает прокрутка потоком некоторго количества попыток занять объект в пустом цикле (без Sleep). Слегка усложним вышеприведенный код:const
FREE = 0; BUSY = 1;
_SLEEP_ATTEMPTS = 100; // сделаем не более 100 попыток (* 15 миллисекунд = полторы секунды)
_SPIN_ATTEMPTS = 200; // 200 быстрых циклов без Sleep
var
BusyFlag: integer; // обычно поле объекта; если локальная переменная - нужно очищать перед использованием!
sleep_attempts, k: integer;
sleep_attempts := _SLEEP_ATTEMPTS;
repeat
for k:=1 to _SPIN_ATTEMPTS do // быстрый цикл без Sleep
if (BusyFlag = FREE) and (InterLockedExchange(BusyFlag, BUSY) = FREE) then // пытаемся занять
// ^^^^^^^^^^^^^^^^^^^^^ первая часть условия не обязательна, но с ней немного быстрее
begin // да - удалось занять!
// осуществляем потоко-опасные действия с объектом/структурой/ресурсом
// ...
BusyFlag := FREE; // освобождаем объект
sleep_attempts := -1; // выйдем из цикла repeat
break; // но сначала из for
end
if sleep_attempts >= 0 then // занять не удалось
begin
Sleep(15); // подождем 15 миллисекунд
dec(sleep_attempts); // уменьшаем число оставшихся попыток
end;
until sleep_attempts <= 0;
if sleep_attempts = 0 then // так и не удалось занять объект за целых полторы секунды - нужно что-то сделать
То, что здесь добавлено в код - это реализация примитивного средства синхронизации, называемого спинлок (SpinLock).
Занятие объекта из пула
Пулом называют набор заранее инициализированных однотипных объектов или ресурсов ОС. Вместо того, чтобы каждый раз создавать и подготавливать к работе новый объект этого типа, программа запрашивает свободный объект у пула (или ищет его там).
Для поиска в пуле свободного объекта и его занятия вышеприведенный код придется ещё лишь немного усложнить:const
FREE = 0; BUSY = 1;
PoolSize = 10;
_SLEEP_ATTEMPTS = 100; // сделаем не более 100 попыток (* 15 миллисекунд = полторы секунды)
_SPIN_ATTEMPTS = 200; // 200 SpinLock-циклов
var
BusyFlags: array [0..PoolSize-1] of integer; // обычно поле объекта; если локальный - нужно очищать!
sleep_attempts, i, k: integer;
sleep_attempts := _SLEEP_ATTEMPTS;
repeat
for k:=1 to _SPIN_ATTEMPTS do // SpinLock-цикл
if sleep_attempts >= 0 then // для выхода после действий с объектом
for i:=0 to PoolSize-1 do // цикл перебора по пулу
begin
if (BusyFlags[i] = FREE) and (InterLockedExchange(BusyFlags[i], BUSY) = FREE) then // пытаемся занять
// ^^^^^^^^^^^^^^^^^^^^^^^^^ первая часть условия не обязательна, но с ней немного быстрее
begin // да - удалось занять!
// осуществляем потоко-опасные действия с объектом/структурой/ресурсом
// with MyObject[i] do ...
// ...
BusyFlags[i] := FREE; // освобождаем объект
sleep_attempts := -1; // выйдем из циклов for k и repeat
break; // но сначала из for i
end;
end;
if sleep_attempts >= 0 then // занять ни один из объектов пула не удалось
begin
Sleep(15); // подождем 15 миллисекунд
dec(sleep_attempts); // уменьшаем число оставшихся попыток
end;
until sleep_attempts <= 0;
if sleep_attempts = 0 then // так и не удалось занять объект за целых полторы секунды - нужно что-то сделать
← →
L_G © (2014-09-29 18:35) [3]Примитивы синхронизации Win32 API
Все более сложные и функциональные средства синхронизации потоков построены с использованием вышеописанных простых средств. В том числе и предоставляемые Win32 API критические секции, события, мьютексы и семафоры (critical section, event, mutex, semaphore). Все они подробно описаны в массе статей и книг, повторять здесь это смысла мало. Рекомендуется к прочтению классический труд Джеффри Рихтера "Создание эффективных Win32 приложений с учётом специфики 64-разрядной версии Windows". По конкретным вопросам Win32 API лучше сначала обратиться к первоисточнику - http://msdn.microsoft.com
Потоки в Delphi: рассеиваем возможные предрассудки
В Delphi для работы с потоками обычно используют описанные программистом классы, порожденные от библиотечного класса TThread. Здесь необходимо пояснить, что поток исполнения кода и объект потока - принципиально разные сущности.
Поток исполнения - это сущность уровня операционной системы, он ничего не знает об объектах, используемых в языках программирования. У потока ОС Windows есть число-идентификатор (handle) и есть привязанная к нему процедура, которая будет исполнена в контексте этого потока (третий параметр функции Win32 API CreateThread).
Объект потока в Delphi - более сложная сущность, инкапсулирующая в себе поток ОС. Как и все объекты Delphi/Object Pascal, объект потока состоит из кода и данных. (Можно сказать, что код принадлежит классу - он общий для всех экземпляров объекта, а данные принадлежат объекту. Класс содержит абстрактное описание данных, а конкретные экземпляры данных размещаются в куче при создании конкретного экземпляра объекта). То, что некий код расположен в каком-то методе класса потока вовсе не означает, что он исполняется в контексте потока, созданного объектом-экземпляром этого класса. В контексте созданного потока будет исполнен лишь метод Execute и всё то, что будет вызвано из него. Все остальные методы, включая конструктор класса, исполняются в контексте того потока, из которого они вызываются. (И, очевидно, любой метод в принципе может быть вызван из любого потока - конечно, если не рассматривать целесообразность этих вызовов.) Поток, в котором был вызван конструктор, можно назвать родительским, а созданный в нем новый поток - дочерним.
То, что какие-то данные принадлежат конкретному экземпляру класса потока (то есть ссылка на них размещена в стеке потока) не означает, что другие потоки не могут к ним обратиться. Чаще всего такие обращения оказываются полезными в конструкторе объекта потока (исполняющемся до его запуска) и в обработчике события OnTerminate (исполняющемся после его завершения). В обоих этих случаях эти обращения будут исполнены в контексте родительского потока и в обоих случаях они полностью безопасны (так как дочерний поток либо еще не стартовал, либо уже завершился).
Но если программист вызывает какие-то методы потока не из его конструктора, метода Execute или обработчика OnTerminate, а из каких-то других мест своего кода, то он должен сам заботиться о потокобезопасности, то есть предусмотреть разделение доступа к данным объекта потока в могущих быть вызванными из других потоков методах.
При обращении кода метода Execute к полям объекта используется ссылка на Self, размещенная в стеке, и, таким образом, каждый поток обращается к полям своего экземпляра объекта. Каких-то волшебных средств, гарантирующих, что к этим данным будет иметь доступ только "свой" поток, нет. Код других потоков вполне может обратиться к ним, если располагает ссылкой на объект потока.
← →
L_G © (2014-09-29 18:35) [4]Четыре варианта одновременной работы потоков с данными
Код потока может обращаться к глобальным переменным и к переменным, расположенным в куче. Все случаи таких обращений следует проанализировать на предмет того, не может ли другой поток (в том числе другой экземпляр того же класса потока) обратиться к этим переменным одновременно с рассматриваемым. Обратиться к данным одновременно два и более потока могут с различными целями:
a) прочитать их,
b) записать без чтения,
c) прочитать, затем записать (возможно, обработать и записать измененные данные),
d) один поток пишет данные, один или более - читают.
В случае (a) - одновременное чтение данных несколькими потоками - нет никаких проблем. Чтение данных любым количеством потоков не может вызвать никаких нежелательных эффектов.
Ситуации (b) - одновременная запись без чтения - возникать в принципе не должно. Область данных, в которую производится запись, должна быть как-то закреплена за конкретным потоком. Это могут быть либо данные, размещенные в стеке потока, либо данные, ссылка на которые расположена в стеке потока, либо какое-то поле в этих данных должно показывать принадлежность к определенному потоку или просто занятость/свободность данных и поток перед началом работы с этими данными должен это поле проверить (то есть считать, а это уже будет относиться к варианту c).
Если данные изменяются (читаются и в скором времени пишутся, вариант c), то нужно организовать очередность (сериализацию) доступа к ним. Для примитивных типов (integer etc.) достаточно использовать Interlocked-функции, для более сложных следует использовать критическую секцию либо цикл ожидания.
Если один поток только пишет данные, а один или более их читают (вариант d), то для примитивных типов тут нет проблем: возможность как-то испортить данные отсутствует. Доступ к более сложным типам (структурам, массивам, объектам) обычно осуществляется через критическую секцию либо цикл ожидания. Иначе читающий поток может прочитать еще не полностью записанные пишущим потоком данные.
Синхронизация групп потоков
Иногда в программе с наборами данных должны поочередно работать несколько разных по функциям групп потоков. Например, 1) потоки, считывающие либо принимающие данные, 2) потоки, их обрабатывающие и 3) потоки, сохраняющие либо отправляющие обратно обработанные данные. Для такой ситуации удобно завести для каждой порции данных отдельный признак, отражающий текущую стадию работы с этой порцией данных. Это может быть как специальное поле в самих данных, так и отдельный массив переменных-признаков. Такое поле может иметь значения, соответствующие стадиям обработки, например: "принимается/готовится к обработке", "готов к обработке", "обрабатывается", "готов к отправке", "отправляется" и "отправлен/место для новых данных свободно". Каждый освободившийся поток может перебирать эти признаки в цикле, ища нужное ему значение и дожидаясь его появления, впадая в Sleep(). Признак должен меняться с использованием InterlockedExchange(). Суть подхода аналогична уже рассмотренной реализации цикла ожидания для пулов - см. последний пример кода выше.
// TODO: другие примеры кода
Опасности злоупотребления TThread.Synchronize
// TODO
TThread.Queue: плюсы и минусы
// TODO
Отдельной сложной проблемой многопоточного программирования являются тупики (deadlock) и методы их избегания. Здесь хочется затронуть лишь её краешек.
В элементарных случаях достаточно избегать вложенности захватов объектов. Если поток занимает только по одному объекту за раз, то есть освобождает каждый объект перед захватом другого, то тупик попросту невозможен. В случаях, когда поток должен захватывать сразу несколько объектов одновременно, помогает установление строгого порядка их захвата. Например, захват производится в порядке увеличения либо уменьшения значений полей, уникально идентифицирующих объекты, либо значений указателей на захватываемые объекты.
← →
alexdn © (2014-09-29 19:33) [5]Выложи на каком нибудь блоге, может и мне когда нибудь пригодится.
← →
L_G © (2014-09-29 19:52) [6]пока не знаю, где лучше, так что пусть это пока будет этапом сбора ошибок, замечаний, пожеланий
← →
Rouse_ © (2014-09-29 20:17) [7]
> кретикуйте
Та, лехко :)
По великому и могучему, коробит: "могущей возникнуть", "методы их избегания".
> "Поток исполнения кода явно привязан к определенному участку
> кода: именно конкретная функция/метод запускается в контексте
> потока, когда он стартует. К данным поток не привязан настолько
> же явно. "
Это не всегда так, особенно с учетом работы под Wow64 держа в памяти еще и каллбэки. Желательно рассмотреть этот нюанс в статье.
Далее, этот момент крайне сумбурен - переписать:Один и тот же участок кода может исполняться одновременно несколькими потоками. Обычно при этом каждый из таких потоков через размещенные в его стеке ссылки получает доступ к своей специально для него выделенной области памяти (например, к полям объекта потока). Если каждый поток обрабатывает данные только в своей области памяти и обращается к специально для него выделенным ресурсам ОС, то никаких средств для разграничения доступа потоков к памяти и ресурсам ОС не требуется.
Про оверхип поподробней надо:Казалось бы, должные всегда быть атомарными операции над примитивными переменнымии - простые чтение и запись - тоже могут преподнести сюрприз. В случае, если переменная расположена по адресу без выравнивания (нечетному для word, не кратному четырем для integer, cardinal или pointer и т.д.), её считывание и запись оказываются для процессора составными операциями и возможны проявления нежелательных эффектов.
Это ошибка,которую так-же нужно описать.
> одновременно исполняемые операции с доступом к одной переменной
> из разных потоков будут выстроены в очередь и не произведут
> никаких нежелательных эффектов.
Подробнее:
Если логика изменения ячейки происходит на основе чения ее текущего значения, то мы не сможе выполнить атомарный IF, ибо соседняя нить уже изменила значение, а мы выпоняем оперцию инкремента по предыдущему состоянию.
По поводу приведенного кода пока ничего говорить не буду, в нем ошибки, впрочем посмотрю опубликованные исходники. Вероятно это результат купирования.
В итоге, рекомендации: TLS рассмотреть, причем, раз уж статья претендует на немного более чем поверхностный обзор, то рассмотреть нужно плотно.
Обьяснить необходимость синхронизации при чтении данных с примерами.
Рассмотреть вариант синхронизации доступа на чтения запись через собственный удобный шлюз без использования куцих VCL оберток.
А вообще, достаточно достойно.
Респект и уважухи :)
← →
Rouse_ © (2014-09-29 20:20) [8]ЗЫ: и да - это все лучше на блог, хотябы на временный, ибо немного не удобно вычитывать такой обьем неотформатированного текста.
← →
L_G © (2014-09-29 20:54) [9]Rouse_, спасибо за замечания
прямо в ближайшие дни заниматься доработкой статьи не планировал, да и вопросы затронуты непростые
буду рад соавторам или можете считать этот текст отданным в public domain
код был набран по быстрому, не компилировался и не тестировался
"TLS" - это не Transport Layer Security, а что-то другое? 8(
"на блог" - пока не знаю, куда, да и заводить ради 1 статейки? (других не планирую)
"неотформатированного" - на параграфы вроде разбит, что еще? слишком широкая колонка текста? иногда в таких случаях сужаю окно браузера
← →
MsGuns © (2014-09-29 21:06) [10]ИМХО, статья - хороший пример как простое можно сделать сложным
← →
Rouse_ © (2014-09-29 21:08) [11]
> буду рад соавторам или можете считать этот текст отданным
> в public domain
Соавтором не буду, банально нет времени, но проконсультировать по данному вопросу готов, благо это моя тематика.
> код был набран по быстрому, не компилировался и не тестировался
Это принципиальный вопрос, код должен быть рабочим и правильным Инче от неподготовленного читателя ускользьнет сам предмет вопроса, который он может изучить в отладчике.
> "TLS" - это не Transport Layer Security, а что-то другое? 8(
Это "Thread Local Storage" - весьма действенный инструмент, обеспечивающий как шлюз к данным конкретной нити, так и механизм вызова ее калбэков (грубо функций, которые стартуют перед ее выполнением).
> "на блог" - пока не знаю, куда, да и заводить ради 1 статейки?
Попробуй - истории давно известны авторы, опубликовавшие только одну свою работу, но "РАБОТУ"!!!
> на параграфы вроде разбит, что еще?
Просто не удобно читать, сложно обьяснить.
← →
Rouse_ © (2014-09-29 21:11) [12]
> MsGuns © (29.09.14 21:06) [10]
> ИМХО, статья - хороший пример как простое можно сделать
> сложным
А вот не согласен, просто в статье затронуты вопросы, в большинстве своем не интересные прикладнику, что не означает что ценность статьи минимальна.
← →
DVM © (2014-09-29 23:21) [13]
> MsGuns © (29.09.14 21:06) [10]
> ИМХО, статья - хороший пример как простое можно сделать
> сложным
Простое? Многопоточность это просто? Не соглашусь.
Имхо многопоточное программирование это одна из тех областей где очень, очень легко допустить ошибку и которую потом очень, очень трудно найти. Сколько копий сломано, например, на lockfree структурах данных (реализаций без видимых ошибок по пальцам пересчитать), которые тоже можно отнести к этой теме. По синхронизации можно книги писать.
← →
Игорь Шевченко © (2014-09-30 15:43) [14]Уже есть хорошая статья "Многопоточность в Delphi", например тут
http://forum.vingrad.ru/forum/topic-60076.html
Страницы: 1 вся ветка
Форум: "Прочее";
Текущий архив: 2015.09.10;
Скачать: [xml.tar.bz2];
Память: 0.59 MB
Время: 0.047 c