这是我参与更文挑战的第2天,活动详情查看: 更文挑战

lambda表达式的使用

使用lambda表达式的目的是解决匿名类的繁琐,以及Java中的函数式编程问题,比如著名的策略模式。

我们使用lambda表达式的主要目的是为了代码简洁,如果使用lambda导致代码阅读与理解费力,不便于沟通,就得不偿失了。

我最早使用lambda表达式是在创建线程时。

对比使用lambda与不使用lambda的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 不使用lambda
*/
new Thread(new Runnable() {
public void run() {
System.out.println("无lambda:线程执行");
}
}).start();

/**
* 使用lambda
*/
new Thread(() -> {
System.out.println("lambda:线程执行");
}).start();

lambda代码更简洁。

【Effective Java】[第三版]第42条,列举了排序的lambda写法,编译器利用类型推导,根据上下文推断出这些类型。代码如下所示:

1
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

作者提到,应该删除所有lambda参数的类型,除非它们的存在能够使程序变得更加清晰。

在第43条,作者推荐:方法引用优先于lambda,即上述代码可以修改为:

1
Collections.sort(words, comparingInt(String::length));

代码不光应该让编译器读懂,更应该让人读懂,如果一个参数,代码阅读者不能很快的推导出其类型,就要考虑lambda的写法是否合适了。lambda没有名称和文档,如果一个计算本身不是自描述的,或者超出了几行,那就不要把它放在一个lambda中。【Effective Java】[第三版]P166。

自定义lambda接口

只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口。【Effective Java】第44条P169。

只有你所需要的函数接口满足以下特征时,才考虑自己编写函数接口:

  1. 通用,并且将受益与描述性名称。
  2. 具有与其关联的严格的契约。
  3. 将受益于定制的缺省方法。

必须始终用@FunctionalInterface注解对自己编写的函数接口进行标注。

一个自定义与使用样例如下:

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface Number<T> {
T operate(T a, T b);
}

// 执行过程中
Number<Integer> add = (a, b) -> a + b;
Number<Integer> multi = (a, b) -> a * b;
System.out.println(add.operate(3, 4)); // 7
System.out.println(multi.operate(3, 4)); // 12

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

根据【Effective Java】[第三版]第六章,枚举与注解 所述,枚举类型是指由一组固定的常量组成合法值的类型。

Talk is Cheap, Show me the Code!

典型枚举模板

下面是一个个人常用的枚举enum模板。

代码分为n个部分:

  1. 所有的枚举变量
    如代码所示的部分:

    1
    TO_PAY, TO_DELIVER, TO_RECEIVE, FINISHED;
  2. 枚举的实例域

    1
    2
    private final int val;
    private final String name;

    枚举天生不可变,因此,枚举的所有实例域均应该设置为final。

  3. 其他类型与枚举类型相互转换的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
       /**
    * 枚举转int
    * @return
    */
    public int getVal() {
    return val;
    }

    /**
    * int转枚举
    * @param val
    * @return
    */
    public static OrderState getOrderState(int val) {
    for (OrderState orderState : OrderState.values()) {
    if (orderState.val == val) {
    return orderState;
    }
    }
    throw new RuntimeException("错误的订单状态");
    }

    其中特别注意:values()这个方法,该方法按顺序返回枚举的所有值数组。

  4. 抽象方法

    通过抽象方法,我们可以避免使用switch-case语句,以避免在以后新增枚举值时,忘记给对应值添加相关特殊操作。
    枚举中也可以使用switch-case。
    【Effective Java】中:“枚举中的switch-case语句适合于给外部的枚举类型增加特定于常量的行为”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* 订单状态
*/
public enum OrderState {
TO_PAY("待付款", 0) {
@Override
public boolean deliver() {
return false;
}
},
TO_DELIVER("待发货", 1) {
@Override
public boolean deliver() {
return true;
}
},
TO_RECEIVE("待收货", 2) {
@Override
public boolean deliver() {
return false;
}
},
FINISHED("已完成", 3) {
@Override
public boolean deliver() {
return false;
}
};

/**
* 枚举天生不可变,所有的域都应声明为final
*/
private final int val;
private final String name;

/**
* 私有修饰,防止外部调用
* @param name
* @param val
*/
private OrderState(String name, int val) {
this.name = name;
this.val = val;
}

/**
* 枚举转int
* @return
*/
public int getVal() {
return val;
}

public String getName() {
return name;
}

/**
* int转枚举
* @param val
* @return
*/
public static OrderState getOrderState(int val) {
for (OrderState orderState : OrderState.values()) {
if (orderState.val == val) {
return orderState;
}
}
throw new RuntimeException("错误的订单状态");
}

/**
* 抽象方法:发货
* @return
*/
public abstract boolean deliver();

}

何时应该使用枚举

每当需要一组固定常量并在编译时就知道其成员的时候,就应该使用枚举。
但是,枚举类型中的常量集并不一定要始终保持不变。

枚举的小缺陷

装载和初始化枚举时,需要空间与时间成本,几乎可以忽略。

非专业翻译,欢迎指正!

——————————-原文翻译————————————-

原文链接:http://gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html

辅助翻译工具:DeepL

何时使用并行流?

[草稿,2014年9月1日。目前,最小程度格式规范,刊登位置待定。]

java.util.streams框架支持对集合和其他资源进行数据驱动的操作。大多数流方法对每个数据元素应用相同的操作。当有多个CPU核心可用时,通过使用集合的parallelStream()方法,”数据驱动 “可以变成 “数据并行”。但是,你应该在什么时候这样做呢?

当操作是独立的,并且计算成本很高,或操作是应用于可有效分割的数据结构的大量元素,或者两者兼而有之时,考虑使用S.parallelStream().operation(F)代替S.stream().operation(F)。更详细地讲:

  • F,每个元素的函数(通常是一个lambda)是独立的:每个元素的计算不依赖或影响任何其他元素的计算。(查看stream包摘要获取关于使用无状态非干扰函数的进一步指导)。
  • S,源集合是可有效分割的。除了Collections,还有一些其他容易并行化的流源,例如java.util.SplittableRandom(对于它,你可以使用stream.parallel()方法来并行化)。但是大多数基于IO的源主要是为顺序使用而设计的。
  • 执行顺序版本的总时间超过了一个最低阈值。如今,在大多数平台上,这个阈值大约是100微秒(同一数量级内)。不过你不需要精确地测量这个。在实践中,你可以通过用N(元素的数量)乘以Q(执行F的每个元素的开销)来估计这个数字,反过来把Q估计为操作或代码行的数量,然后检查N*Q至少是10000。(如果不确定,可以再加一两个零)因此,当F是一个很小的函数,如x->x+1,那么它需要N>=10000个元素,并行执行才是值得的。反之,当F是一个大规模的计算,比如在国际象棋游戏中寻找最佳的下一步,Q因子是如此之高,以至于只要集合是完全可分割的,N就不重要了。
  • (个人补充)对于IO操作,数据库操作,则要去估计时间开销了,代码行数已经没有太多的参考价值。

流框架没有(也不能)强制要求满足这些条件。如果计算不是独立的,那么并行运行将没有任何意义,甚至可能导致严重的错误。其他标准源于三个工程问题和权衡:

  • 启动
    随着处理器多年来不断增加内核,大多数处理器也增加了功率控制机制,这会使内核启动缓慢,有时还会有JVM、操作系统和管理程序所带来的额外开销。此启动开销与大量内核处理并行子任务需要的时间相当。一旦启动,并行计算可能比顺序计算更节能(取决于各种处理器和系统细节;例如,见Federova等人的这篇文章)。
  • 颗粒度
    对已经很小的计算进行细分是不值得的。框架通常会将问题分割开来,以便由系统中所有可用的核来处理子问题。如果每个核心在启动后实际上没有什么可做的,那么(大多顺序执行)设置为并行计算的努力就被白费了。考虑到现在内核的实际范围是2到256个,这个阈值也避免了过度任务拆分的影响。
  • 可分割性
    最有效的可分割集合包括ArrayLists和{Concurrent}HashMaps,以及简单的数组(即那些形式为T[],可使用静态java.util.Arrays方法分割)。效率最低的是LinkedLists、BlockingQueues和大多数基于IO的资源。其他资源则介于两者之间。(如果数据结构内部支持随机访问、高效搜索或两者兼而有之,那么它们往往是可有效分割的。) 如果分割数据的时间比处理数据的时间要长,那么这些努力就白费了。所以,如果计算的Q因子足够高,即使是LinkedList,你也可能获得并行加速,但这并不常见。此外,有些数据源不能被完全分割成单个元素,所以如何很好地分割任务可能存在限制。

收集这些影响的详细测量结果可能是困难的(尽管使用诸如JMH这样的工具时稍加细心就可以做到)。但总体效果是很容易看到的。你可以自己做实验来感受一下。例如,在一台32核的测试机器上,在ArrayList上运行max()或sum()这样的小函数,平衡点非常接近10K大小。更大的尺寸可以看到高达20倍的速度提升。小于10K大小的运行时间并不比10K的运行时间少多少,所以往往比顺序执行要慢。最糟糕的减速发生在少于100个元素的情况下–这激活了一堆线程,而这些线程最终却无事可做,因为计算在它们开始之前就已经完成了。另一方面,当每个元素的计算都很耗时的时候,如果使用ArrayList这样的高效且完全可分割的集合,就能立即看到速度提升效果。

另一种说法是,在没有足够的计算量的情况下,使用parallel()可能会花费你大约100微秒的时间,而在合理的情况下使用它应该至少节省这么多的时间(对于非常大的问题可能是几个小时)。确切的成本和收益随时间和平台的变化而变化,而且在不同的情况下也会有所不同。例如,在一个顺序循环中并行运行一个微小的计算,会很大程度的影响程序运行效果。(这样做的微观基准测试可能无法预测实际的使用情况)。

一些问题和回答

  1. 为什么JVM不能自己找出是否使用并行模式?
    它可以尝试,但它会经常给出错误的答案。在过去的三十年里,对完全无指导的自动多核并行化的追求并没有获得一致的成功,所以流框架采用了更安全的方法,即只要求用户做出是/否的决定。这些决定依赖于不太可能完全消失的工程权衡,而且与顺序编程中一直在做的决定相似。例如,当你在一个只容纳一个元素的集合中寻找最大的元素时,你可能会遇到一百倍的开销(减速),而不是直接使用该元素(不在一个集合内)。有时JVM可以为你优化这种开销。但这在顺序的情况下不常发生,而在并行的情况下从不发生。另一方面,我们确实期待工具的发展能够帮助用户做出更好的决定。

  2. 如果我对参数(F、N、Q、S)的了解太少,无法做出一个好的决定怎么办?
    这也类似于常见的顺序编程问题。例如调用Collection方法S.contains(x),如果S是HashSet,通常会很快,如果是LinkedList,则会很慢,否则就在两者之间。通常,处理这个问题的最好方法是,使用集合的组件的作者不要直接导出它,而是导出基于它的操作。这样用户就不会受到这些决定的影响。这同样适用于并行操作。例如,一个有内部集合 “价格 “的组件可以使用一个大小阈值来定义一个方法,除非每个元素的计算很昂贵。比如说:

1
2
3
4
5
6
public long getMaxPrice() { return priceStream().max(); }

private Stream priceStream() {
return (price.size() < MIN_PAR) ?
prices.stream() : prices.parallelStream()。
}

​ 你可以用各种方式扩展这个想法,以处理关于何时和如何使用并行的各种考虑。

  1. 如果我的函数可能会做IO或同步,怎么办?
    一个极端是那些不能通过独立标准的函数,包括内在的顺序性IO,对锁定的同步资源的访问,以及一个执行IO的并行子任务的失败对其他子任务产生副作用的情况。将这些并行化不会有太大意义。在另一个极端,是执行偶尔的瞬时IO或同步的计算,很少有块(例如大多数形式的日志和大多数并发集合的使用,如ConcurrentHashMap)。这些都是无害的。介于两者之间的情况需要最多判断。如果每个子任务都可能在相当长的时间内被阻塞,等待IO或访问,那么CPU资源可能会被闲置,而程序或JVM没有办法使用它们。每个人都不会乐意见到这种情况。在这种情况下,并行流通常不是一个好的选择,但是有很好的替代方案,例如async-IO和CompletableFuture设计。

  2. 如果我的源是基于IO的呢?
    目前,基于JDK IO的流源(例如BufferedReader.lines())主要用于顺序使用,在元素到达时逐一处理。支持高效地批量处理缓冲IO的机会是存在的,但目前这需要定制开发流源、Spliterators和/或Collectors。一些常见的形式可能会在未来的JDK版本中得到支持。

  3. 如果我的程序在一台繁忙的计算机上运行,而且所有的内核都被使用了,怎么办?
    机器一般只有一套固定的内核,当你执行并行操作时,不可能神奇地创造更多的内核。然而,只要明确满足选择并行执行的标准,通常就没有任何理由担心。你的并行任务将与其他任务竞争CPU时间,所以你会看到较少的速度提升。在大多数情况下,这仍然比其他方法更有效。底层机制的设计是这样的:如果没有其他核心可用,那么与顺序性能相比,你只会看到一个小的减速,除非系统已经过载,以至于它把所有的时间都花在上下文切换上而不是做任何真正的工作,或者被调整为假设所有的处理都是顺序的。如果你在这样的系统上,管理员可能已经禁用了多线程/cores的使用,作为JVM配置的一部分。如果你是这样一个系统的管理员,你可以考虑这样做。

  4. 所有的操作都是在并行模式下并行的吗?
    是的,至少在某种程度上是这样的,尽管Stream框架在选择如何做到这一点时服从于源和方法的约束。一般来说,较少的约束条件可以实现更多的潜在并行化。另一方面,不能保证框架会提取并应用所有可能的并行化机会。在某些情况下,如果你有时间和专业知识,你可能能够手工制作一个明显更好的并行解决方案。

  5. 我将得到多少并行速度的提升?
    如果你遵循这些准则,通常是值得的。可预测性不是现代硬件和系统的强项,所以通常的答案是不可能的。缓存定位、垃圾收集率、JIT编译、内存争用、数据布局、操作系统调度策略以及管理程序的存在,都是可以产生重大影响的因素。这些因素在顺序性能中也起作用,但在并行环境中往往被放大。一个在顺序执行中造成10%差异的问题可能会在并行中造成10倍的差异。

    流框架包括一些设施,可以帮助你提高加速的机会。例如,对IntStream这样的基元使用特殊化,在并行中的效果往往比顺序的大,因为它不仅减少了开销(和足迹),还增强了缓存的定位性。而使用ConcurrentHashMap而不是HashMap作为并行 “收集 “操作的目标,可以减少内部开销。随着人们对该框架的经验积累,会有更多的提示和指导出现。

  6. 这一切都太可怕了! 我们是不是应该制定一个政策,使用JVM属性来禁用并行?
    我们不想告诉你该怎么做。为程序员引入新的出错方式可能是很可怕的。编码、设计和判断方面的错误肯定会发生。但有些人几十年来一直在语言,启用应用程序级的并行性会导致重大灾难,但至今这些灾难并没有发生。


由 Doug Lea 书写, 感谢 Brian Goetz, Paul Sandoz, Aleksey Shipilev, Heinz Kabutz, Joe Bowbeer等人的帮助

e

  1. RPC 远程过程调用:机器无关
  2. SPI - Service Provider Interface:单机解耦
  3. Http远程请求

**本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看 活动链接 **

首先需要注意的是,使用freemaker生成的文档,其本质仍然为xml文档。

使用freemaker生成word文档主要实现步骤如下:

添加依赖(第1步) -> 制作模板(第2,3,4,5,6步) -> 程序编写(第7,8,9,10步)

详细步骤:

  1. 下载导入freemaker-xx.jar包,或者 maven:pom.xml中添加freemaker依赖

  2. 修改已有的”文档.doc”:将填充内容修改为

  3. 文档另存为“xxx_template.xml”,建议复制备份一份“xxx_template-副本.xml”

  4. 使用文本编辑器,notepad++/sublineText等打开xml文件,使用插件美化xml(notepadd++美化路径为:插件->XML Tools -> Pretty print),将其中的{{parames}}替换为 ${paranames},有些{{`, `}} 分开了记得合并或删除

  5. 对于需要list, 格式为

    1
    <#list listitems as item> ...${item.para1} ... ${item.para2}... </#list>
  6. 另存为”xxx_template.ftl”

  7. 项目中建立模板文件路径包:….template.package, 你喜欢的包名都可以

  8. 包中建立MDoc.java类:内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    /***
    * MDoc类,根据模板与数据生成文件
    */
    package com.xxxxx.export.doc.template;

    import java.io.BufferedWriter;
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.OutputStreamWriter;
    import java.io.UnsupportedEncodingException;
    import java.io.Writer;
    import java.util.Map;

    import freemarker.template.Configuration;
    import freemarker.template.Template;
    import freemarker.template.TemplateException;

    public class MDoc {
    private Configuration configuration = null;
    public MDoc() {
    configuration = new Configuration(Configuration.getVersion());
    configuration.setDefaultEncoding("utf-8");
    }

    /**
    *
    * @param dataMap 要填入模板的数据
    * @param fileName 生成文档的路径
    * @param template 模板文件的名称,如"xxx.ftl"
    * @throws UnsupportedEncodingException
    */
    public void createDoc(Map<String, Object> dataMap, String fileName, String template) throws UnsupportedEncodingException {
    // dataMap 要填入模本的数据文件
    // 这里我们的模板是放在与本类同一个包路径下
    configuration.setClassForTemplateLoading(MDoc.class, "/" + MDoc.class.getPackage().getName().replace(".", "/"));
    Template t = null;
    try {
    t = configuration.getTemplate(template);
    } catch (IOException e) {
    e.printStackTrace();
    }
    // 输出文档路径及名称
    File outFile = new File(fileName);
    Writer out = null;
    FileOutputStream fos = null;
    try {
    fos = new FileOutputStream(outFile);
    OutputStreamWriter oWriter = new OutputStreamWriter(fos, "UTF-8");
    // 这个地方对流的编码不可或缺,使用main()单独调用时,应该可以,但是如果是web请求导出时导出后word文档就会打不开,并且包XML文件错误。主要是编码格式不正确,无法解析。
    out = new BufferedWriter(oWriter);
    } catch (FileNotFoundException e1) {
    e1.printStackTrace();
    }

    try {
    t.process(dataMap, out);
    out.close();
    fos.close();
    } catch (TemplateException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

  9. 将 “xxx_template.ftl” 拷贝至同一包路径下。

    image.png

  10. 调用方法关键代码如下:

1
2
3
4
5
6
7
8
9
String filepath = "D://..../filename.doc";
MDoc mdoc = new MDoc();
try {
mdoc.createDoc(dataMap, filepath, "xxx_template.ftl");
} catch (UnsupportedEncodingException e) {
LogKit.error("导出word错误:e = " + e.getMessage());
}
File outputfile = new File(filepath);
....

一些其他需求:

  1. 插入图片:

    需要将图片链接转换为base64格式,并放置到到模板中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- ${pic_index}表示取元素在列表中的下标index -->
    ...
    <!-- 资源索引 -->
    <#list picUrls as picUrl>
    <Relationship Id="rId${picUrl_index}Png" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image${memUrl_index}.png"/>
    </#list>
    ...
    <!-- base64资源 -->
    <#list picUrls as picUrl>
    <pkg:part pkg:name="/word/media/image${picUrl_index}.png" pkg:contentType="image/png" pkg:compression="store">
    <pkg:binaryData>${picUrl.imageBase64!''}</pkg:binaryData>
    </pkg:part>
    </#list>
    ...
    <!-- 引用,仅放置关键处代码,具体需要使用模板来制作 -->
    <a:blip r:embed="rId${picUrl_index}Png" cstate="print">
    ...
  2. 换行:

    1
    paraHaveLineBreak.replaceAll("/n", "<w:br/>")

1. 循环删除元素

1
2
3
4
5
6
7
8
9
List list = ....
Iterator<T> it = list.iterator();
while(it.hasNext()) {
T element = it.next()
if(shouldBeDeletedObject(element)) {
// 删除元素
it.remove()
}
}

2. parallelStream代替foreach

parallelStream,并行处理流。

When to use parallel streams 何时应该使用parallelStream,概述为:当操作是独立的并且计算量大或者应用于高效可拆分数据结构的许多元素时,或者考虑同时使用这两种方法时,请考虑使用S.parallelStream().operation(F)代替S.stream().operation(F)

更详细地:

  • F,每个元素的函数(通常为lambda)是独立的:每个元素的计算均不依赖于或不影响任何其他元素的计算。(有关使用无状态无干扰功能的更多指导,请参阅 流包摘要。)
  • S,源集合可以有效地拆分。除了Collections之外,还有其他一些易于并行化的流源,例如java.util.SplittableRandom(可以使用该stream.parallel()方法来并行化)。但是大多数基于IO的源主要是为顺序使用而设计的。
  • 执行顺序版本的总时间超过了最小阈值。大多数平台的阈值大约为100微秒(十分之一)。不过,您不需要精确地测量它。在实践中,您可以通过将N(元素数)乘以QF的每个元素的成本),然后将Q表示为操作数或代码行数,然后检查N * Q在至少10000。因此,当 F是类似的微小函数时x -> x + 1,则需要N> = 10000并行执行的元素是值得的。相反,当F是一项庞大的计算,例如在国际象棋中找到最佳下一个动作时, Q因子是如此之高,以至于N无关紧要,只要集合可以完全拆分即可。

所以当每个任务相互之间互不影响,且耗时较大时(大于100微秒),则可以考虑使用parrallelStream来优化。

TODO 当parallelStream有一个任务发生异常会怎样?

3. 调用第三方接口必须统一封装,必须获取所有可能的异常,最后的catch必须是 catch(Exception e) {…}

1
2
3
4
5
6
7
8
9
10
11
12
13
public Object getInfoFromOtherAPI() {
try {
//call API
} catch (SpecialException e) {
//TODO
}
...
catch (Exception e) {
//最后的catch必须捕获所有可能的异常
return null;
}

}

写作小技巧:文章写好之后,过一周再发布,并且这一周尽量少看这篇文章;一周后如果还觉得写得不错,就发出来。

题目来源

《编程之美》的第一道题目,原文中给出了C语言的解法、相关函数接口与工具。思考如何使用Go来实现这一目标?

写一个程序,让用户来决定Windows任务管理器的CPU占用率。可以实现下面三种情况:

  1. CPU的占用率固定在50%,为一条直线;
  2. CPU的占用率为条直线,但是具体占用率由命令行参数觉得(1~100);
  3. CPU的占用率状态是一个正弦曲线
  4. 如果你的电脑是双核的,那么你的程序会有什么样的结果为什么?

解决思路:

思考:如何使CPU的占用率为50%呢?
我们认为大体上有两种思路:

  1. CPU一直运行,但速率/频率是全速运行的50%。
  2. CPU按时间来划分,50%的时间全速运行,50%的时间不运行。
为什么放弃思路1:调节CPU频率

​ (1) CPU的频率是与CPU的时钟周期有关,需要获取更多的硬件支持,而非软件角度

​ (2) 不一定具有普适性,比如调节Intel CPU与其他品牌的CPU频率方法不同,不同系列CPU支持也不同,指令集也不同。

​ (3) 控制CPU频率需要程序较高的执行权限,比如root权限

​ (4) 当然并不能说这种思路完全不可行,Linux有 cpufrequtils 支持设置CPU的运行模式,其中有一种模式“userspace” , 即用户自定义模式,供用户应用程序调节CPU运行频率,对这一思路感兴趣的同学可以尝试一下。

​ (5) 最主要的原因是,当我们限制CPU频率为全速的50%时,CPU利用率会显示多少呢?也许此时,50%的频率又变成了CPU利用率的分母。

CPU利用率的计算公式:

RealTimeCPULoad=1-(RTCPUPerformance/CPUPerformanceBase)*100%。

但如同不应该拿筷子喝粥一样,人不应该陷入一种思路而不自拔。

思路2:控制运行与休眠时间比例

如果在一定时间范围内,控制程序运行与休眠时间的比例,也就控制了CPU的利用率。

思路2的方式是可行的,且在短时间内是可以通过代码实现的。

比如我们在1秒内,让程序执行500毫秒,休眠500毫秒,则可以认为CPU的利用率为50%。

实现思路2,我们需要理清以下问题:

  1. CPU利用率的统计周期是多久?是否有函数或方法可获取到CPU的周期数?
  2. 如何让CPU “忙” 起来?
  3. 如何让CPU “闲” 起来?
  4. 多核心的CPU利用率统计规则?
  5. 如何表达正弦曲线的X轴:时间?

CPU利用率的统计周期

观察任务管理器,大致可推断出统计周期为1秒。实际在程序中需要通过不同的参数值来进行测试。

让CPU “忙” 起来

根据《编程之美》的叙述,当程序执行运算,做一些复杂操作,死循环等能够让CPU占用率上升。应该采用哪种方式呢?

根据解决思路,让CPU忙起来的时间段内,需要让CPU短暂利用率为100%。因此我们需要考察与测试,哪种方式能够让CPU的占用率达到100%。
感兴趣的同学可在稍后的程序中测试以下几种运算是否有差别:
(1) 计算MD5值
(2) 执行+1操作
(3) 执行空循环

让CPU “闲” 下来

类似C语言中的Sleep函数,Go中也有time.Sleep(time.Duration)函数,形参time.Duration表示休眠的时间段。

多核心的CPU利用率统计规则

假如CPU有两个核心,一个核心运行状态为100%利用率,另一个核心为空闲状态,CPU的利用率会使什么情况呢?

首先提出假设:Windows任务处理器的CPU占用率为多个核心的总占用率,即:N个核的CPU的占用率公式为:

CPU占用率 = (Z_1 + Z_2 + ... + Z_N) / N

其中,Z_i表示第i个核的利用率。

在后续程序中,可通过配置不同的协程goroutine数目来测试如何达到理想目标。参考设置的值有1,2(内核数),4(逻辑处理器数量)。

如何表达X轴:时间

e61190ef76c6a7ef00b9298bf7faaf51f2de6684.jpg

如图,正弦曲线如图所示。

正弦函数sin(X)的取值范围为区间[-1, 1],CPU利用率区间为[0, 100%],做一个sin(X) -> CPU利用率的映射,很容易得出:
CPU利用率 = (sin(x) + 1) / 2, 其中x为程序运行时间
任务管理器展示CPU占用率时间窗口为60秒,正弦函数一个周期为区间[0, 2π],做 [0, 60] -> [0, 2π] 的映射;即随着时间从060秒变化,x的变化为02π;即通过设置x的每秒步长为2π/60 ≈ 0.1,则60秒的窗口可绘制一个完整的正弦曲线。
即程序运行x秒时,程序的利用率 y = ( sin(0.1x) + 1 )/ 2。

以下代码实现了CPU利用率呈现正弦函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

package main

import (
"fmt"
"math"
"time"
)

func main() {
// 通过设置不同的coreNum测试多核心CPU利用率
coreNum := 4
for i := 0; i < coreNum; i++ {
go task(i)
}
// 通过等待输入实现main-goroutine不会终止
a := ""
fmt.Scan(&a)

}

func task(id int) {
var j float64 = 0.0
// 通过设置step来决定一个60秒的统计窗口展示几个正弦函数周期
var step float64 = 0.1
for j = 0.0; j < 8*2*math.Pi; j += step {
compute(1000.0, math.Sin(j)/2.0+0.5, id)
}
}

/**
* t 一个总的CPU利用率的统计周期,1000毫秒,感兴趣的可以测试一下时间段小于1000毫秒与大于1000毫秒的情况下曲线如何
* percent [0, 1], CPU利用率百分比
*/
func compute(t, percent float64, id int) {
// t 总时间,转换为纳秒
var r int64 = 1000 * 1000
totalNanoTime := t * (float64)(r) // 纳秒
runtime := totalNanoTime * percent // 纳秒
sleeptime := totalNanoTime - runtime // 纳秒
starttime := time.Now().UnixNano() // 当前的纳秒数
d := time.Duration(sleeptime) * time.Nanosecond // 休眠时间
fmt.Println("id:", id, ", totaltime = ", t, ", runtime = ", runtime, ", sleeptime = ", sleeptime, " sleep-duration=", d, ", nano = ", time.Now().UnixNano())
for float64(time.Now().UnixNano())-float64(starttime) < runtime {
// 此处易出错:只能用UnixNano而不能使用Now().Unix()
// 因为Unix()的单位是秒,而整个运行周期
}
time.Sleep(d)
}

留几个问题给小伙伴们思考:

  1. compute函数中为什么要转换为纳秒?

答↑:因为一个周期是1000毫秒,也就是1秒,你不能用秒级单位来执行循环。当然,也可以用Millisecond或者Microsecond,但是不能用秒,而我只找到了两个熟悉的函数,time.Now().UnixNano(),time.Now().Unix(),所以只能选用第一个纳秒,小伙们可以可以尝试一下用秒回有什么现象

  1. 多个逻辑处理器,运行一段时间后,各个协程之间的执行进度不同了,这是为什么?如何保持同步?
    答↑:进度不同,1是可能受其他程序影响,2是各个核心的速度略有差异。可以通过在compute中传递从程序开始运行到当前时刻的时间差,而非传递1000毫秒来实现同步;或者使用其他同步方法

  2. 让部分核心始终“闲”,让部分核心始终“忙”会有什么现象?循环中执行各种不同运算有何种差别?

答↑:自行测试吧

  1. 能否消除其他程序占用CPU的影响呢?

答↑:我不知道
尝试获取cpu利用率:github/shirou/gopsutil/cpu,并在每次进入compute函数时通过在原百分比percent中移除其他程序执行所占百分比,但是后来发现,受限于gopsutil/cpu获取CPU利用率的方法,cpu.Percent(duration time.Duration, percpu bool)获取cpu利用率方法本身就需要1秒,所以你会得到一个十分跳跃的曲线。我暂时没有找到其他方法。欢迎知道的小伙伴在评论去留言指教