Skip to content

Commit 86bf101

Browse files
committed
Fix issue#112:翻译小节复杂性和代价
1 parent 5731617 commit 86bf101

File tree

1 file changed

+350
-0
lines changed

1 file changed

+350
-0
lines changed

docs/book/24-Concurrent-Programming.md

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2461,8 +2461,358 @@ public class SynchronizedFactory{
24612461
<!-- Effort, Complexity,Cost -->
24622462
## 复杂性和代价
24632463

2464+
假设您正在做披萨,我们把从整个流程的当前步骤到下一个步骤所需的工作量,在这里一一表示为枚举变量的一部分:
2465+
2466+
2467+
2468+
```java
2469+
// concurrent/Pizza.java import java.util.function.*;
2470+
2471+
import onjava.Nap;
2472+
public class Pizza{
2473+
public enum Step{
2474+
DOUGH(4), ROLLED(1), SAUCED(1), CHEESED(2),
2475+
TOPPED(5), BAKED(2), SLICED(1), BOXED(0);
2476+
int effort;// Needed to get to the next step
2477+
2478+
Step(int effort){
2479+
this.effort = effort;
2480+
}
2481+
2482+
Step forward(){
2483+
if (equals(BOXED)) return BOXED;
2484+
new Nap(effort * 0.1);
2485+
return values()[ordinal() + 1];
2486+
}
2487+
}
2488+
2489+
private Step step = Step.DOUGH;
2490+
private final int id;
2491+
2492+
public Pizza(int id){
2493+
this.id = id;
2494+
}
2495+
2496+
public Pizza next(){
2497+
step = step.forward();
2498+
System.out.println("Pizza " + id + ": " + step);
2499+
return this;
2500+
}
2501+
2502+
public Pizza next(Step previousStep){
2503+
if (!step.equals(previousStep))
2504+
throw new IllegalStateException("Expected " +
2505+
previousStep + " but found " + step);
2506+
return next();
2507+
}
2508+
2509+
public Pizza roll(){
2510+
return next(Step.DOUGH);
2511+
}
2512+
2513+
public Pizza sauce(){
2514+
return next(Step.ROLLED);
2515+
}
2516+
2517+
public Pizza cheese(){
2518+
return next(Step.SAUCED);
2519+
}
2520+
2521+
public Pizza toppings(){
2522+
return next(Step.CHEESED);
2523+
}
2524+
2525+
public Pizza bake(){
2526+
return next(Step.TOPPED);
2527+
}
2528+
2529+
public Pizza slice(){
2530+
return next(Step.BAKED);
2531+
}
2532+
2533+
public Pizza box(){
2534+
return next(Step.SLICED);
2535+
}
2536+
2537+
public boolean complete(){
2538+
return step.equals(Step.BOXED);
2539+
}
2540+
2541+
@Override
2542+
public String toString(){
2543+
return "Pizza" + id + ": " + (step.equals(Step.BOXED) ? "complete" : step);
2544+
}
2545+
}
2546+
```
2547+
2548+
这只算得上是一个简单的状态机,就像Machina类一样。
2549+
2550+
制作一个披萨,当披萨饼最终被放在盒子中时,就算完成最终任务了。 如果一个人在做一个披萨饼,那么所有步骤都是线性进行的,即一个接一个地进行:
2551+
2552+
```java
2553+
// concurrent/OnePizza.java
2554+
2555+
import onjava.Timer;
2556+
2557+
public class OnePizza{
2558+
public static void main(String[] args){
2559+
Pizza za = new Pizza(0);
2560+
System.out.println(Timer.duration(() -> {
2561+
while (!za.complete()) za.next();
2562+
}));
2563+
}
2564+
}
2565+
/* Output:
2566+
Pizza 0: ROLLED
2567+
Pizza 0: SAUCED
2568+
Pizza 0: CHEESED
2569+
Pizza 0: TOPPED
2570+
Pizza 0: BAKED
2571+
Pizza 0: SLICED
2572+
Pizza 0: BOXED
2573+
1622
2574+
*/
2575+
```
2576+
2577+
时间以毫秒为单位,加总所有步骤的工作量,会得出与我们的期望值相符的数字。 如果您以这种方式制作了五个披萨,那么您会认为它花费的时间是原来的五倍。 但是,如果这还不够快怎么办? 我们可以从尝试并行流方法开始:
2578+
2579+
```java
2580+
// concurrent/PizzaStreams.java
2581+
// import java.util.*; import java.util.stream.*;
2582+
2583+
import onjava.Timer;
2584+
2585+
public class PizzaStreams{
2586+
static final int QUANTITY = 5;
2587+
2588+
public static void main(String[] args){
2589+
Timer timer = new Timer();
2590+
IntStream.range(0, QUANTITY)
2591+
.mapToObj(Pizza::new)
2592+
.parallel()//[1]
2593+
.forEach(za -> { while(!za.complete()) za.next(); }); System.out.println(timer.duration());
2594+
}
2595+
}
2596+
/* Output:
2597+
Pizza 2: ROLLED
2598+
Pizza 0: ROLLED
2599+
Pizza 1: ROLLED
2600+
Pizza 4: ROLLED
2601+
Pizza 3:ROLLED
2602+
Pizza 2:SAUCED
2603+
Pizza 1:SAUCED
2604+
Pizza 0:SAUCED
2605+
Pizza 4:SAUCED
2606+
Pizza 3:SAUCED
2607+
Pizza 2:CHEESED
2608+
Pizza 1:CHEESED
2609+
Pizza 0:CHEESED
2610+
Pizza 4:CHEESED
2611+
Pizza 3:CHEESED
2612+
Pizza 2:TOPPED
2613+
Pizza 1:TOPPED
2614+
Pizza 0:TOPPED
2615+
Pizza 4:TOPPED
2616+
Pizza 3:TOPPED
2617+
Pizza 2:BAKED
2618+
Pizza 1:BAKED
2619+
Pizza 0:BAKED
2620+
Pizza 4:BAKED
2621+
Pizza 3:BAKED
2622+
Pizza 2:SLICED
2623+
Pizza 1:SLICED
2624+
Pizza 0:SLICED
2625+
Pizza 4:SLICED
2626+
Pizza 3:SLICED
2627+
Pizza 2:BOXED
2628+
Pizza 1:BOXED
2629+
Pizza 0:BOXED
2630+
Pizza 4:BOXED
2631+
Pizza 3:BOXED
2632+
1739
2633+
*/
2634+
```
2635+
2636+
现在,我们制作五个披萨的时间与制作单个披萨的时间就差不多了。 尝试删除标记为[1]的行后,你会发现它花费的时间是原来的五倍。 你还可以尝试将QUANTITY更改为48101617,看看会有什么不同,并猜猜看为什么会这样。
2637+
2638+
PizzaStreams类产生的每个并行流在它的forEach()内完成所有工作,如果我们将其各个步骤用映射的方式一步一步处理,情况会有所不同吗?
2639+
2640+
```java
2641+
// concurrent/PizzaParallelSteps.java
2642+
2643+
import java.util.*;
2644+
import java.util.stream.*;
2645+
import onjava.Timer;
2646+
2647+
public class PizzaParallelSteps{
2648+
static final int QUANTITY = 5;
2649+
2650+
public static void main(String[] args){
2651+
Timer timer = new Timer();
2652+
IntStream.range(0, QUANTITY)
2653+
.mapToObj(Pizza::new)
2654+
.parallel()
2655+
.map(Pizza::roll)
2656+
.map(Pizza::sauce)
2657+
.map(Pizza::cheese)
2658+
.map(Pizza::toppings)
2659+
.map(Pizza::bake)
2660+
.map(Pizza::slice)
2661+
.map(Pizza::box)
2662+
.forEach(za -> System.out.println(za));
2663+
System.out.println(timer.duration());
2664+
}
2665+
}
2666+
/* Output:
2667+
Pizza 2: ROLLED
2668+
Pizza 0: ROLLED
2669+
Pizza 1: ROLLED
2670+
Pizza 4: ROLLED
2671+
Pizza 3: ROLLED
2672+
Pizza 1: SAUCED
2673+
Pizza 0: SAUCED
2674+
Pizza 2: SAUCED
2675+
Pizza 3: SAUCED
2676+
Pizza 4: SAUCED
2677+
Pizza 1: CHEESED
2678+
Pizza 0: CHEESED
2679+
Pizza 2: CHEESED
2680+
Pizza 3: CHEESED
2681+
Pizza 4: CHEESED
2682+
Pizza 0: TOPPED
2683+
Pizza 2: TOPPED
2684+
Pizza 1: TOPPED
2685+
Pizza 3: TOPPED
2686+
Pizza 4: TOPPED
2687+
Pizza 1: BAKED
2688+
Pizza 2: BAKED
2689+
Pizza 0: BAKED
2690+
Pizza 4: BAKED
2691+
Pizza 3: BAKED
2692+
Pizza 0: SLICED
2693+
Pizza 2: SLICED
2694+
Pizza 1: SLICED
2695+
Pizza 3: SLICED
2696+
Pizza 4: SLICED
2697+
Pizza 1: BOXED
2698+
Pizza1: complete
2699+
Pizza 2: BOXED
2700+
Pizza 0: BOXED
2701+
Pizza2: complete
2702+
Pizza0: complete
2703+
Pizza 3: BOXED
2704+
Pizza 4: BOXED
2705+
Pizza4: complete
2706+
Pizza3: complete
2707+
1738
2708+
*/
2709+
```
2710+
2711+
答案是“否”,事后看来这并不奇怪,因为每个披萨都需要按顺序执行步骤。因此,没法通过分步执行操作来进一步提高速度,就像上文的PizzaParallelSteps.java里面展示的一样。
2712+
2713+
我们可以使用CompletableFutures重写这个例子:
2714+
2715+
```java
2716+
// concurrent/CompletablePizza.java
2717+
2718+
import java.util.*;
2719+
import java.util.concurrent.*;
2720+
import java.util.stream.*;
2721+
import onjava.Timer;
2722+
2723+
public class CompletablePizza{
2724+
static final int QUANTITY = 5;
2725+
2726+
public static CompletableFuture<Pizza> makeCF(Pizza za){
2727+
return CompletableFuture
2728+
.completedFuture(za)
2729+
.thenApplyAsync(Pizza::roll)
2730+
.thenApplyAsync(Pizza::sauce)
2731+
.thenApplyAsync(Pizza::cheese)
2732+
.thenApplyAsync(Pizza::toppings)
2733+
.thenApplyAsync(Pizza::bake)
2734+
.thenApplyAsync(Pizza::slice)
2735+
.thenApplyAsync(Pizza::box);
2736+
}
2737+
2738+
public static void show(CompletableFuture<Pizza> cf){
2739+
try{
2740+
System.out.println(cf.get());
2741+
} catch (Exception e){
2742+
throw new RuntimeException(e);
2743+
}
2744+
}
2745+
2746+
public static void main(String[] args){
2747+
Timer timer = new Timer();
2748+
List<CompletableFuture<Pizza>> pizzas =
2749+
IntStream.range(0, QUANTITY)
2750+
.mapToObj(Pizza::new)
2751+
.map(CompletablePizza::makeCF)
2752+
.collect(Collectors.toList());
2753+
System.out.println(timer.duration());
2754+
pizzas.forEach(CompletablePizza::show);
2755+
System.out.println(timer.duration());
2756+
}
2757+
}
2758+
/* Output:
2759+
169
2760+
Pizza 0: ROLLED
2761+
Pizza 1: ROLLED
2762+
Pizza 2: ROLLED
2763+
Pizza 4: ROLLED
2764+
Pizza 3: ROLLED
2765+
Pizza 1: SAUCED
2766+
Pizza 0: SAUCED
2767+
Pizza 2: SAUCED
2768+
Pizza 4: SAUCED
2769+
Pizza 3: SAUCED
2770+
Pizza 0: CHEESED
2771+
Pizza 4: CHEESED
2772+
Pizza 1: CHEESED
2773+
Pizza 2: CHEESED
2774+
Pizza 3: CHEESED
2775+
Pizza 0: TOPPED
2776+
Pizza 4: TOPPED
2777+
Pizza 1: TOPPED
2778+
Pizza 2: TOPPED
2779+
Pizza 3: TOPPED
2780+
Pizza 0: BAKED
2781+
Pizza 4: BAKED
2782+
Pizza 1: BAKED
2783+
Pizza 3: BAKED
2784+
Pizza 2: BAKED
2785+
Pizza 0: SLICED
2786+
Pizza 4: SLICED
2787+
Pizza 1: SLICED
2788+
Pizza 3: SLICED
2789+
Pizza 2: SLICED
2790+
Pizza 4: BOXED
2791+
Pizza 0: BOXED
2792+
Pizza0: complete
2793+
Pizza 1: BOXED
2794+
Pizza1: complete
2795+
Pizza 3: BOXED
2796+
Pizza 2: BOXED
2797+
Pizza2: complete
2798+
Pizza3: complete
2799+
Pizza4: complete
2800+
1797
2801+
*/
2802+
```
2803+
2804+
并行流和CompletableFuturesJava并发工具箱中最先进发达的技术。 您应该始终首先选择其中之一。 当一个问题很容易并行处理时,或者说,很容易把数据分解成相同的、易于处理的各个部分时,使用并行流方法处理最为合适(而如果您决定不借助它而由自己完成,您就必须撸起袖子,深入研究Spliterator的文档)。
2805+
2806+
而当工作的各个部分内容各不相同时,使用CompletableFutures是最好的选择。比起面向数据,CompletableFutures更像是面向任务的。
2807+
2808+
对于披萨问题,结果似乎也没有什么不同。实际上,并行流方法看起来更简洁,仅出于这个原因,我认为并行流作为解决问题的首次尝试方法更具吸引力。
2809+
2810+
由于制作披萨总需要一定的时间,无论您使用哪种并发方法,你能做到的最好情况,是在制作一个披萨的相同时间内制作n个披萨。 在这里当然很容易看出来,但是当您处理更复杂的问题时,您就可能忘记这一点。 通常,在项目开始时进行粗略的计算,就能很快弄清楚最大可能的并行吞吐量,这可以防止您因为采取无用的加快运行速度的举措而忙得团团转。
2811+
2812+
使用CompletableFutures或许可以轻易地带来重大收益,但是在尝试更进一步时需要倍加小心,因为额外增加的成本和工作量会非常容易远远超出你之前拼命挤出的那一点点收益。
24642813

24652814
<!-- Summary -->
2815+
24662816
## 本章小结
24672817

24682818
[^1]:例如,Eric-Raymond在“VIIX编程艺术”(Addison-Wesley2004)中提出了一个很好的案例。

0 commit comments

Comments
 (0)