Parallel Programming in Java 是 Coursera 的上的一门课程,一共有四周课程内容,讲述Java中的并行程序设计。这里是第二周课程的内容笔记。主要内容为 Functional Parallelism,即 函数式并行
Functional parallelism 理解的重点在于 Future Tasks 和 Future objects(或称 promise objects)。
future task可以理解为是一种描述计算图的方式,通过 future 表示当前任务计算完成之后的结果,供后面的步骤调用,因此在建模时就能够很自然的描述出全部计算图的依赖关系,在依据函数相互的调用关系就能完成并行建模。在 future 模型中有两个关键的问题:
上图是来自课程Quiz的一道题目,更好地展示 future 的分析作用,其中可以看出
通过以上分析,我认为从 future 块到计算图的还原最好采用逆向分析的方式,即应用递归程序的思想(实际上框架似乎也是这么分析的)
框架的使用方法和之前的方式较为类似,关键点还是在于实现 compute()
函数进行计算,并使用 join()
函数完成阻塞操作,主要有以下需要特别注意的点:
compute()
函数是有返回值的,不能是 void
类型相当于给计算结果建立 Cache,例如:对于 y 1 = G ( x 1 ) y_{1}=G(x_{1}) y1=G(x1) ,当计算完毕之后,不仅仅赋值给 y 1 y_{1} y1 。会同时记录下这个结果来自于 f u t u r e { G , x } future\{G,x\} future{G,x} ,因此在下次调用这个结果时,就可以通过直接查表获取到结果从而避免计算。
Memoization 是动态规划算法的设计来源,即通过使用存储来换取运算时间上的优化。
因为依旧是使用 future 模型进行建模,因此这里还是要求实现一个 get()
操作来获取计算出来的结果的值。
这是 Java 8 中加入的新特性,主要针对一个 for 循环,可以通过调用 parallel stream 实现并行化得循环计算。
students.stream().forEach(s \rightarrow→ System.out.println(s));
students.stream()
.filter(s -> s.getStatus() == Student.ACTIVE)
.mapToInt(a -> a.getAge())
.average();
计算得关键点有两个,即 filter
用来过滤集合中符合条件得元素,map
用来调用集合中每个元素的计算值。使用 stream 的的方式就可以方便的建立并行化的计算了
tudents.parallelStream()
// or
Stream.of(students).parallel()
functional determinism:指函数在相同的输入下会有相同输出的性质
structural determinism:指程序中对于相同的输入会产生相同计算图的性质
程序中的不确定的性通常是由于数据竞争导致的
data race freedom = functional determinism + structural determinism
有数据竞争出现的程序并不一定是非确定的程序
没有数据竞争也不一定能保证确定性
使用课程中介绍的模型,在不发生数据竞争的前提下就可以保证是确定性程序
benign non-determinism:指程序中虽然不能保证确定性,但是非确定的结果对于程序的正确性来说是可以接受的
/**
* Sequentially computes the number of students who have failed the course
* who are also older than 20 years old. A failing grade is anything below a
* 65. A student has only failed the course if they have a failing grade and
* they are not currently active.
*
* @param studentArray Student data for the class.
* @return Number of failed grades from students older than 20 years old.
*/
public int countNumberOfFailedStudentsOlderThan20Imperative(
final Student[] studentArray) {
int count = 0;
for (Student s : studentArray) {
if (!s.checkIsCurrent() && s.getAge() > 20 && s.getGrade() < 65) {
count++;
}
}
return count;
}
/**
* TODO compute the number of students who have failed the course who are
* also older than 20 years old. A failing grade is anything below a 65. A
* student has only failed the course if they have a failing grade and they
* are not currently active. This should mirror the functionality of
* countNumberOfFailedStudentsOlderThan20Imperative. This method should not
* use any loops.
*
* @param studentArray Student data for the class.
* @return Number of failed grades from students older than 20 years old.
*/
public int countNumberOfFailedStudentsOlderThan20ParallelStream(
final Student[] studentArray) {
return (int)Stream.of(studentArray).parallel()
.filter(s -> !s.checkIsCurrent()
&& s.getGrade() < 65 && s.getAge() > 20)
.count();
}
/**
* Sequentially computes the most common first name out of all students that
* are no longer active in the class using loops.
*
* @param studentArray Student data for the class.
* @return Most common first name of inactive students
*/
public String mostCommonFirstNameOfInactiveStudentsImperative(
final Student[] studentArray) {
List<Student> inactiveStudents = new ArrayList<Student>();
for (Student s : studentArray) {
if (!s.checkIsCurrent()) {
inactiveStudents.add(s);
}
}
Map<String, Integer> nameCounts = new HashMap<String, Integer>();
for (Student s : inactiveStudents) {
if (nameCounts.containsKey(s.getFirstName())) {
nameCounts.put(s.getFirstName(),
new Integer(nameCounts.get(s.getFirstName()) + 1));
} else {
nameCounts.put(s.getFirstName(), 1);
}
}
String mostCommon = null;
int mostCommonCount = -1;
for (Map.Entry<String, Integer> entry : nameCounts.entrySet()) {
if (mostCommon == null || entry.getValue() > mostCommonCount) {
mostCommon = entry.getKey();
mostCommonCount = entry.getValue();
}
}
return mostCommon;
}
/**
* TODO compute the most common first name out of all students that are no
* longer active in the class using parallel streams. This should mirror the
* functionality of mostCommonFirstNameOfInactiveStudentsImperative. This
* method should not use any loops.
*
* @param studentArray Student data for the class.
* @return Most common first name of inactive students
*/
public String mostCommonFirstNameOfInactiveStudentsParallelStream(
final Student[] studentArray) {
Map<String, Long> map = Stream.of(studentArray).parallel()
.filter(s -> !s.checkIsCurrent())
.collect(Collectors.groupingBy(Student::getFirstName,
Collectors.counting()));
return map.keySet().stream()
.max((x, y) -> Long.compare(map.get(x), map.get(y))).get();
}
相比之下,这个例子更能体现出 stream 的易用性,在这里,collect 被用作一个收集器进行分类汇总,然后将结果传递给下游收集器 Collectors.counting()
进行进一步的 reduce 计算。
出了上面给出的例子,reduce
也是一个功能强大的 API,更多信息参考:
Java8-15-Stream 收集器 01-归约与汇总+分组
Java Streams,第 2 部分- 使用流执行聚合-轻松地分解数据