Поиск

Безопасность потоков и классы .NET

В группах новостей и почтовых рассылках я часто встречал вопрос: во всех ли классах System.* .NET обеспечена безопасность потоков? Отвечаю: нет, но так и должно быть. Если бы доступ к функциональности каждого класса был упорядочен, производительности системы был бы нанесен серьезный ущерб. Например, вообразите использование одного из классов-наборов, если бы он получал блокировку монитора при каждом вызове метода Add. А теперь допустим, что вы создаете экземпляр объекта-набора и добавляете к нему около тысячи объектов. Производительность при этом будет удручающей — вплоть до того, что систему будет невозможно использовать.

Правила использования потоков

Когда использовать потоки, а когда лучше всего бежать от этого, как от чумы? В этом разделе я опишу некоторые распространенные сценарии, в которых потоки могут быть чрезвычайно полезны для вашего приложения, а также ситуации, в которых несколько потоков лучше не использовать.

Когда использовать потоки

Когда вы стремитесь достичь повышенного параллелизма, упростить конструкцию и эффективнее использовать процессорное время.

Повышенный параллелизм

Очень часто приложениям требуется выполнять несколько задач одновременно. Например, однажды я писал систему хранения документации для банка, которая получала данные с оптических дисков, хранящихся в устройствах с автоматической сменой оптических дисков. Представьте себе огромные массивы данных, о которых здесь идет речь, и устройство с одним приводом и 50 сменными дисками, обслуживающее до нескольких гигабайт данных. Иногда на загрузку диска и поиск запрошенного документа уходило не менее 5—10 секунд. Стоит ли говорить, что мое приложение не соответствовало бы идеалам производительности, если бы во время выполнения всех этих действий оно блокировало бы пользовательский ввод! Поэтому для обработки пользовательского ввода я создал другой поток, который выполнял физическую работу по получению данных и обеспечивал продолжение работы пользователя. Он уведомлял главный поток о загрузке документа. Это очень удачный пример разделения труда по загрузке документа и обработке пользовательского ввода между двумя потоками.

Упрощенная структура

Популярный способ упрощения структуры сложных систем — использование очередей и асинхронной обработки. Чтобы задействовать такую конструкцию, вам придется подготовить очереди для обработки событий, происходящих в вашей системе. Вместо прямого вызова методов создаются объекты и помещаются в очереди, в которых производится их обработка. На другом конце этих очередей работают многопоточные серверные программы, настроенные на отслеживание сообщений, приходящих в эти очереди. Преимущество упрощенных конструкций этого типа — надежность, устойчивость и расширяемость основанных на них систем.

Лучшее использование процессорного времени

Часто ваше приложение реально не выполняет никакой работы, в то же время продолжая использовать свой квант. В моем примере с системой хранения документации один поток ожидал загрузки диска устройством. Очевидно, что это ожидание имело место из-за особенностей оборудования и не требовало процессорного времени. Другим аналогичным примером может служить ожидание вывода документов на печать или окончания операции ввода-вывода жесткого диска или CD-ROM. В каждом из этих случаев процессорное время не используется. Эти случаи являются кандидатами на перевод в потоки, работающие в фоновом режиме.

Когда лучше обойтись без потоков

Распространенной ошибкой новичков является попытка развертывания нескольких потоков в каждом приложении. Это может быть намного хуже, чем вовсе не иметь потоков! Как и в случае любого другого инструмента из вашего арсенала средств программирования, потоки нужны только там, где без них не обойтись. Не применяйте несколько потоков в приложении как минимум в следующих случаях: когда затраты при этом превышают выгоды, когда вы не определили производительность для обоих вариантов и когда вы не можете сформулировать причину, по которой следует использовать потоки.

Затраты больше выгод

Как вы узнали из раздела "Безопасность потоков и синхронизация", написание многопоточных приложений занимает несколько больше времени и требует больших усилий. Бывает, что небольшие преимущества, которые дает использование нескольких потоков, просто не стоят времени, потраченного на реализацию многопоточности.

Производительность для обоих случаев невозможно сравнить

Если вы — новичок в программировании многопоточности, для вас может быть неожиданностью обнаружить, что издержки, связанные с созданием и диспетчеризацией потоков, могут привести к тому, что одно-поточное приложение работает быстрее! Все зависит от того, что вы делаете и действительно ли вы можете распределить выполнение независимых задач между потоками. Например, если вам надо считать с диска три файла, создание для этого трех потоков не принесет вам пользы, поскольку все они будут обращаться к одному и тому же жесткому диску. Поэтому всегда старайтесь протестировать оба прототипа — однопо-точную и многопоточную версию своей системы, прежде чем тратить время и средства на разработку решения, которое может быть неоправданно с точки зрения производительности.

Нет веской причины

Не следует рассматривать применение многопоточности как подход по умолчанию. Из-за врожденной сложности написания многопоточных приложений всегда следует ориентироваться на однопоточный код, если только у вас нет веской причины поступить иначе.

Подведем итоги

Многопоточность позволяет приложениям разделять задачи и решать независимо каждую из них, максимально эффективно используя процессорное время. Однако многопоточность является верным выбором не всегда и порой может замедлить работу приложения. Создание и управление потоками на С# осуществляется посредством класса System. Threading. Thread. Безопасность потоков является важным понятием, связанным с созданием и использованием потоков. Безопасность потоков означает, что члены объекта всегда поддерживаются в действительном состоянии при одновременном использовании несколькими потоками. Важно, чтобы наряду с изучением синтаксиса многопоточности вы также поняли, когда ее применять, а именно: для повышения параллелизма, упрощения структуры и оптимального использования процессорного времени.