![]() |
Модераторы: Poseidon, Snowy, bems, MetalFan |
![]() ![]() ![]() |
|
PointerToNil |
|
||||||||
![]() Профиль Группа: Участник Сообщений: 108 Регистрация: 17.6.2013 Репутация: 3 Всего: 6 |
(это набросок незаконченной статьи, когда-то написанной и отложенной "до лучших времен", но, поскольку времена лучше не становятся, выкладываю как есть. кретикуйте!
![]() Для ускорения понимания вами целесообразности чтения этого текста, сразу резюмирую всё нижеописанное. Проблемы многопоточного программирования - это проблемы разграничения доступа кода потоков исполнения к используемым ими данным. Для лучшего понимания этих проблем и методов их решения будут подробно рассмотрены наиболее простые и прозрачные средства - interlocked-функции и циклы захвата/ожидания ресурсов с их использованием, в том числе для пула ресурсов. Поняв их суть, можно будет более осознанно использовать примитивы синхронизации Win32 API и их обертки из Delphi VCL. Отдельный раздел посвящен прояснению заблуждений о фундаментальных принципах работы потоков, часто встречающихся среди начинающих Delphi-программистов. Предложено простое решение для синхронизации нескольких функциональных групп потоков. При чтении не следует пропускать непонятные места - следует разобраться в смысле каждого абзаца до полного его понимания, иначе написанное далее может оказаться еще более непонятным. Основы: что такое поток исполнения Поток исполнения кода (thread) - это последовательность инструкций процессора, исполняющихся в определенном самими этими инструкциями порядке. (Другой вариант перевода термина thread - нить. Стоит упомянуть, что термин stream - последовательность байт - тоже иногда переводят как "поток". Но не давайте себя запутать!) Непрерывность исполнения потока не гарантируется: он может быть в любой момент прерван планировщиком процессов (частью ядра ОС) с сохранением состояния процессора и передачей управления другому потоку. Позже исполнение потока будет возобновлено с восстановлением сохраненного ранее состояния процессора. Такие прерывания исполнения потоков часто происходят на машинах с одним процессорным ядром и несколько реже, если ядер несколько и несколько потоков исполняются одновременно. Того, что поток от начала и до конца будет исполнен на одном ядре, тоже не гарантируется. Иногда исполнение прерванного потока продолжается на другом ядре, но это происходит незаметно для программы (или практически незаметно, google "проблемы использования инструкции rtdsc"). Поток исполнения кода явно привязан к определенному участку кода: именно конкретная функция/метод запускается в контексте потока, когда он стартует. К данным поток не привязан настолько же явно. Можно сказать, что поток привязан к данным, размещенным на стеке, то есть к локальным переменным, так как стек у каждого потока свой собственный. (При создании каждого экземпляра потока выделяется отдельная область памяти для размещения его стека.) К любым переменным, расположенным в куче (heap) и к любым глобальным (статическим) переменным любой из потоков может обратиться в любой момент. Их адреса для всех потоков одного процесса одинаковы. Ссылки на такие переменные, размещенные на стеке, как бы привязывают конкретные данные к конкретному потоку, но нет никаких гарантий, что к этим данным не сможет обратиться другой поток. Один и тот же участок кода может исполняться одновременно несколькими потоками. Обычно при этом каждый из таких потоков через размещенные в его стеке ссылки получает доступ к своей специально для него выделенной области памяти (например, к полям объекта потока). Если каждый поток обрабатывает данные только в своей области памяти и обращается к специально для него выделенным ресурсам ОС, то никаких средств для разграничения доступа потоков к памяти и ресурсам ОС не требуется. Необходимость разграничения доступа То что поток может в любой момент обратиться к любой переменной, размещенной в куче или глобальной (статической), не означает того, что это можно делать без всяких предосторожностей. Разграничение доступа к памяти в случаях, когда одновременный доступ может привести к нежелательным результатам - забота программиста. Такое разграничение обычно называют сериализацией, то есть выстраиванием желающих получить доступ к ресурсу в очередь. (Не путать с сериализацией данных - созданием в одном непрерывном участке памяти копии или функционального эквивалента данных, размещенных в несмежных участках памяти.) Другое название для разграничения доступа потоков к памяти и ресурсам ОС - синхронизация. Часто архитектура приложения такова, что одним потокам поручают ввод информацию, другим - обработку, а третьим - вывод. Синхронизация - это обеспечение того, чтобы потоки второй и третьей очередей не "забегали вперед", то есть не обращались к данным, еще не подготовленным для них потоками предыдущей очереди. Каждый поток должен сначала дождаться готовности очередной порции данных для него. Эти моменты ожидания и обеспечивают синхронность работы всех потоков (одновременно: вводящие - вводят, обрабатывающие - обрабатывают, выводящие - выводят) без ошибок и конфликтов. Применение термина "синхронизация" к потокам может сбить с толку, так как доступ к памяти и ресурсам ОС потоки должны получать вовсе не синхронно (одновременно), а наоборот - последовательно, по очереди. Еще раз, синхронизацию потоков следует понимать только как обеспечение корректности их одновременной работы. Синхронизация потоков не имеет ничего общего с синхронизацией нескольких копий данных, находящихся в разных местах - распространением изменений от одной копии данных ко всем остальным (например, синхронизацией кэш-памяти различных ядер многоядерного процессора или синхронизацией данных в распределенных базах данных). Суть основной проблемы (могущей возникнуть при одновременном доступе нескольких потоков к одной области памяти) При одновременном доступе двух или нескольких потоков к одной области памяти могут возникнуть эффекты, из-за которых дальнейшее исполнение кода может пойти по пути, который нельзя определить заранее. При этом каждый поток корректно считывает и записывает конкретные ячейки памяти. (Механизм кэширования памяти, даже если у каждого ядра кэш-память своя, работает прозрачно для программиста, обеспечивая корректную синхронизацию кэшей различных ядер.) Нежелательные эффекты возникают из-за того, что операции, которые в понимании программиста должны быть атомарными, оказываются состоящими из нескольких стадий, между которыми могут вклиниться операции с этой же областью памяти на другом ядре. Например, инкремент (операция увеличения значения переменной на единицу) на уровне команд процессоров 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(). Простейший цикл захвата/ожидания Есть простой и универсальный принцип разграничения доступа потоков к данным. Вот он: перед тем, как начать использовать объект потоко-опасным образом (например, записывать данные в область памяти, принадлежащую объекту), поток должен занять этот объект, предварительно убедившись, что он пока свободен. Занятый объект остальные потоки ни читать, ни изменять не должны. Завершив использование, поток освобождает занятый им объект. В качестве признака свободы/занятости объекта можно использовать обычную переменную типа integer. Код потока может считать такую переменную и при равенстве значения условному "свободно" попытаться занять объект, изменив значение на "занято". Но если два потока сделают это одновременно, то оба прочитают значение "свободно", оба запишут "занято" и оба останутся в уверенности, что объект успешно занят именно им. Поэтому после проверки значения на "свободно" следует вызвать функцию атомарной замены (InterlockedExchange) для смены значения на "занято". Если результатом вызова (предыдущим значением переменной) окажется "свободно", то это гарантирует, что именно текущий поток занял объект, но если вернется "занято", то это означает, что между чтением, вернувшим "свободно" и обменом его на "занято" другой поток ухитрился обменять "свободно" на своё "занято" первым, то есть попытка текущего потока занять объект не удалась. В случае неудачи поток обычно ненадолго усыпляется в ожидании освобождения объекта. Всё это обернуто в цикл, задающий число попыток или таймаут занятия (время, в течение которого нужно продолжать попытки).
То же плюс SpinLock Бывает, что объекты занимаются совсем ненадолго, и в таких случаях "засыпание" на целых 15 миллисекунд при занятости объекта может заметно замедлить программу (а меньшее время "сна" Windows не всегда может обеспечить). В таких случаях помогает прокрутка потоком некоторго количества попыток занять объект в пустом цикле (без Sleep). Слегка усложним вышеприведенный код:
То, что здесь добавлено в код - это реализация примитивного средства синхронизации, называемого спинлок (SpinLock). Занятие объекта из пула Пулом называют набор заранее инициализированных однотипных объектов или ресурсов ОС. Вместо того, чтобы каждый раз создавать и подготавливать к работе новый объект этого типа, программа запрашивает свободный объект у пула (или ищет его там). Для поиска в пуле свободного объекта и его занятия вышеприведенный код придется ещё лишь немного усложнить:
Примитивы синхронизации 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, размещенная в стеке, и, таким образом, каждый поток обращается к полям своего экземпляра объекта. Каких-то волшебных средств, гарантирующих, что к этим данным будет иметь доступ только "свой" поток, нет. Код других потоков вполне может обратиться к ним, если располагает ссылкой на объект потока. Четыре варианта одновременной работы потоков с данными Код потока может обращаться к глобальным переменным и к переменным, расположенным в куче. Все случаи таких обращений следует проанализировать на предмет того, не может ли другой поток (в том числе другой экземпляр того же класса потока) обратиться к этим переменным одновременно с рассматриваемым. Обратиться к данным одновременно два и более потока могут с различными целями: a) прочитать их, b) записать без чтения, c) прочитать, затем записать (возможно, обработать и записать измененные данные), d) один поток пишет данные, один или более - читают. В случае (a) - одновременное чтение данных несколькими потоками - нет никаких проблем. Чтение данных любым количеством потоков не может вызвать никаких нежелательных эффектов. Ситуации (b) - одновременная запись без чтения - возникать в принципе не должно. Область данных, в которую производится запись, должна быть как-то закреплена за конкретным потоком. Это могут быть либо данные, размещенные в стеке потока, либо данные, ссылка на которые расположена в стеке потока, либо какое-то поле в этих данных должно показывать принадлежность к определенному потоку или просто занятость/свободность данных и поток перед началом работы с этими данными должен это поле проверить (то есть считать, а это уже будет относиться к варианту c). Если данные изменяются (читаются и в скором времени пишутся, вариант c), то нужно организовать очередность (сериализацию) доступа к ним. Для примитивных типов (integer etc.) достаточно использовать Interlocked-функции, для более сложных следует использовать критическую секцию либо цикл ожидания. Если один поток только пишет данные, а один или более их читают (вариант d), то для примитивных типов тут нет проблем: возможность как-то испортить данные отсутствует. Доступ к более сложным типам (структурам, массивам, объектам) обычно осуществляется через критическую секцию либо цикл ожидания. Иначе читающий поток может прочитать еще не полностью записанные пишущим потоком данные. Синхронизация групп потоков Иногда в программе с наборами данных должны поочередно работать несколько разных по функциям групп потоков. Например, 1) потоки, считывающие либо принимающие данные, 2) потоки, их обрабатывающие и 3) потоки, сохраняющие либо отправляющие обратно обработанные данные. Для такой ситуации удобно завести для каждой порции данных отдельный признак, отражающий текущую стадию работы с этой порцией данных. Это может быть как специальное поле в самих данных, так и отдельный массив переменных-признаков. Такое поле может иметь значения, соответствующие стадиям обработки, например: "принимается/готовится к обработке", "готов к обработке", "обрабатывается", "готов к отправке", "отправляется" и "отправлен/место для новых данных свободно". Каждый освободившийся поток может перебирать эти признаки в цикле, ища нужное ему значение и дожидаясь его появления, впадая в Sleep(). Признак должен меняться с использованием InterlockedExchange(). Суть подхода аналогична уже рассмотренной реализации цикла ожидания для пулов - см. последний пример кода выше.
Опасности злоупотребления TThread.Synchronize // TODO TThread.Queue: плюсы и минусы // TODO Отдельной сложной проблемой многопоточного программирования являются тупики (deadlock) и методы их избегания. Здесь хочется затронуть лишь её краешек. В элементарных случаях достаточно избегать вложенности захватов объектов. Если поток занимает только по одному объекту за раз, то есть освобождает каждый объект перед захватом другого, то тупик попросту невозможен. В случаях, когда поток должен захватывать сразу несколько объектов одновременно, помогает установление строгого порядка их захвата. Например, захват производится в порядке увеличения либо уменьшения значений полей, уникально идентифицирующих объекты, либо значений указателей на захватываемые объекты. |
||||||||
|
|||||||||
Poseidon |
|
|||
![]() Delphi developer ![]() ![]() ![]() ![]() Профиль Группа: Комодератор Сообщений: 5273 Регистрация: 4.2.2005 Где: Гомель, Беларусь Репутация: 53 Всего: 133 |
Не понятно для кого предназначена статья. Если для новичков, которые только начинают изучать многопоточность, то слишком заумно все написано и мало конкретики. Грубо говоря, новички нифига не поймут. Если для опытных, то ничего интересного и нового нет. Общие впечатления - воды много, а нужного нет. Понять с 0 что-нибудь про потоки по данной статье будет трудно, и разобраться более детально в том, что уже знаешь, тоже статья не позволяет. Статья очень напоминает брошурку. Есть что почитать, что-то освежить, но использовать ее как пособие нельзя.
-------------------- Если хочешь, что бы что-то работало - используй написанное, если хочешь что-то понять - пиши сам... |
|||
|
||||
PointerToNil |
|
|||
![]() Профиль Группа: Участник Сообщений: 108 Регистрация: 17.6.2013 Репутация: 3 Всего: 6 |
По идее, статья должна давать ответы на определенные вопросы. Для тех, у кого именно такие вопросы есть, она и может оказаться полезной. Не для новичков без практического интереса, которые могут прочитать и забыть, а для новичков, уже столкнувшихся с определенными задачами на практике. (Возможно, как-то их уже решивших, но сомневающихся в правильности/оптимальности решения. Или не понимающих, как их решить.) Или могущих столкнуться очень скоро.
Некоторые части статьи появились под влиянием вопросов и дискуссий именно на этом форуме. |
|||
|
||||
drkot |
|
|||
![]() Ищущий ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1042 Регистрация: 5.5.2006 Репутация: 5 Всего: 8 |
Вы правы, "должна". Но как уже заметил, Poseidon, для новичка статья тяжеловата (по изложению), а для "посвященного" ничего нового не несет. Как по мне, нет смысла описывать варианты синхронизации. Их может быть очень много и конкретный подход зависит от задачи. А вот развить тему было бы совсем не плохо... и только из этого получилась бы статья. Так как возникновение узких мест при синхронизации бич не только новичков. -------------------- Ошибка не становится истиной по причине широкого распространения, как и Истина не становится Ошибкой из-за того, что никто её не видит. |
|||
|
||||
Poseidon |
|
|||
![]() Delphi developer ![]() ![]() ![]() ![]() Профиль Группа: Комодератор Сообщений: 5273 Регистрация: 4.2.2005 Где: Гомель, Беларусь Репутация: 53 Всего: 133 |
Кстати да, эта тема была бы интересна и полезна. Описание и разъяснение узких мест всегда полезнее общего описания вопроса. Я бы с удовольствием почитал бы. Добавлено через 1 минуту и 50 секунд Про Queue тоже. Это "чудо" появилось не так уж и давно, я изучал Delphi по "семерке", а там Queue не было. Так что это можно было бы узнать что-то новое. -------------------- Если хочешь, что бы что-то работало - используй написанное, если хочешь что-то понять - пиши сам... |
|||
|
||||
drkot |
|
||||||||||
![]() Ищущий ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1042 Регистрация: 5.5.2006 Репутация: 5 Всего: 8 |
Рано или поздно при построении много поточного приложения приходится думать о синхронизации. Для этого существует множество методов и средств. Можно использовать как низкоуровневые системный функции, так и объектные обертки над ними. Но как ни странно, речь пойдет не о них... а о том, как синхронизации сказывается на работе приложения.
Простейшее средство предоставляемое Delphi это TThread.Synchronize. Удобная на первый взгляд вещица. Поддерживает анонимные функции, что позволяет вписывать синхронный код непосредственно в контексте. При этом экономится время на создание лишних процедур и сохраняется хорошая читаемость кода. Собственно если брать классический пример иллюстрирующий работу с потоками в Delphi (три типа сортировок), то на первый взгляд никаких проблем с синхронизацией там нет. Но это только на первый взгляд, так как при имеющемся масштабе задачи они просто не заметны "на глаз". И так подробнее. Имеем три потока которые синхронно обновляют форму в главном потоке. Вызовы обновления происходят в основном цикле при каждом изменении данных. Ожидаемое количество вызовов синхронного обновления около 5000 раз (исходя из математической сложности каждой из сортировок и вероятности выполнения условия 0,25). Теперь эксперимент. Загружаем пример в IDE Delphi, и запускаем его в AQtime.Performance Profile. Даем приложению отработать, и закрываем его. В полученном результате нас интересует три строчки (выделено желтым): рис1
Сразу стоит обратить внимание на PaintLine, это собственно процедура которая и выполняет реальные действия по прорисовке, и как можно видеть она заняла всего 0,34 секунды при этом была выполнена 18569 раз. Что весьма не мало ![]()
Оберткой для рисования выступает TSortThread::DoVisualSwap и как видим она потребила не на много больше ресурсов 0,35 секунд (что вполне логично)
И наконец самое главное TSortThread::VisualSwap этот метод ожидает синхронизации. Смотрим время... 0,74 секунды... откуда такой прирост? Что делает эта процедура так долго? Ответ прост - "ничего". Она ждет синхронизации. Проводим простой расчет эффективности: время простоя 0,74-0,35=0,39 доля простоя 0,39/0,7488=52% Получается что данный алгоритм работает с КПД 48%. Думаю это не очень хорошо. Попробуем "улучшить" ситуацию... заменим Synchronize на Queue
и еще раз снимем временной отпечаток рис2 Даже при беглом взгляде видно, что ситуация значительно улучшилась. Время работы сократилось в 7,5 раз (с 0,75 до 0,1). Стоит также обратить PaintLine, по факту время сократилось, а количество вызовов увеличилось. Связано это с тем, что прорисовка происходит тоже синхронно (не DOS все таки) и часть вызовов была "проигнорирована" (суть несколько сложнее, но будем называть так). Собственно такое повединие хотя и не критично, но и не хорошо... Зачем нам 20000 лишних сообщений? Но это еще не все. VisualSwap все также занимает значительную часть времени работы потока. Попробуем побороть... Глаз человека нормально воспринимает анимацию 10-20 кадров, следовательно дергать перерисовку чаще нет никакой надобности.
рис3 Результат значительно лучше того который был в начале. Думаю на этом можно остановиться. Хочу подчеркнуть, что показан скорее принцип, нежели алгоритм действий. Главное понимание что много поточность не улучшает работу приложения сама по себе. Присоединённый файл ( Кол-во скачиваний: 2 ) ![]() -------------------- Ошибка не становится истиной по причине широкого распространения, как и Истина не становится Ошибкой из-за того, что никто её не видит. |
||||||||||
|
|||||||||||
drkot |
|
|||
![]() Ищущий ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1042 Регистрация: 5.5.2006 Репутация: 5 Всего: 8 |
Собственно криво косо, но как-то так
PS: Чукча не читатель, чукча писатель ;) Это сообщение отредактировал(а) drkot - 3.10.2014, 18:07 -------------------- Ошибка не становится истиной по причине широкого распространения, как и Истина не становится Ошибкой из-за того, что никто её не видит. |
|||
|
||||
kami |
|
|||
Эксперт ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1806 Регистрация: 25.8.2007 Где: Санкт-Петербург Репутация: 23 Всего: 72 |
Поправьте, если ошибаюсь, но. Судя по коду Classes.pas: Queue - это не замена Synchronize, т.к. выполняется асинхронно. Соответственно - применение должно быть несколько в другом ключе. Ибо если вызвавший Queue поток завершится раньше, чем TSynchronizeRecord попадет в обработку в основном потоке - может быть больно. Сравнение по производительности в данном случае мне кажется некорректным. Добавлено через 42 секунды Собственно - это так, или я ошибаюсь? |
|||
|
||||
drkot |
|
|||
![]() Ищущий ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1042 Регистрация: 5.5.2006 Репутация: 5 Всего: 8 |
если вызывать метод завершившегося потока, то да. если же метод принимающего сообщение, то проблем не будет.
Почему? -------------------- Ошибка не становится истиной по причине широкого распространения, как и Истина не становится Ошибкой из-за того, что никто её не видит. |
|||
|
||||
kami |
|
|||
Эксперт ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1806 Регистрация: 25.8.2007 Где: Санкт-Петербург Репутация: 23 Всего: 72 |
Раз опровержения моего предположения, что Queue - асинхронная штука не последовало, значит хоть в этом я не ошибся ![]() А по сути - решаемые с помощью Synchronize и Queue задачи слишком разные, но у читателя может возникнуть ощущение, что раз Queue быстрее, то надо использовать только его, что приведет к нежелательным последствиям в виде головной боли при отладке многопоточного приложения. P.S. Не упрекаю ни в коем случае, но без упоминания асинхронности Queue набросок не выглядит завершенным ![]() |
|||
|
||||
drkot |
|
|||
![]() Ищущий ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1042 Регистрация: 5.5.2006 Репутация: 5 Всего: 8 |
с этим наверно не соглашусь. Обе функции реализуют вызов в контексте основного потока. Первая при этом делает это синхронно, то есть ожидает в очереди (синхронизации) до вызова и ждет пока вызов завершится. Вторая же отправляет сообщение в очередь основного потока, для вызова обработчика. Так что назначение у них весьма сходное, а различны только ситуации в которых они могут применяться. Конечно, пример, на базе которого мне довелось развить эту тему, можно назвать неудачным, но другого общедоступного под рукой не оказалось. Демонстрировать механизмы синхронизации на примере перерисовки данных очень плохая практика, так как перерисовка в VCL имеет свой отдельный механизм который мало управляем и хорошо срыт (как минимум от начинающих). Так что мне виделось что пинать меня начнут именно за асинхронную прорисовку... Возможно не правильно расставил акценты... Но не важно кто быстрее а кто медленнее, важно как построена потоковая модель. Так в приведенном примере наблюдаем три потока, которые конкурируют за медленный ресурс (бутылочное горлышко). Такая модель не эффективна, так как увеличение количества потоков приведет к пропорциональному росту времени ожидания синхронизации, и как следствие быстродействие будет на уровне одно-поточного приложения. В частности для выше описанной задачи приемлема следующая схема (учитывая необходимости визуализации процесса): 1) основной поток (форма) 2) поток синхронной прорисовки 3) три потока сортировок Потоки сортировок на каждой итерации устанавливают свой TEvent останавливаются на ожидании Event потока прорисовки. Поток прорисовки ждет срабатывания Event всех трех потоков сортировки, рисует информацию на форме, дергает свой Event. Учитывая, что скорость работы потоков сортировки высокая, то они будут быстро синхронизироваться на потоке прорисовки. А так как конкуренции за прорисовку не будет (один поток рисует) то задержка на Synchronize будет минимальна. В итоге: имеем прорисовку каждой итерации сортировки, отсутствие конкуренции на медленных операциях и управляемую частоту обновления информации. Добавлено через 9 минут и 53 секунды Если использовать Queue для прорисовки, то нужно ограничивать количество поступающих сообщений. Так как нет смысла повторного вызова прорисовки до того времени пока не завершился предыдущий. Простейший вариант дергать флаг или тот же Event. В начале прорисовки устанавливаем Event, в конце сбрасываем. пока Event поднят, то не ведем отправку сообщений (или что лучше не вызываем функцию прорисовки, а просто игнорируем сообщение). Также можно вместо самой прорисовки дергать InvalidateRect, а прорисовка произойдет уже в основном потоке асинхронно. Главное что хотел сказать, нужно заниматься поиском узких мест. Так как в много поточных приложениях это частая проблема. -------------------- Ошибка не становится истиной по причине широкого распространения, как и Истина не становится Ошибкой из-за того, что никто её не видит. |
|||
|
||||
Poseidon |
|
||||||
![]() Delphi developer ![]() ![]() ![]() ![]() Профиль Группа: Комодератор Сообщений: 5273 Регистрация: 4.2.2005 Где: Гомель, Беларусь Репутация: 53 Всего: 133 |
Кстати, там же [в справке], есть очень серьезная заметка, которую так же, думаю, стоит упомянуть:
-------------------- Если хочешь, что бы что-то работало - используй написанное, если хочешь что-то понять - пиши сам... |
||||||
|
|||||||
kami |
|
|||
Эксперт ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1806 Регистрация: 25.8.2007 Где: Санкт-Петербург Репутация: 23 Всего: 72 |
Если цензурно выразить всё, что я думаю про справку в Delphi, то: нормальная справка была по Delphi 7 включительно. Всё, что после - в большинстве случаев просто ни о чем. Посему - гораздо проще и правильнее посмотреть в генофонд, чем пытаться вытащить зерно истины из того, что у меня в D2010 вылезает по нажатию F1. |
|||
|
||||
![]() ![]() ![]() |
Правила форума "Delphi: Общие вопросы" | |
|
Запрещается! 1. Публиковать ссылки на вскрытые компоненты 2. Обсуждать взлом компонентов и делиться вскрытыми компонентами
Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Snowy, MetalFan, bems, Poseidon, Rrader. |
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей) | |
0 Пользователей: | |
« Предыдущая тема | Delphi: Общие вопросы | Следующая тема » |
|
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности Powered by Invision Power Board(R) 1.3 © 2003 IPS, Inc. |