Есть ли сборка в "медленной" версии Future.traverse?

Я обнаружил, что создание большого количества фьючерсов для одного запроса пользователя, как правило, является плохой практикой. Эти фьючерсы могут заполнить контекст выполнения, который повлияет на другие запросы. Это маловероятно, чего вы действительно хотите. Хранение фьючерсов число малое - создать новые фьючерсы только для понятий, используя flatMap и т.д. Но иногда может потребоваться создать Будущее для каждого элемента Seq. Использование проблемы Future.sequence или Future.traverse, описанной выше. Таким образом, я получил это решение, которое не создает фьючерсы для каждого элемента коллекции одновременно:

  def ftraverse[A, B](xs: Seq[A])(f: A => Future[B])(implicit ec: ExecutionContext): Future[Seq[B]] = {
    if(xs.isEmpty) Future successful Seq.empty[B]
    else f(xs.head) flatMap { fh => ftraverse(xs.tail)(f) map (r => fh +: r) }
  }

Интересно, может, я изобретаю колесо, и на самом деле такая функция уже существует где-то в стандартной библиотеке Scala? Также я хотел бы знать, вы столкнулись с описанной проблемой и как вы ее разрешили? Может быть, если это хорошо известная проблема с Futures, я должен создать запрос на перенос в Future.scala, чтобы эта функция (или более обобщенная версия) была включена в стандартную библиотеку?

UPD: более общая версия с ограниченным parallelism:

  def ftraverse[A, B](xs: Seq[A], chunkSize: Int, maxChunks: Int)(f: A => Future[B])(implicit ec: ExecutionContext): Future[Seq[B]] = {
    val xss = xs.grouped(chunkSize).toList
    val chunks = xss.take(maxChunks-1) :+ xss.drop(maxChunks-1).flatten
    Future.sequence{ chunks.map(chunk => ftraverse(chunk)(f) ) } map { _.flatten }
  } 

Ответ 1

Нет, в стандартной библиотеке нет ничего подобного. Должен быть или нет, я не могу сказать. Я не думаю, что очень часто нужно выполнять Future в строгой последовательности. Но когда вы этого хотите, очень легко реализовать свой собственный метод, чтобы сделать это, как и у вас. Я лично просто держу метод в своих собственных библиотеках для этой цели. Однако было бы удобно иметь способ сделать это со стандартной библиотекой. Если бы это было так, это было бы более общим.

На самом деле очень просто изменить текущий traverse для обработки Future последовательно, а не параллельно. Ниже приведена текущая версия, в которой вместо рекурсии используется foldLeft:

def traverse[A, B, M[X] <: TraversableOnce[X]](in: M[A])(fn: A => Future[B])(implicit cbf: CanBuildFrom[M[A], B, M[B]], executor: ExecutionContext): Future[M[B]] =
    in.foldLeft(Future.successful(cbf(in))) { (fr, a) =>
      val fb = fn(a)
      for (r <- fr; b <- fb) yield (r += b)
    }.map(_.result())

Future создаются до flatMap, назначая val fb = fn(a) (и таким образом исполняемый ранее). Все, что нужно сделать, это переместить fn(a) внутри flatMap, чтобы задержать создание следующего Future в коллекции.

def traverseSeq[A, B, M[X] <: TraversableOnce[X]](in: M[A])(fn: A => Future[B])(implicit cbf: CanBuildFrom[M[A], B, M[B]], executor: ExecutionContext): Future[M[B]] =
    in.foldLeft(Future.successful(cbf(in))) { (fr, a) =>
      for (r <- fr; b <- fn(a)) yield (r += b)
    }.map(_.result())

Другой способ ограничить влияние выполнения большого числа Future заключается в использовании для них другого ExecutionContext. Например, в веб-приложении я могу оставить один ExecutionContext для вызовов базы данных, один для вызовов на Amazon S3 и один для медленных вызовов базы данных.

В очень простой реализации можно использовать фиксированные пулы потоков:

import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext
val executorService = Executors.newFixedThreadPool(4)
val executionContext = ExecutionContext.fromExecutorService(executorService)

Большое количество выполняемых здесь Future заполнит ExecutionContext, но это помешает им заполнить другие контексты.

Если вы используете Akka, вы можете легко создать ExecutionContext из конфигурации с помощью Dispatchers в ActorSystem:

my-dispatcher {
  type = Dispatcher
  executor = "fork-join-executor"
  fork-join-executor {
    parallelism-min = 2
    parallelism-factor = 2.0
    parallelism-max = 10
  }
  throughput = 100
}

Если у вас есть ActorSystem, называемый system, вы можете получить к нему доступ через:

implicit val executionContext = system.dispatchers.lookup("my-dispatcher")

Все это зависит от вашего варианта использования. Хотя я отделяю свои асинхронные вычисления в разных контекстах, бывают случаи, когда я все еще хочу traverse последовательно сгладить использование этих контекстов.

Ответ 2

Кажется, что ваша проблема не связана с количеством созданных вами фьючерсов, а с той честностью, с которой они выполняются. Рассмотрим, как обрабатываются обратные вызовы по фьючерсам (map, flatMap, onComplete, fold и т.д.): Они помещаются в очередь исполнителей и выполняются, когда результаты их родительских фьючерсов завершены.

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

Ответ 3

Разве это не параллельные коллекции?

val parArray = (1 to 1000000).toArray.par
sum = parArray.map(_ + _)
res0: Int = 1784293664

выглядит как обычный синхронный вызов метода, но параллельная коллекция будет использовать threadpool для вычисления карты параллельно (условия гонки!). Здесь вы найдете более подробную информацию: http://docs.scala-lang.org/overviews/parallel-collections/overview.html

Ответ 4

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

Вы можете вызвать ExecutionContext.fromExecutorService или ExecutionContext.fromExecutor, чтобы сделать это.