目录

背景简述

本人是一个自学一年Java的小菜鸡,理论上跟大多数新手的水平差不多,但我入职的新公司是要求转Clojure语言的。坊间传闻:通常情况下,最好是有一定Java的开发工作经验,再转CLojure可能容易一些。我入职后的实际经历也确实让我感受到了Clojure的自学难度略大于自学Java,遇到的困难主要与中文资料较少有关,具体为:

1 中文的面向新手的较为系统的教程材料较少,目前个人感觉最好用的还是《CLojure编程 Emerick著》这本书,网上应该很好找,如果大家没有电子版的话可以留言,我看到后就立刻分享给大家

2 中文的网上相关问题和讨论较少, 以前学Java的时候基本遇到的问题用百度就能解决,现在大概率要直接用bing或谷歌,或者直接在stackoverflow(虽然是英文的,但貌似是最好用的IT问答网站)上查

我的这个系列笔记主要是基于 0工作经验的后端开发转学Clojure 的场景下完成的,里面有一些个人观点和个人理解的注释,写的时候是为了便于自己理解相关的概念,现在分享出来一方面是希望能帮助像我一样的新手更好地理解,另一方面也是希望有高手能够发现错误并帮忙斧正,谢谢

一些格式的简单约定:

粗体:比较重要的内容

斜体:我个人理解/观点或是补充内容,大家选择性食用

P15:表示书上第15页

第4章 多线程和并发

Clojure为了解决多线程带来的问题,提供的手段有:

  1. 减少程序中可变状态的使用,利用不可变的值、集合,以及他们所提供的值语义和高效的操作

    举例:
(def map-test {:a "aa" :b "bb"})
=> #'helloworldclojure.core/map-test
map-test
=> {:a "aa", :b "bb"}
(assoc map-test :c "cc")
=> {:a "aa", :b "bb", :c "cc"}
map-test
=> {:a "aa", :b "bb"}
  1. 确实需要对状态进行修改,那么对可变的状态进行隔离,并且限制对这些可变值的修改方法
  2. 实在没有选择的情况下,可以用纯的锁、线程以及Java提供的高质量并发API

Clojure没有让并发编程变得非常简单,但是提供了一些新颖且久经考验工具,可以让并发编程更容易,写出来的东西更可靠

能够看出,Clojure的解决方案也是层层递进的关系

4.0 我的问题

Clojure提供了哪些Java中没有的东西

不可变的值、集合及对应的操作

CLojure提供的工具为什么新颖且久经考验工具,可以让并发编程更容易,写出来的东西更可靠

比如无需手动加锁自动确保高并发下数据一致性的ref(ref的alter commute还能分情况解决不同的问题)等

4.1 术语

实体:函数或者宏,可以认为是一些逻辑的封装

4.1.1 一个必须要先确定的思考基础

多线程的基础是一个cpu内的多线程进行计算,而不是分布式的多台服务器多个cpu进行计算,因此不要过分纠结于分布式情况下会出现的情况,本章内容是在单个cpu,多个线程的场景下完成的

4.2 计算在时间和空间内的转换

4.2.1 delay

主要作用及特点:

  1. 是一种让一些代码延迟执行的机制,代码只会在显式地调用deref的时候执行
  2. 对所包含的代码只执行一次,然后会把返回值缓存起来,之后再访问都是直接返回,可以理解为之后再执行的时候是没有副作用的,P160

补充:

  1. deref:解引用,一般使用语法糖@,解引用可以获取到引用的当前值,通常只有在高阶函数或者解引用时指定一个超时、超时特性的时候采用deref
  2. realized? : 检测delay的内容是否已经获取到了,比如(realized? (:content d))

4.2.2 future

主要作用及特点:

  1. 自动在另一个线程里面执行所包含的代码,然后通过解引用获取值,但如果future未完成就解引用,则会阻塞当前线程
  2. 缓存返回值,之后再解引用获得的是储存的返回值
  3. 可以在解引用的时候指定“超时时间”和“超时值”

P163 有和delay的对比案例,简单说就是多个调用者使用delay时,会导致当前线程阻塞(可以简单理解为串行),但是future则会新开一个线程,是通过异步执行的,阻塞时间缩短甚至没有阻塞时间,这个小改变就能大幅提升系统吞吐量

future和手动启动线程执行代码相比具备的优势:

  1. future和agent共享的是一个线程池的线程,因此共享资源就比自己独立创建更高效
  2. 使用future更加简洁
  3. future更容易和相关的Java API交互

4.2.3 promise

主要作用及特点:

  1. 类似于delay和future,可以被解引用,并且解引用的时候可以传入一个超时的参数,如果promise没执行好,那么代码会阻塞,直到值准备好
  2. 创建时不会指定任何代码或者函数来最终产生出它们的值,执行到某个点的时候,可以通过deliver函数填充一个值

详解见P164,简要说明就是一个一次性的单值管道,多个并发进程之间的显式的关系定义

4.3 简单地并行化

agent:一种并发组件,可以很高效地完成并行化计算任务

pmap:并行版的map,内部使用了future

补充:

dorun:彻底实例化惰性序列,P167

4.4 状态和标识

在clojure中,状态和标识有本质区别,即便在其他语言中这两个概念被当做同一个概念处理

状态:任何一个状态是不会发生改变的,状态可能随着时间的不同而不同,但是不同的两个时间下就是两种不同的状态

标识:标识是指表示一个东西的逻辑实体,比如Sarah,而不是她在某个特定时间点的状态,或者说标识在任何时间点都有一个特定的状态,而每个状态的变更都不会对已有的状态产生影响

标识可以理解为就只是一个名字而已,或者说某一时刻的名字而已,但是状态是内在属性的内容,CLojure中让标识的状态是不可变的,任何对标识的访问一定获得的是同样的内容,不同内容是因为已经是另一个标识了,或者说是另一个状态

4.5 Clojure的引用类型

4种引用类型以及它们的语义使得我们可以设计并发程序,并让这个并发程序最大限度地利用已有硬件的最大计算能力,同时能避免使用线程和锁可能会带来的一系列bug

四种引用类型的共通点:

  1. 引用类型都是包含其他值的容器,容器里面的值可以利用某些函数进行修改
  2. 访问值的语法都是通过解引用deref或@,返回的事引用的状态的一个快照,这个快照是不可变的值,但是引用的状态是有可能在之后发生变化的
  3. 解引用是绝对不会造成阻塞的,解引用不会和其他操作相互干扰,这个和delay promise future形成鲜明对比,后者是有可能阻塞的

4.6 并发操作的分类

对并发操作分类的概念,能够帮助我们更清楚地理解每种类型最合适在什么样的场合下使用

4.6.1 协调

一个“协调”的操作是为了产生正确的结果,这个操作里面的多个角色必须相互合作(或者至少不要相互干扰),典型例子就是银行转账。

4.6.2 同步

“同步”是指线程会等待、阻塞或者睡眠,知道它获得对于指定上下文的独占访问,而“异步”操作是指调用线程不用阻塞在一个调用上面,它可以继续去做别的事情

4.6.3 选用引用类型的标准



当为某个问题选择引用类型的时候,对照这个分类即可

协调且异步的组合通常在分布式系统里面更加常见,比如最终一致性的数据库只保证一段时间之后,所有的对于状态的修改会合并到最终状态中去。

4.7 原子类型(Atom)

最基本的引用类型,是实现同步的、无需协调的、原子的“先比较再设值”的修改策略,每一个操作都是自动完全隔离的,没办法协调,也一定是阻塞等待的

函数方法:

swap!(格式:swap! 要修改的原子类型 函数 一些参数)

compare-and-set!

reset!

使用swap! 对原子类型进行修改,通过P175的例子可以看出,swap!的重试是指重新获取新值,进行比较,与旧值匹配之后才会完成修改

使用compare-and-set!是传入一个比较的值,如果修改成功就返回true,否则返回false

使用reset!就是不管现在的值究竟是什么,就是要重新设值

4.8 通知和约束

引用类型除了可以通过deref获取当前值之外,还有两个共同的特性:分别让我们对引用的状态进行监控,以及对新的状态的合法性进行校验,分别以观察器和校验器的形式提供

4.8.1 观察器

观察器是在引用的状态发生改变的时候会被调用的函数,Clojure的观察器要比设计模式的观察者更加通用,因为Clojure的观察器只是一个函数而已,而且通知的机制也是现成的,不需要自己实现。

一般来说,观察者函数使得改变可以即时通知到其他引用或者系统是非常方便的

一个观察器必须接受4个参数:key 发生改变的引用(四个引用类型之一) 引用的旧状态 现在的新状态

add-watch是用来增加一个观察器给引用,remove-watch是用来移除一个观察器的

identify函数直接返回的是传入的参数,P178

4.8.2 校验器

校验器可以以任何想要的方式对引用的状态进行控制,简单理解就是对引用进行状态修改,需要经过校验器的判断,校验器同意才能完成状态修改,校验器必须返回true才能完成修改

是通过:validator关键字指定的,只有atom ref agent类型可以在创建的时候通过:validator直接指定一个校验器,如果要修改校验器,可以使用set-validator!函数

4.9 ref

ref可以保证多个线程可以交互地对这个ref进行操作:

  1. ref会一直保持一个一致的状态,不会出现外界可见的不一致状态
  2. 多个ref之间不可能产生竞争条件
  3. 不需要手动使用锁
  4. 不可能出现死锁

4.9.1 软件事务内存

任何协调多个线程,对共享存储进行修改的方式,都可以称为STM,就像gc之于手动管理内存,STM就是对手动锁管理的简化

通常情况下,使用经过证明的、自动化手段将开发人员从底层细节中解放出来,效果甚至比底层领域专业人员手写的代码效果要好

CLojure的STM使用的就是数据库中的多版本并发控制(MVCC),对一系列ref的每个修改都是具有事务语义的,符合ACI,不符合D单纯是因为STM是纯内存实现的

when-let在本地绑定为空的时候是不会执行的,P183

4.9.2 对ref进行修改的机制

dosync:开启Clojure的STM和事务

alter commute ref-set:多个事务试图对一个或多个ref同时进行有冲突的修改,冲突与否是由这三个对ref进行修改的函数所决定的

4.9.2.1 alter

alter的语义为:当一个事务要提交的时候,ref的全局的值必须和这个事务内第一次调用alter的时候一样,否则事务会被重启,从头再执行一遍



这张图里面的结果就是,虽然t1开始的早,但是t1提交时,a的值已经被t2改变了,所以事务重启,从头重新执行一遍

4.9.2.2 commute

alter可以认为是对ref状态最安全的修改方式,不过没有对修改ref的可重排序进行任何假设,一些情况下,是可以对ref的修改操作进行重排序的,这时可以用commute代替alter

commute没有什么神奇的,单纯只是计算了两次,第一次是事务内正常计算,第二次是提交的时候利用ref的最新全局值重新计算一遍,commute的使用要非常谨慎,最简单的判断方式还是操作重排是否会对结果产生影响

用了重排序,是可以减少潜在的冲突几率和重试次数的,从而大幅提升吞吐量

我个人的理解是,重点关注commute的两次计算特性。第一次计算的时候是会获取一次全局最新值,只是应付一波计算,但第二次计算才是真正获取全局最新值并提交修改,相较于alter每次获取都很较真的对比,commute的第一次获取(事务内计算)并不会很较真,且第二次获取(提交时获取)也不会将全部事务推倒重来,而是只搞定自己的事情,因此使用commute一定是在即便无重试,也不影响结果的情况下才可以

commute的函数需要是可重新排序,而不会影响程序的语义,也就是说commute应该只用在可以对修改ref状态的操作进行重排的场景中

注意:commute和alter的不同

  1. alter的返回值是要更新到ref的全局状态的值,这个事务内的值就是最终提交的值,而commute产生的事务内的值不一定是最终提交的值,因为所有被commute的函数在最终提交的时候会利用ref的最新全局值重新计算一遍
  2. 利用commute对ref进行修改从来不会导致重读,因此不会导致一个事务重试

    关于commute和alter的对比代码,见案例

个人理解:alter里面会有大量的重新计算,这些重新计算会浪费很多时间,而在commute里面,不做任何的重新计算,提交前算出来是多少就是多少,确保了速度,等到最后真正提交的时候再算一次,需要注意的是,关于多线程的考虑一定是基于一个cpu内完成的,所以这一切才能完成

update-in的源码:

4.9.2.3 ref-set

和alter的语义是一样的,事务提交前如果ref的状态发生改变,则事务会被重试,只不过ref-set是直接传入一个要设置的值

4.9.2.3 通过校验器来保证本地的一致性

校验器和STM的交互非常方便,如果校验器发现非法状态,则抛出一个异常,然后导致当前事务失败

4.9.3 STM的一些缺点

4.9.3.1 事务内绝对不能执行有副作用的函数

因为事务有可能重试,如果副作用是数据库写操作,那么会多次重复写,因此可以使用 io!宏,当被用在事务中的时候,就会抛出异常,因此可以把有副作用的函数用io!宏包起来,防止被误用在事务里面

4.9.3.2 最小化每个事务的范围

尽量不要写长事务,因为串行以及重试会造成性能的降低

活锁:相当于STM世界里面的死锁,简单说就是事务要一直重试,导致一直无法结束

STM解决事务一直竞争导致导致的活锁,就是barging机制,即老事务和新事务竞争时,强迫新事务重试,让老事务先走,如果老事务还是不能提交,直接抛异常

4.9.3.3 读线程也可能重试

deref对引用类型来说是保证不会阻塞的,但如果在一个事务内利用deref对一个ref解引用,那么是有可能触发事务重试的,但是STM维护了一个事务中涉及ref的历史值,这样即便其他事务提交了新的值,当前事务仍然可以获取它自己的值(从历史值中获取),但如果历史值无法获取(变更次数超过了将当前线程的历史值干掉了),那么仍然会阻塞线程直到获取到最新值或历史值为止

4.9.3.3 write skew

STM保证了ref状态的事务一致性,如果事务一致性依赖一个ref,而对其只有读取没有修改,那么STM就无法通过alter commute set-ref知道事务一致性依赖ref这件事(因为不修改就不会调用这三个函数),那么如果事务读取的ref在其他事务中被修改,有可能导致当前事务依赖的还是旧值,最终提交的话,整个状态就不一致了,这个情况叫write skew

这个时候用ensure来避免write skew : 对ref进行解引用,但如果当前事务提交之前,ref被修改了,会导致当前事务重试

(ensure a) (alter a identity) (ref-set a @a)这三个的语义相同,都是做了一个“空写”,作用都是保证读出的值在提交之前不会发生修改

4.10 var

var和其他引用类型不同在于,它们的状态不实在时间尺度进行维护的,它们提供的是一个命名空间内全局的实体,可以在每个线程上把这个实体绑定到不同的值

Clojure里面对一个符号进行求值,就是在当前命名空间孕照名字是这个符号的var,并对其解引用以获取他的值,也可以直接引用var,并且手动解引用,#'是(var map)的语法糖,就是直接引用var,然后@是解引用,因此下面两行代码的效果是一样的

map

@#'map

4.10.1 定义var

顶层的函数和值都保存在var中,它们都是利用def或者其引申物定义在当前的命名空间中

私有var:添加名为:private 值为true的元数据即可

私有函数:defn-

文档字符串:就是变量名后面那个可以认为是用以解释的字符串

常量:添加:const关键字

4.10.2 动态作用域

一般情况下,var的作用域在定义它的形式之内,但是var提供“动态作用域”的特性是个例外

添加:dynamic可以利用binding对var在每个线程的值进行覆盖,一般期望通过binding来对根绑定进行覆盖的动态var,会在命名的时候以星号开头,以星号结尾

动态作用域广泛应用,比如提供数据库的配置信息给类库

4.10.3 var不是变量

不要把var和其他语言的变量混淆在一起,def定义的都是顶级var, 本质上是被设计来保存一些直到程序结束都不再改变的值,如果确实希望一种可以改变的东西,那么使用其他引用类型,然后用一个var来保存它

个人理解:var的作用有两个,第一个是与其他引用类型平行的,作用就是保存一个尽量不会变的值,另一个是高于其他引用类型的,即保存其他引用类型

alter-var-root : 对var的根绑定进行修改

4.10.4 前置声明

先定义var,但是不赋值

4.11 agent

agent是一种不协调、异步的引用类型,这意味着对于一个agent的状态的修改与对别的agent的状态的修改是完全独立的,而且发起对agent进行改变的线程跟真正改变agent值的线程不是同一个线程

agent和atom、ref区分开的特点:

  1. 可以安全地利用agent进行I/O以及其他各种副作用的操作
  2. agent是STM感知的,因此它们可以很安全地用在事务重试的场景下

4.11.1 agent action和更新函数send send-off

更新函数及参数整体叫做agent action,更新函数调用只是简单的把action放到一个action队列上,然后在一些专门的线程上串行执行,每个action的结果都是agent的一个新状态

send send-off区别是是否会在一个固定大小的线程池中运行

send:固定大小线程池,不适宜发送阻塞的action,因为会阻止其他cpu密集型action更好地利用cpu资源

send-off:不限制大小的线程池(跟future用的同一个线程池),适宜发送阻塞的、非cpu密集型的action

使用send发送到任何代理的所有操作都在一个线程池中运行,该线程池中的线程数比处理器的物理数量多。这导致它们接近CPU的全部容量。

如果您使用send发出1000个请求,则实际上不会产生太多的切换开销,无法立即处理的呼叫只需等待处理器可用即可。如果它们阻塞了,则线程池可能会耗尽。

使用send-off时,将为每个呼叫创建一个新线程。如果您发送了1000个请求,那些无法立即处理的函数仍会等待下一个可用处理器,但是如果send-off线程池碰巧处于运行效率较低的状态时,它们可能会导致启动线程的额外开销。线程阻塞发生阻塞是可以接受的,因为每个任务(潜在地)都有一个专用线程。

如果需要等待的话,则使用await方法

4.11.1 处理agent action中的错误

因为agent action是异步的,所以action抛出的异常和发送这个action的不是一个线程,对这种情况的默认处理策略是将agent默默地挂掉,我们可以解引用获取最后的状态,但是进一步发送action会失败

尝试给一个挂掉的agent发送action会返回导致这个agent挂掉的异常

agent-error函数可以用来获取异常

restart-agent可以重启一个挂掉的agent,它将agent的状态重置为我们提供的值,并且使它能够继续接收action

:clear-actions标志位可以将agent队列上面阻塞的action全部清除掉

4.11.1.1 agent的错误处理器以及模式

agent支持两种失败模式,是通过:error-mode确定的:

:fail(默认值):一个错误会导致agent进入失败状态

:continue:执行时如果抛出异常,这个异常会被直接忽略掉继续执行队列中其他action,并且也能继续接收新的action,这使得我们没必要调用restart-agent,但是忽略掉错误而不做任何事情并不合适,因此会制定一个错误处理器

错误处理器::continue模式下使用,这是一个接收两个参数的函数,分别是发生错误的agent以及这个异常对象,通过指定:error-handler来指定,处理器中可以进行很多其他的操作,P214

4.11.2 I/O、事务以及嵌套的Send

agent可以非常安全地被利用来协调I/O或者其他类型的阻塞操作,而且由于agent的简单语义,使得它们可以称为简化涉及异步I/O操作的理想组件

4.11.2.1 利用agent来并行化工作量

将发送agent的方式分成两种,即阻塞性和非阻塞性的action,因为这样可以利用它们最有效地使用不同资源的能力,比如cpu、io、网络等

4.12 使用Java并发原语

这一部分就和Java里面的使用基本类似了,实际开发中用到的也不是很多,不再赘述

上一篇:《Clojure编程》笔记 第3章 集合类与数据结构

下一篇:《Clojure编程》笔记 第5章 宏

《Clojure编程》笔记 第4章 多线程和并发的更多相关文章

  1. java并发编程笔记(九)——多线程并发最佳实践

    java并发编程笔记(九)--多线程并发最佳实践 使用本地变量 使用不可变类 最小化锁的作用域范围 使用线程池Executor,而不是直接new Thread执行 宁可使用同步也不要使用线程的wait ...

  2. [书籍翻译] 《JavaScript并发编程》第六章 实用的并发

    本文是我翻译<JavaScript Concurrency>书籍的第六章 实用的并发,该书主要以Promises.Generator.Web workers等技术来讲解JavaScript ...

  3. 《python核心编程》读书笔记--第18章 多线程编程

    18.1引言 在多线程(multithreaded,MT)出现之前,电脑程序的运行由一个执行序列组成.多线程对某些任务来说是最理想的.这些任务有以下特点:它们本质上就是异步的,需要多个并发事务,各个事 ...

  4. C#高级编程笔记之第二章:核心C#

    变量的初始化和作用域 C#的预定义数据类型 流控制 枚举 名称空间 预处理命令 C#编程的推荐规则和约定 变量的初始化和作用域 初始化 C#有两个方法可以一确保变量在使用前进行了初始化: 变量是字段, ...

  5. java 面向对象编程--第十四章 多线程编程

    1.  多任务处理有两种类型:基于进程和基于线程. 2.  进程是指一种“自包容”的运行程序,由操作系统直接管理,直接运行,有自己的地址空间,每个进程一开启都会消耗内存. 3.  线程是进程内部单一的 ...

  6. C#高级编程笔记之第一章:.NET体系结构

    1.1 C#与.NET的关系 C#不能孤立地使用,必须与.NET Framework一起使用一起考虑. (1)C#的体系结构和方法论反映了.NET基础方法论. (2)多数情况下,C#的特定语言功能取决 ...

  7. Python核心编程笔记 第三章

    3.1     语句和语法    3.1.1   注释( # )   3.1.2   继续( \ )         一般使用换行分隔,也就是说一行一个语句.一行过长的语句可以使用反斜杠( \ ) 分 ...

  8. 编写高质量代码:改善Java程序的151个建议(第8章:多线程和并发___建议126~128)

    建议126:适时选择不同的线程池来实现 Java的线程池实现从根本上来说只有两个:ThreadPoolExecutor类和ScheduledThreadPoolExecutor类,这两个类还是父子关系 ...

  9. 《Clojure编程》笔记 第5章 宏

    目录 背景简述 第5章 宏 5.0 术语 5.1 宏到底是什么 5.1.1 宏不是什么 5.1.2 有什么是宏能做而函数不能做的 5.1.3 宏vsRuby的eval 5.2 编写你的第一个宏 5.3 ...

随机推荐

  1. LightningChart运行互动示例介绍

    LightningChart.NET完全由GPU加速,并且性能经过优化,可用于实时显示海量数据-超过10亿个数据点. LightningChart包括广泛的2D,高级3D,Polar,Smith,3D ...

  2. p.array 的shape (2,)与(2,1)的分别是什么意思

    numpy.ndarray.shap是返回一个数组维度的元组. (2,)与(2,1)的区别如下:   ndarray.shape:数组的维度.为一个表示数组在每个维度上大小的整数元组.例如二维数组中, ...

  3. pytorch和tensorflow的爱恨情仇之张量

    pytorch和tensorflow的爱恨情仇之基本数据类型:https://www.cnblogs.com/xiximayou/p/13759451.html pytorch版本:1.6.0 ten ...

  4. CentOS openssh升级到openssh-7.2版本

    查看现在的版本SSH -V 一.准备 备份ssh目录(重要) cp -rf /etc/ssh /etc/ssh.bak [ 可以现场处理的,不用设置 安装telnet,避免ssh升级出现问题,导致无法 ...

  5. A4988两相四线步进电机驱动模块使用经验

    1.A4988模块可以驱动两相四线步进电机,模块引脚及接线图如下: 2.步进电机引线如下: 3.引脚: ENABLE:低电平有效,用于打开和关闭场效应管的输出: RESET:低电平有效,芯片复位: S ...

  6. CRUD,分页,排序,搜索与AngularJS在MVC

    下载source - 53.1 MB 介绍 在选择最新的技术时,有几个因素会起作用,包括这些技术将如何与我们的项目集成.这篇文章解决了开始使用AngularJS和MVC的乞丐的问题.这篇文章告诉使用语 ...

  7. ConcurrentHashMap原理分析(一)-综述

    概述 ConcurrentHashMap,一个线程安全的高性能集合,存储结构和HashMap一样,都是采用数组进行分桶,之后再每个桶中挂一个链表,当链表长度大于8的时候转为红黑树,其实现线程安全的基本 ...

  8. Java 合并Word文档

    合并文档可以是将两个包含一定逻辑关系的文档合并成一个完整的文档,也可以是出于方便文档存储.管理的目的合并多个文档为一个文档.下面,就将以上文档操作需求,通过Java程序来实现Word文档合并.合并文档 ...

  9. 多测师讲解html _段落标签002_高级讲师肖sir

    <html> <head> <meta charset="UTF-8"> <title>段落标签</title> < ...

  10. day01 Pyhton学习

    一.python介绍 python是一种解释型.弱类型的高级编程语言. 编译型:是把源程序的每一条语言编译成机器语言,并保存成二进制文件,给计算机执行,运算速度快. 优点:程序执行效率高,可以脱离语言 ...