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

Я попытался сделать необязательным выполнение операции map последовательно или параллельно, например, используя следующий код:

val runParallel = true
val theList = List(1,2,3,4,5)
(if(runParallel) theList.par else theList) map println //Doesn't run in parallel

Что я заметил, так это то, что операция «карта» не выполняется параллельно, как я ожидал. Хотя без условного условия это было бы:

theList.par map println   //Runs in parallel as visible in the output

Тип выражения (if(runParallel) theList else theList.par), который, как я ожидаю, будет ближайшим общим предком обоих типов theList и theList.par, является пугающим типом, который я не буду вставлять сюда, но на него интересно посмотреть (через консоль scala:)

:type (if(true) theList else theList.par)

Почему map в параллельной коллекции не работает параллельно?

ОБНОВЛЕНИЕ: это обсуждается в SI-4843, но из тикета JIRA это неясно почему это происходило на Scala 2.9.x.


person omid    schedule 18.01.2013    source источник


Ответы (1)


Объяснять, почему это происходит, долго: в Scala 2.9.x (не знаю, как в других версиях) такие методы коллекций, как filter или map, опираются на механизм CanBuildFrom. Идея состоит в том, что у вас есть неявный параметр, который используется для создания построителя новой коллекции:

def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {
    val b = bf(repr)
    b.sizeHint(this) 
    for (x <- this) b += f(x)
    b.result
  }

Благодаря этому механизму метод карты определен только в трейте TraversableLike, и его подклассы не должны его переопределять. Как видите, внутри сигнатуры карты методов есть много параметров типа. Давайте посмотрим на тривиальные:

  • B — это тип элементов новой коллекции.
  • A — это тип элементов в исходной коллекции.

Рассмотрим более сложные:

  • That — это новый тип коллекции, который может отличаться от текущего типа. Классический пример — когда вы сопоставляете, например, BitSet с помощью toString:

     scala> val a = BitSet(1,3,5)
     a scala.collection.immutable.BitSet = BitSet(1, 3, 5)
     scala>  a.map {_.toString}
     res2: scala.collection.immutable.Set[java.lang.String] = Set(1, 3, 5)
    

Поскольку создание BitSet[String] незаконно, результатом вашей карты будет Set[String] Наконец, Repr — это тип текущей коллекции. Когда вы пытаетесь сопоставить коллекцию с функцией, компилятор разрешает подходящий CanBuildFrom, используя параметры типа.

Поскольку это разумно, метод карты был переопределен в параллельных коллекциях в ParIterableLike следующим образом:

 def map[S, That](f: T => S)(implicit bf: CanBuildFrom[Repr, S, That]): That = bf ifParallel { pbf =>
    executeAndWaitResult(new Map[S, That](f, pbf, splitter) mapResult { _.result })
  } otherwise seq.map(f)(bf2seq(bf))

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

Однако что происходит, когда вы делаете

(if(runParallel) theList.par else theList) map println //Doesn't run in parallel

метод карты выполняется в результате

  (if(runParallel) theList.par else theList) 

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


Заключение

Когда вы вызываете метод map, компилятор должен найти подходящий CanBuildFrom[Repr, B, That] для операции. Так как наш Repr1 не относится к параллельной коллекции, не будет CanBuildFrom[Repr1,B,That], способных предоставить параллельный строитель. На самом деле это правильное поведение в отношении реализации коллекций Scala, если бы поведение было другим, это означало бы, что каждая карта непараллельных коллекций также выполнялась бы параллельно.

Дело в том, что для того, как коллекции Scala разработаны в 2.9.x, нет альтернативы. Если компилятор не предоставляет CanBuildFrom для параллельной коллекции, карта не будет параллельной.

person Edmondo1984    schedule 18.01.2013
comment
Спасибо за отличный подробный ответ. - person omid; 21.01.2013