Структурированный параллелизм (JEP-428) — это подход к программированию, который организует параллельные задачи иерархически, требуя, чтобы родительские задачи ждали завершения своих дочерних задач перед завершением. Этот метод сводит к минимуму утечку ресурсов, позволяет избежать бесхозных задач и упрощает управление параллельным кодом. Структурированный параллелизм использует облегченную реализацию VirtualThread, предоставляемую проектом Loom.

Структурированный объем задач

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

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

//Weather service interface
interface WeatherService {
  String getWeather();
}

//Weather provider 1
 class WeatherService1 implements WeatherService {

    @Override
    public String getWeather() {
      waitRandom(); //Wait for random amount of time
      return "Sunny";
    }
  }

//Weather provider 2
  class WeatherService2 implements WeatherService {

    @Override
    public String getWeather() {
      waitRandom();//Wait for random amount of time
      return "Rainy";
    }
  }

//Weather provider 3
  class WeatherService3 implements WeatherService {

    @Override
    public String getWeather() {
      waitRandom();//Wait for random amount of time
      return "Cloudy";
    }
  }

В предыдущем коде мы установили интерфейс WeatherService и предоставили три отдельные простые реализации для эмуляции трех отдельных поставщиков погоды. Далее мы рассмотрим, как использовать StructuredTaskScope для разработки службы получения данных о погоде, придерживаясь принципов структурированного параллелизма.

public static void main(String[] args) {
    try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
      //A new task scope is opened
      Future<String> res1 = scope.fork(() -> new WeatherService1().getWeather());
      Future<String> res2 = scope.fork(() -> new WeatherService2().getWeather());
      Future<String> res3 = scope.fork(() -> new WeatherService3().getWeather());
      scope.join();
      System.out.println(scope.result());
    } catch (ExecutionException e) {
      throw new RuntimeException(e);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

Класс StructuredTaskScope предлагает два статических внутренних класса: ShutdownOnSuccess и ShutdownOnFailure, оба из которых расширяют класс StructuredTaskScope. Чтобы реализовать структурированный параллелизм, мы можем создать экземпляр любого из этих классов в зависимости от наших потребностей или создать собственный класс, расширяющий StructuredTaskScope. После инициализации задачи можно отправлять экземпляру StructuredTaskScope с помощью метода fork. Внутри StructuredTaskScope используются VirtualThreads, предоставляемые Project Loom. При разветвлении каждой новой задачи создается новый VirtualThread.

ShutdownOnSuccess фиксирует первый результат и закрывает область задачи, чтобы прервать незавершенные потоки и разбудить владельца. Этот класс предназначен для случаев, когда подойдет результат любой подзадачи («вызвать любую») и когда нет необходимости ждать результатов других незавершенных задач. Он определяет методы для получения первого результата или создания исключения в случае сбоя всех подзадач.

ShutdownOnFailure фиксирует первое исключение и закрывает область задачи. Этот класс предназначен для случаев, когда требуются результаты всех подзадач («вызвать все»); если какая-либо подзадача не удалась, то результаты других незавершенных подзадач больше не нужны. Если определяет методы для создания исключения в случае сбоя какой-либо из подзадач.

Так как класс StructuredTaskScope реализует AutoCloseable, рекомендуется открывать область в блоке try-with-resources.

Области задач можно открывать внутри области других задач, и это приводит к древовидной структуре, в которой область охвата задач становится родительской для вновь созданной области задач. Это приводит к древовидной структуре, которая полезна при передаче значений с областью действия (JEP-429).

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

- Отношение родитель-потомок устанавливается, когда поток, запущенный в области задач, открывает свою собственную область задач. Поток, запущенный в области задач «A», который открывает область задач «B», устанавливает отношение родитель-потомок, где область задачи «A» является родительской областью задачи «B».
- Родитель-потомок отношение устанавливается с вложенностью, когда поток открывает область задачи «B», затем открывает область задачи «C» (до закрытия «B»), затем объемлющая область задачи «B» является родителем вложенной задачи область "C".
Потомками области задачи являются дочерние области задач, родителем которых она является, плюс потомки дочерних областей задач, рекурсивно

Чем он отличается от ExecutorService, ThreadPools, CompletableFuture и т. д.

Структурированный параллелизм и существующие механизмы параллелизма в Java предназначены для управления параллельными задачами, но они делают это по-разному и имеют свои преимущества и недостатки.

Структурированный параллелизм

  1. Иерархическая организация: структурированный параллелизм обеспечивает соблюдение родительско-дочерних отношений между задачами, требуя, чтобы родительские задачи ждали завершения дочерних задач перед завершением.
  2. Уменьшение утечек ресурсов. За счет того, что родительские задачи ожидают выполнения дочерних задач, структурированный параллелизм сводит к минимуму утечки ресурсов, поскольку все ресурсы очищаются после завершения всей иерархии задач.
  3. Предотвращение бесхозных задач. Структурированный параллелизм позволяет избежать бесхозных задач, поскольку дочерние задачи должны выполняться раньше родительских, что предотвращает оставление задач без присмотра.
  4. Простая обработка ошибок. Структурированный параллелизм упрощает обработку и распространение ошибок, поскольку ошибки можно обрабатывать и распространять в рамках иерархии задач.

ExecutorService и пулы потоков

  1. ExecutorService и пулы потоков. Платформа параллелизма Java предоставляет высокоуровневые API, такие как ExecutorService, и различные реализации пулов потоков, такие как FixedThreadPool и CachedThreadPool, для управления параллельными задачами.
  2. CompletableFuture. Класс Java CompletableFuture обеспечивает асинхронные вычисления, позволяя разработчикам связывать и комбинировать задачи более структурированным образом. Однако это по своей сути не обеспечивает структурированного параллелизма.
  3. Менее организованный: механизмы параллелизма Java по своей сути не обеспечивают структурированный параллелизм, что в некоторых случаях усложняет анализ и управление параллельным кодом.

Заключение

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

Использование StructuredTaskScope и связанных с ним изменений API упрощает для разработчиков написание чистого, удобного в сопровождении и эффективного параллельного кода. Упрощая обработку и распространение ошибок, снижая вероятность утечки ресурсов и предотвращая бесхозные задачи, структурированный параллелизм прокладывает путь к более надежному и масштабируемому параллельному программированию.

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