Рейтинговые книги
Читем онлайн ЯЗЫК ПРОГРАММИРОВАНИЯ С# 2005 И ПЛАТФОРМА .NET 2.0. 3-е издание - Эндрю Троелсен

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 122 123 124 125 126 127 128 129 130 ... 259

Фоновые потоки (иногда называемые демонами) рассматриваются средой CLR, как возобновляемые ветви выполнения, которыми можно пренебречь в любой момент времени (даже при выполнении ими своих задач). Поэтому, когда все приоритетные потоки завершаются, все фоновые потоки будут завершены автоматически в результате выгрузки домена приложения.

Важно понять, что понятия приоритетного и фонового потоков – это не синонимы понятий первичного и рабочего потока. По умолчанию каждый поток, создаваемый с помощью метода Thread.Start(), автоматически оказывается приоритетным потоком. А это значит, что домен приложения не будет выгружен до тех пор, пока в нем все потоки не завершат свою работу. В большинстве случаев это будет именно тем поведением, которое требуется.

Но предположим, что нам нужно вызвать Printer.PrintNumbers() во вторичном потоке, который должен действовать, как фоновый поток. Это означает, что для метода, на который указывает тип Thread (посредством делегата ThreadStart или ParameterizedThreadStart), должна допускаться возможность безболезненного его завершения, как только все приоритетные потоки закончат свою работу. Для настройки такого потока достаточно установить значение true (истина) для свойства IsBackground.

static void Main(string[] args) {

 Printer p = new Printer();

 Thread bgroundThread = new Thread(new ThreadStart(p.PrintNumbers));

  bgroundThread.IsBackground = true;

  bgroundThread.Start();

}

Обратите внимание на то, что метод Main() здесь не вызывает Console.ReadLine(), чтобы гарантировать присутствие консоли на экране до нажатия клавиши «Enter». Поэтому при выполнении этого приложения оно сразу же прекратит свою работу, так как объект Thread сконфигурирован для работы в фоновом потоке. С началом работы метода Main() создается приоритетный первичный поток, поэтому, как только выполнение программной логики Main() завершится, домен приложения будет выгружен, и это произойдет до того, как вторичный поток завершит свою работу. Однако, закомментировав строку, в которой устанавливается свойство IsBackground, вы обнаружите, что на консоль выводятся все числа, поскольку для того, чтобы домен приложения будет выгружен из содержащего его процесса, все приоритетные потоки должны завершить свою работу.

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

Исходный код. Проект BackgroundThread размещен в подкаталоге, соответствующем главе 14.

Проблема конкурентного доступа

До сих пор все многопоточные приложения, созданные вами при изучении материала этой главы, были устойчивыми в отношении потоков, поскольку в них соответствующие методы вызывались только одним объектом Thread. Конечно, некоторые из ваших приложений могут быть настолько же простыми, но большинство многопоточных приложений содержит очень много вторичных потоков. С учетом того, что все потоки в домене приложения могут претендовать на доступ к открытым данным приложения одновременно, представьте себе, что может случиться, если к одному и тому же элементу данных получит доступ множество потоков. Поскольку планировщик потоков может приостановить работу потока в любой момент времени, что будет, если поток А будет отстранен от выполнения своей работы на полпути до того, как он эту работу завершит? Поток В будет читать некорректные данные.

Чтобы проиллюстрировать проблему конкурентного доступа, давайте построим еще одно консольное приложение C#, которое мы назовем MultiThreadedPrinting, Это приложение будет использовать класс Printer, созданный нами ранее, но на этот раз метод PrintNumbers() "заставит" текущий поток делать паузы произвольной длительности в соответствии со случайно генерируемыми значениями.

public class Printer {

 public void PrintNumbers() {

  …

  for (int i = 0; i ‹ 10; i++) {

   Random r = new Random();

   Thread.Sleep(1000 * r.Next(5));

   Console.Write(i + ", ");

  }

  Console.WriteLine();

 }

}

Метод Main() отвечает за создание массива из десяти объектов Thread с уникальными именами), каждый из который вызывает один и тот же экземпляр Printer.

class Program {

 static void Main(string[] args) {

  Console.WriteLine("***** Синхронизация потоков *****n");

  Printer p = new Printer();

  // Создание 10 потоков, указывающих на один и тот же метод

  // одного и того же объекта.

 Thread[] threads = new Thread[10];

  for (int i = 0; i ‹ 10; i++) {

   threads[i] =new Thread(new ThreadStart(p.PrintNumbers));

   threads[i].Name = string.Format("Рабочий поток #{0}", i);

  }

  // Теперь старт каждого их них.

  foreach (Thread t in threads) t.Start();

  Console.ReadLine();

 }

}

Перед тем как выполнить тестовый запуск программы, давайте обсудим 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. При блокировке метода уровня экземпляра можно использовать просто ссылку на текущий тип.

// Использование текущего объекта в качестве маркера потока.

lock(this) {

 // Весь программный код в этом контексте оказывается

 // устойчивым в отношении потоков.

}

При внимательном изучении метода PrintNumbers() становится ясно, что совместно используемым ресурсом, за доступ к которому соперничают потоки, является окно консоли. Поместите в рамки соответствующего контекста блокировки все операторы взаимодействии с типом Console так, как показано ниже.

public void PrintNumbers() {

 lock (this) {

  // Вывод информации Thread.

  Console.WriteLine("-› {0} выполняет PrintNumbers()", Thread.CurrentThread.Name);

  // Вывод чисел.

  Console.Write("Ваши числа": ");

  for (int i = 0; i ‹ 10; i++) {

   Random r = new Random();

   Thread.Sleep(1000 * r.Next(5));

   Console.Write(i + ", ");

  }

  Console.WriteLine();

 }

}

Тем самым вы создадите метод, который позволит текущему потоку завершить выполнение своей задачи. Как только поток вступит в контекст блокировки, соответствующий маркер блокировки (в данном случае эта ссылка на текущий объект) станет недоступным другим потокам, пока блокировка не будет снята в результате выхода потока из контекста блокировки. Например, если маркер блокировки получает поток А, то другие потоки не смогут войти в контекст до тех пор, пока поток А не освободит маркер блокировки.

1 ... 122 123 124 125 126 127 128 129 130 ... 259
На этой странице вы можете бесплатно читать книгу ЯЗЫК ПРОГРАММИРОВАНИЯ С# 2005 И ПЛАТФОРМА .NET 2.0. 3-е издание - Эндрю Троелсен бесплатно.
Похожие на ЯЗЫК ПРОГРАММИРОВАНИЯ С# 2005 И ПЛАТФОРМА .NET 2.0. 3-е издание - Эндрю Троелсен книги

Оставить комментарий