Шрифт:
Обычно конфигурация потока для выполнения в фоновом режиме может быть полезна тогда, когда соответствующий рабочий поток выполняет некритичные задания, которые оказываются не нужными после завершения выполнения главной задачи программы.
Исходный код. Проект BackgroundThread размещен в подкаталоге, соответствующем главе 14.
Проблема конкурентного доступа
До сих пор все многопоточные приложения, созданные вами при изучении материала этой главы, были устойчивыми в отношении потоков, поскольку в них соответствующие методы вызывались только одним объектом Thread. Конечно, некоторые из ваших приложений могут быть настолько же простыми, но большинство многопоточных приложений содержит очень много вторичных потоков. С учетом того, что все потоки в домене приложения могут претендовать на доступ к открытым данным приложения одновременно, представьте себе, что может случиться, если к одному и тому же элементу данных получит доступ множество потоков. Поскольку планировщик потоков может приостановить работу потока в любой момент времени, что будет, если поток А будет отстранен от выполнения своей работы на полпути до того, как он эту работу завершит? Поток В будет читать некорректные данные.
Чтобы проиллюстрировать проблему конкурентного доступа, давайте построим еще одно консольное приложение C#, которое мы назовем MultiThreadedPrinting, Это приложение будет использовать класс Printer, созданный нами ранее, но на этот раз метод PrintNumbers "заставит" текущий поток делать паузы произвольной длительности в соответствии со случайно генерируемыми значениями.
Метод Main отвечает за создание массива из десяти объектов Thread с уникальными именами), каждый из который вызывает один и тот же экземпляр Printer.
Перед тем как выполнить тестовый запуск программы, давайте обсудим cо-ответствующую проблему. Здесь первичный поток в рамках домена приложения порождает десять вторичных рабочих потоков. Каждому рабочему потоку дается указание вызвать метод PrintNumbers одного и того же экземпляра Printer. Поскольку здесь не предпринято никаких мер по блокированию общедоступных ресурсов данного объекта (консоли), имеется большая вероятность того, что текущий поток будет приостановлен до того, как метод PrintNumbers закончит вывод всех своих результатов. Вы не знаете точно, когда это случиться (и случится ли вообще), поэтому нужно быть готовым к непредвиденным результатам. Например, может получиться вывод, показанный на рис. 14.8.
Рис. 14.8. Конкуренция в действии, первая попытка
Выполните приложение еще несколько раз. На рис. 14.9 показана другая возможность вывода (ваши результаты, очевидно, тоже будут другими).
Рис. 14.9. Конкуренция в действии, вторая попытка
Ясно, что проблемы здесь действительно есть. Каждый поток дает указание объекту Printer печатать числовые данные, и планировщик потоков запускает выполнение этих потоков в фоновом режиме. В результате получается несогласованный вывод. В этом случае мы должны программно организовать синхронизованный доступ к совместно используемым ресурсам. Нетрудно догадаться, что в пространстве имен System.Threading есть целый ряд типов, имеющих отношение к синхронизации. А язык программирования C# предлагает специальное ключевое слово, как раз для решения задач синхронизации совместного доступа к данным в многопоточных приложениях.
Замечание. Если у вас не получается сгенерировать непредвиденный вывод, увеличьте число потоков с 10 до 100 (например) или добавьте в свою программу вызов Thread.Sleep. В конце концов вы все равно столкнетесь с проблемой конкурентного доступа
Синхронизация с помощью ключевого слова lock в C#
Первой из возможностей, которую вы можете применить в C# для синхронизации доступа к совместно используемым ресурсам, является использование ключевого слова lock. Это ключевое слово позволяет определить контекст операторов, которые должны синхронизироваться между потоками. В результате входящие потоки не смогут прервать текущий поток, пока он выполняет свою работу. Ключевое слово lock требует, чтобы вы указали маркер (объектную ссылку), который потребуется потоку для входа в пределы контекста lock. При блокировке метода уровня экземпляра можно использовать просто ссылку на текущий тип.
При внимательном изучении метода PrintNumbers становится ясно, что совместно используемым ресурсом, за доступ к которому соперничают потоки, является окно консоли. Поместите в рамки соответствующего контекста блокировки все операторы взаимодействии с типом Console так, как показано ниже.