Компонентный подход в программировании


Средства создания многопоточных программ


Как в Java, так и в C# возможно создание многопоточных приложений. Вообще говоря, каждая программа на этих языках представляет собой набор потоков (threads), выполняющихся параллельно. Каждый поток является исполняемым элементом, имеющим свой собственный поток управления и стек вызовов операций. Все потоки в рамках одного процесса (одной виртуальной машины Java или одного процесса среды .NET) имеют общий набор ресурсов, общую память, общий набор объектов, с которыми могут работать.

Каждый поток представляется в языке объектом некоторого класса (java.lang.Thread в Java и System.Threading.Thread в C#). Для запуска некоторого кода в виде отдельного потока необходимо определить особую операцию в таком объекте и выполнить другую его операцию.

В Java это можно сделать двумя способами.

Первый — определить класс-наследник java.lang.Thread и перегрузить в этом классе метод public void run(). Этот метод, собственно, и будет выполняться в виде отдельного потока.

Другой способ — определить класс, реализующий интерфейс java.lang.Runnable и его метод void run(). После чего построить объект класса Thread на основе объекта только что определенного класса.

В обоих случаях для запуска выполнения потока нужно вызвать в объекте класса Thread (в первом случае — его наследника) метод void start().

В C# также можно использовать два способа. С помощью первого можно создать обычный поток, с помощью второго — поток, которому при запуске нужно передать какие-то данные.

Для этого нужно определить метод, который будет выполняться в рамках потока. Этот метод должен иметь тип результата void. Список его параметров в первом случае должен быть пустым, во втором — состоять из одного параметра типа object.

В первом варианте на основе этого метода создается делегат типа System.Threading.ThreadStart, во втором — типа System.Threading. ParameterizedThreadStart.

Этот делегат передается в качестве аргумента конструктору объекта класса System.Thread.

Поток запускается выполнением метода Start() у объекта класса Thread в первом случае, или метода Start(object) во втором.

class T extends Thread { int id = 0; public T(int id) { this.id = id; }

public void run() { System.out.println ("Thread " + id + " is working"); } }

public class A { public static void main(String[] args) { Thread th1 = new T(1), th2 = new T(2), th3 = new Thread( new Runnable() { public void run() { System.out.println ("Runnable is working"); } });



th1.start(); th2.start(); th3.start(); } }

using System; using System.Threading;

class T { int id; public T(int id) { this.id = id; }

public void m() { Console.WriteLine ("Thread " + id + " is working"); } }

public class A { static void m() { Console.WriteLine ("Nonparameterized thread" + " is working"); }

static void m(object o) { Console.WriteLine ("Thread with object " + o + " is working"); }

public static void Main() { Thread th1 = new Thread( new ThreadStart(m)), th2 = new Thread( new ThreadStart(new T(1).m)), th3 = new Thread( new ParameterizedThreadStart(m));

th1.Start(); th2.Start(); th3.Start(2); } }

<
При разработке приложений, основанных на параллельном выполнении нескольких потоков, большое значение имеют вопросы синхронизации работы этих потоков. Синхронизация позволяет согласовывать их действия и аккуратно передавать данные, полученные в одном потоке, в другой. И недостаточная синхронизация, и избыточная приводят к серьезным проблемам. При недостаточной синхронизации один поток может начать работать с данными, которые еще находятся в обработке у другого, что приведет к некорректным итоговым результатам. При избыточной синхронизации как минимум производительность приложения может оказаться слишком низкой, а в большинстве случаев приложение просто не будет работать из-за возникновения тупиковых ситуаций (deadlocks), в которых два или более потоков не могут продолжать работу, поскольку ожидают друг от друга освобождения необходимых им ресурсов.

В обоих языках имеются конструкции, которые реализуют синхронизационный примитив, называемый монитором (monitor). Монитор представляет собой объект, позволяющий потокам "захватывать" и "отпускать" себя. Только один поток может "держать" монитор в некоторый момент времени — все остальные, попытавшиеся захватить монитор после его захвата этим потоком, будут приостановлены до тех пор, пока этот поток не отпустит монитор.

Для синхронизации используется конструкция, гарантирующая, что некоторый участок кода в каждый момент времени выполняется не более чем одним потоком. В начале этого участка нужно захватить некоторый монитор, в качестве которого может выступать любой объект ссылочного типа, в конце — отпустить его. Такой участок помещается в блок (или представляется в виде одной инструкции), которому предшествует указание объекта-монитора с ключевым словом synchronized в Java или lock в C#.



public class PingPong extends Thread { boolean odd; PingPong (boolean odd) { this.odd = odd; }

static int counter = 1; static Object monitor = new Object();

public void run() { while(counter < 100) { synchronized(monitor) { if(counter%2 == 1 && odd) { System.out.print("Ping "); counter++; } if(counter%2 == 0 && !odd) { System.out.print("Pong "); counter++; } } } }

public static void main (String[] args) { Thread th1 = new PingPong (false), th2 = new PingPong (true);

th1.start(); th2.start(); } }
using System; using System.Threading;

public class PingPong { bool odd;

PingPong (bool odd) { this.odd = odd; }

static int counter = 1; static object monitor = new object();

public void Run() { while(counter < 100) { lock(monitor) { if(counter%2 == 1 && odd) { Console.Write("Ping "); counter++; } if(counter%2 == 0 && !odd) { Console.Write("Pong "); counter++; } } } }

public static void Main() { Thread th1 = new Thread( new ThreadStart( new PingPong(false).Run)), th2 = new Thread( new ThreadStart( new PingPong(true).Run));

th1.Start(); th2.Start(); } }


Кроме того, в Java любой метод класса может быть помечен как synchronized. Это значит, что не более чем один поток может выполнять этот метод в каждый момент времени в рамках объекта, если метод нестатический и в рамках всего класса, если он статический.

Такой модификатор эквивалентен помещению всего тела метода в блок, синхронизированный по объекту this, если метод нестатический, а если метод статический — по выражению this.getClass(), возвращающему объект, который представляет класс данного объекта.

В Java также имеется стандартный механизм использования любого объекта ссылочного типа в качестве монитора для создания более сложных механизмов синхронизации.

Для этого в классе java.lang.Object имеются методы wait(), приостанавливающие текущий поток до тех пор, пока другой поток не вызовет метод notify() или notifyAll() в том же объекте, или пока не пройдет указанное время. Все эти методы должны вызываться в блоке, синхронизированном по данному объекту.

Однако это механизм достаточно сложен в использовании и не очень эффективен. Для реализации более сложной синхронизации лучше пользоваться библиотечными классами из пакетов java.util.concurrent и java.util.concurrent.locks, появившихся в JDK версии 5 (см. ниже).


Содержание раздела