Средства создания многопоточных программ
Как в 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 (см. ниже). |