Глубины Indy. 2. Техническая поддержка
Скачать 1.03 Mb.
|
16.3. Процессы против потоков Процессы отличаются от потоков, но их часто путают. Процесс это полностью законченный экземпляр приложения, который требует ресурсов для начала выполнения, включая передачу управления операционной системе и распределение дополнительной памяти. Преимущество процессов состоит в том, что они полностью изолированы один от другого, тогда как потоки изолированы только частично. Если процесс падает, то все остальные процесс остаются в неприкосновенности. 16.4. Потоки против процессов Потоки подобны процессам и являются параллельно выполняемым кодом. Но потоки являются частью родительского процесса. Каждый поток имеет свой собственные стек, но использует совместную куча вместе с другими потоками, того же процесса. Потоки быстрее создавать, чем процесс. Потоки также создают меньшую нагрузку на операционную систему и требуют меньше памяти для работы. Поскольку потоки не полностью изолированы друг от друга, то их взаимодействие друг с другом значительно проще. 16.5. Переменные потоков Переменные потока объявляются с использование ключевого слова ThreadVar. Переменные потока подобны глобальным переменным и объявляются подобным образом. Различие в том, что обычная глобальная переменная является глобальной для всех потоков, а переменные потока специфичны для каждого потока. Так в каждом потоке, появляется свое собственное глобальное пространство. Переменные потока могут быть полезны, когда трудно передавать ссылки на объект между библиотеками или изолированными кусками кода. Конечно переменные потока имеют и ограничения. Переменные потока не могут быть использованы или объявлены внутри пакетов. Везде где только возможно, должны быть использованы члены- переменные в классе потока. Они обеспечивают меньшую нагрузку и доступны для использования в пакетах. 73 16.6. Термины потоковый (threadable) и потоко-безопасный (threadsafe) Термин потоко-безопасный (threadsafe) часто используется или трактуется неправильно. Его часто применяют одновременно к потоковый (threadable) и потоко-безопасный (threadsafe), что приводит к ошибкам. В данном тексте, термины потоковый и потоко- безопасный точно определены и имеют разное значение. 16.6.1. Термин потоковый (threadable) Термин потоковый означает, что объект может использоваться внутри потока или использоваться потоком, если он должным образом защищен. Объект помечается как потоковый и обычно не имеет зависимости от потоков. Если объект потоковый, то это означает, что он может использоваться в один потоке в каждый момент времени. Это может быть обеспечено созданием его локально в потоке или через глобальную защиту ресурсов. Примеры потоковых переменных – это значения типа Integer, String и другие ординарные типы, TList, TStringList и большинство невизуальных классов. Объект может иметь доступ к глобальным переменным или элементам управления GUI. Неограниченное (а часто и не нужное) использование глобальных переменных в большинстве случаях, это то что мешает сделать компоненты потоковыми. Объект может быть библиотекой, компонентом, процедурой. 16.6.2. Термин потоко-безопасный (threadsafe) Термин потоко-безопасный означает, что объект осведомлен о потоках и имеет внутреннюю свою защиту ресурсов. Потоко-безопасные значения объекты могут использоваться в одном или нескольких потоках без применения защиты ресурсов. Примером потоко-безопасных классов является VCL класс TThreadList, а также потоко- безопасные классы Indy. Конечно операционная система также потоко-безопасна. 16.7. Синхронизация Синхронизация – это процесс передачи информации из вторичного потока основному. VCL поддерживает это с помощью метода Sychronize класса TThread. 16.8. Класс TThread Класс TThread это класс реализации потоков, который включен в VCL и предоставляет неплохую базу для построения потоков. Для реализация потока класс наследуется от TThread и переписывается метод Execute. 16.9. Компонент TThreadList Компонент TThreadList – это потоко-безопасная реализация класса TList. Класс TList может быть использован в любом количестве потоков без необходимости защиты от одновременного доступа. 74 Класс TThreadList работает подобно TList, но не совсем также. Некоторые методы, такие как as Add, Clear и Remove аналогичны. Для других операций, класс the TThreadList должен быть заблокирован с помощью метода LockList. метод LockList – это функции и она возвращает ссылку на внутренний экземпляр класса TList. Когда он заблокирован, все другие потоки будут заблокированы. По этому, очень важно разблокировать (Unlock) как можно быстрее. Пример операций с TThreadList: with MyThreadList.LockList do try t := Bytes div {/} KILOry for i := 0 to Count - 1 do begin // Operate on list items Items[i] := Uppercase(Items[i]); end ; finally MyThreadList.UnlockList; end ; Очень важно, что бы список всегда был разблокирован по окончанию кода и поэтому всегда блокирование и разблокирование должны делаться в защитном блоке try..finally. Если список остается заблокированным, то это приведет к зависанию других потоков при попытке их доступа к списку. 16.10. Indy Indy содержит много дополнительных классов, которые дополняют VCL возможностями поддержки потоков. Данные классы в действительности независимы от ядра Indy и полезны и для всех приложений. Они существуют в Indy, поскольку Indy разработана для работы с потоками. Indy не только использует эти классы для серверных реализаций, но и предоставляет их разработчику. Данная глава предоставляет краткий обзор этих классов. 16.11. Компонент TIdThread Компонент TIdThread – это наследник от TThread и добавляет дополнительные расширенные свойства, большинство из которых предназначены для построения серверов и также предоставляет поддержку пулов потоков и повторного использования. Если вы знакомы с потоками VCL, то очень важно принять во внимание, что TIdThread резко различается в нескольких ключевых областях. В TThread, метод Execute должен быть перегружен в наследниках, но в TIdThread должен быть перегружен метод Run. Ни в коем случаен не перегружайте метод Execute класса TIdThread, так как это будет препядствовать внутренним операциям TIdThread. Для всех наследников TIdThread, метод Run должен быть перегружен. Когда поток становится активным, выполняется метод Run. Метод Run последовательно вызывается в TIdThread, пока поток не будет остановлен. Это может быть не очевидно для большинства клиентских программ, тем не менее это может быть особо полезно для всех серверов и некоторых случаев на клиенте. В этом также отличие от метода Execute класса TThread. Метод Execute вызывается только один раз. Когда завершается Execute, то поток также завершен. 75 Есть и другие различия между TIdThread и TThread, но они не так значительны, как Run и Execute. 16.12. Класс TIdThreadComponent Класс TIdThreadComponent – это компонент, который позволяет вам визуально строить новые потоки, просто добавляя событие в дизайн тайм. Это основано на визуальной инкапсуляции TIdThread, что позволяет делать новые потоки очень просто. Для использования TIdThreadComponent добавьте его на вашу форму, определите событие OnRun и установите свойство Active. Пример использования TIdThreadComponent можно увидеть в демонстрационном примере TIdThreadComponent, доступным с сайта проекта. 16.13. Метод TIdSync Метод TThread имеет метод Synchronize, но он не имеет возможности передавать параметры в синхронизационные методы. Метод TIdSync имеет такую возможность. Метод TIdSync также позволяет возвращать значения из главного потока. 16.14. Класс TIdNotify Синхронизация прекрасна, когда количество потоков небольшое. Тем не менее в серверных приложениях со многими потоками, они становятся узким горлышком и могут серьезно снизить производительность. Для решения этой проблемы, должны использоваться оповещения. В Indy класс TIdNotify реализует оповещения. Оповещения позволяют общаться с главным потоком, но в отличие от синхронизации потока неблокируют его, пока оповещение обрабатывается. Оповещения выполняют функцию, подобную синхронизации, но без снижения производительности. Тем не менее, оповещения имеют и ряд ограничений. Одно из них, что значение не может быть возвращено из главного потока, поскольку оповещения не останавливают вызоваюший поток. 16.15. Класс TIdThreadSafe Класс TIdThreadSafe – это базовый класс, для реализации потоко-безопасных классов. Класс TIdThreadSafe никогда не используется сам по себе и разработан только как базовый класс. Indy содержит уже готовых наследников: TIdThreadSafeInteger, TIdThreadSafeCardinal, TIdThreadSafeString, TIdThreadSafeStringList, TIdThreadSafeList. Эти классы могут использоваться для потоко-безопасных версий типов integer, string и так далее. Они могут затем могут безопасно использоваться в любом количестве потоков, без необходимости заботиться об этом. В дополнение они поддерживают явное блокирование для расширенных целей. 16.16. Общие проблемы Наибольшей проблемой при работе с потоками является параллельное выполнение. Поскольку потоки выполняются параллельно, то имеется проблема доступа к общим данным. При использовании потоков в приложении, возникают следующие проблемы: 76 При выполнение клиентов в потоках, приносит следующие проблемы с конкурированием: 1. обновление пользовательского интерфейса из потока. 2. общение с главным потоком из вторичных потоков. 3. доступ к данным главного потока из вторичных потоков. 4. возвращение результата из потока. 5. определение момента завершения потока. 16.17. Узкие места Многие разработчики создают многопоточные приложения, которые работают нормально, пока количество потоков маленькое, но приложение деградирует, когда количество потоков увеличивается. Это эффект узкого горлышка. Эффект узкого горлышка проявляется когда какой то кусок кода блокирует другие потоки и другие потоки вынуждены ожидать его завершения. Не важно насколько быстр остальной код, проблема только в одном медленном куске. Код будет работать настолько быстро, насколько быстро работает его самая медленная часть. Многие разработчики вместо поиска узкого горлышка, тратят свое время на улучшение частей кода, который они считают недостаточно быстрым, тогда как узкое горлышко не позволяет достичь какого-либо эффекта. Обычно устранение одного узкого горлышка дает ускорение, больше чем сотни других оптимизаций. Поэтому, сфокусируйтесь на поиске узкого горлышка. И только после этого смотрите другой код для возможной оптимизации. Несколько узких мест будет рассмотрено ниже. 16.17.1. Реализация критических секций Критические секции эффективным и простым способом позволяют управлять доступом к ресурсам, чтобы только один поток имел доступ к ресурсу одновременно. Часто одна критическая секция используется для защиты множества ресурсов. Допустим, что ресурсы A, B и C имеют одну общую критическую секцию для их защиты, хотя каждый ресурс независим. Проблема возникает, когда используется B, то A и C также заблокированы. Критические секции простые и выделенная критическая секция должна использоваться для каждого ресурса. Критические секции иногда могут заблокировать слишком много кода. Количество кода, между методами Enter и Leave в критической секции должно быть минимально возможным и в большинстве случаев должно использоваться несколько критических секций, если это возможно. 16.17.2. Класс TMREWS Класс TMREWS может дать значительно увеличение производительности перед критическими секциями, если в основном доступ идет только по чтению и только иногда по записи. Тем не менее, класс TMREWS более сложный, чем критическая секция и требуется больше кода для установки блокировки. Для небольших кусков кода, даже если запись минимальна, обычно критическая секция работает лучше, чем TMREWS. 77 16.17.3. Синхронизация (Synchronizations) Синхронизация обычно используется для обновления пользовательского интерфейса. Проблема состоит в том, что синхронизация останавливает поток, пока она не будет закончена. Поскольку только главный поток обслуживает синхронизацию, только он может выполняться и остальные потоки выстраиваюися в очередь. Это делает все синхронизации самым главным узким горлышком. Используйте оповещение везде где только возможно. 16.17.4. Обновление пользовательского интерфейса Многопоточные приложения часто делают слишком много обновлений пользовательского интерфейса. Потеря производительности быстро наступает из-за задержек в обновлении пользовательского интерфейса. В большинстве случаев многопоточные приложения выполняются медленнее, чем однопоточная версия. Особое внимание должно быть сделано при реализации серверов. Сервер есть сервер и основная его задача обслуживать клиентов. Пользовательский интерфейс вторичен в данном случае. Поэтому пользовательский интерфейс лучше сделать в отдельном приложении, которое будет специальным клиентом для сервера. Другое решение – это использовать оповещения или пакетные обновления. В обоих этих случаях пользовательский интерфейс немного будет заторможен, но лучше иметь пользовательский интерфейс, который будет задержан на одну секунду, чем 200 клиентов, каждый их которых будет задержан на ту же секунду. Королями являются клиенты. 17. Серверы В Indy есть несколько серверных моделей, в зависимости от ваших потребностей и используемых протоколов. Следующие несколько глав введут вас в серверные компоненты Indy. 17.1. Типы серверов 17.1.1. Класс TIdTCPServer Наиболее известный сервер в Indy – это TIdTCPServer. Класс TIdTCPServer создает вторичный слушающий поток, который независим от главного потока программы. Слушающий поток следит за входящими запросами от клиентов. Для каждого клиента, которому он отвечает, он создает новый поток, для специфического сервиса, индивидуального соединения. Все соответствующие события затем возбуждаются к контексте этого потока. 78 17.1.1.1. Роль потоков Сервера Indy разработаны вокруг потоков и работают подобно тому, как в Unix работают сервера. Приложения Unix обычно взаимодействуют со стеком напрямую с минимумом абстракции, а то и без нее. Indy изолирует программиста от стека, используя высокий уровень абстракции и реализует многие детали внутри, автоматически и скрытно. Обычно, сервера Unix имеют один или несколько слушающих процессов, которые следят за запросами от клиентов. При запросе от клиента, который сервер акцептирует, сервер размножает процесс, для обработки каждого клиентского соединения. Обслуживание множества клиентов таким образом очень просто, так как каждый процесс работает только с одним клиентом. Процесс также может запускаться в своем контексте безопасности, который может быть установлен слушателем или процессом на основе аутентификации, прав или других основаниях. Сервера Indy работают подобным образом. Windows в отличии от Unix не размножает процессы, но зато Windows хорошо работает с потоками. Сервера Indy размещают поток для каждого клиентского соединения, вместо создания отдельного законченного процесса, как это делает Unix. Это дает все преимущества процессов, без наличия недостатков. 17.1.1.2. Сотни потоков Для сильно нагруженного сервера может потребоваться сотни или даже тысячи потоков. Есть общее убеждение, что сотни и тысячи потоков могут убить вашу систему. Это неверное убеждение. Количество потоков, которые запущены на вашей системе, может удивить вас. При минимальном количестве запущенных серверов и указанными запущенными приложениями: 79 Моя система имеет 513 созданных потоков: Вы видите, что даже при 513 потоках процессор нагружен только на 1%. Сильно нагруженный сервер IIS (Microsoft Internet Information Server) может создать сотни и тысячи потоков. Данный тест был выполнен на Pentium III 750 MHz с 256 MB оперативной памяти. На большинстве серверов, потоки находятся в режиме ожидания данных. При ожидании на блокирующем сокете, поток находится в неактивном состоянии. Поэтому на сервере из 500 потоков, только 25 могут оказаться активными одновременно. В сокетных приложениях, наибольшие ограничения накладывает самый медленный компонент. В сетевых приложениях, сетевая плата обычно самый медленный компонент и поэтому ничего не сможет превысить возможностей сетевой платы. Даже самая быстрая сетевая плата, во много раз медленнее процессора, и является узким местом, если она задействована по полной. 17.1.1.3. Реальные ограничения на потоки Реально средняя система начинает испытать проблемы, только когда процесс создаст свыше 1000 потоков, из-за нехватки памяти. Размер стека потока может быть уменьшен за счет увеличения количества потоков, тем не менее, надо рассмотреть и другие альтернативные причины. Большинству серверов требуется только несколько сотен потоков. Тем не менее, сильно нагруженные сервера или сервера, у которых трафик низкий, но множество подключенных клиентов, таких как чат (Chat) сервер требуют другой реализации. Такие сервера, в Indy могут создавать тысячи потоков. Также следует понять, что количество клиентов совсем не означает тоже количество конкурирующих потоков. Когда каждый клиент выделяет отдельный поток, то поток выделяется клиенту только при соединении. Многие сервера обслуживают кратко живущие соединения, такие как HTTP сервера. HTTP соединения для страниц обычно живут только одну секунду или менее, особенно если используется прокси или кэширование. Предположим 1 секунду на соединение и только 500 потоков, что позволяет до 30 000 клиентов в час. 80 Indy 10 имеет и другие модели, в дополнение к потокам серверов. Возможности Indy 10 ограничены только размером доступной памяти для размещения сокетов. 17.1.1.4. Модели серверов Есть два пути построения TCP серверов: с командными обработчиками и с событиями OnExecute. Командные обработчики делают построение серверов много проще, но не во всех ситуациях. Командные обработчики удобны для протоколов, которые обмениваются командами в текстовом формате, но не очень удобны для протоколов, которые имеют команды двоичной структуры или совсем не имеют командной структуры. Большинство протоколов текстовые и могут использоваться командные обработчики. Командные обработчики полностью опциональны. Если они не используются сервера Indy продолжают использовать старые методы. Командные обработчики рассмотрены в деталях в главе «Командные обработчики». Некоторые протоколы являются двоичными или не имеют командной структуры и не пригодны для использования командных обработчиков. Для таких серверов должно использоваться событие OnExecute. Событие OnExecute постоянно вызывается пока существует соединение и передает соединение, как аргумент. Реализация очень простого сервера с использованием события OnExecute выглядит так: |