动态语言已死?

静态类型复兴

动态语言已死?

Published: 19 Dec 2014 Category: 其它

真相总会不期而遇。它们总是不经意间降临,譬如当我读到这条微博的时候:

这是关于Facebook的Flow的一个很不错的讨论 – http://t.co/5KTKakDB0w — David J. Pearce (@whileydave) November 23, 2014

David是Whiley编程语言的作者,这门语言内建了许多静态类型检查的特性,它比较小众,但粉丝还不少。它的一个很有意的特性就是流敏感(flow sensitive)类型(有时也被称为流类型),当它与联合(union)类型配合使用的时候会比较有用。下面是从它的使用向导中摘录的一个例子:

function indexOf(string str, char c) => null|int:
 
function split(string str, char c) => [string]:
  var idx = indexOf(str,c)
 
  // idx has type null|int
  if idx is int:
 
    // idx now has type int
    string below = str[0..idx]
    string above = str[idx..]
    return [below,above]
 
  else:
    // idx now has type null
    return [str] // no occurrence
   

记住了,像Ceylon这样的语言也支持流敏感类型,甚至是Java在也在一定程度上也是支持的,因为Java也有联合类型!

try {
    ...
}
catch (SQLException | IOException e) {
    if (e instanceof SQLException)
        doSomething((SQLException) e);
    else
        doSomethingElse((IOException) e);
}

Java的流敏感类型是显式且拖沓的。我们当然希望编译器能推导出所有的类型。像下面这么写的话也会进行类型检查并且能够通过编译就好了:

try {
    ...
}
catch (SQLException | IOException e) {
    if (e instanceof SQLException)
        // e is guaranteed to be of type SQLException
        doSomething(e);
    else
        // e is guaranteed to be of type IOException
        doSomethingElse(e);
}

流类型或者说流敏感类型指的是编译器可以从当前程序的控制流中推导出唯一可能的类型。它是在像Ceylon这样的现代语言中才出现一个相对较新的概念,它使得静态类型变得异常强大,尤其是当语言本身能支持通过var或者val关键字来进行复杂的类型推导的时候。

配备了Flow之后的静态类型的JavaScript

我们回到David的那条微博并看一下这篇文章对Flow是如何评价的:

http://sitr.us/2014/11/21/flow-is-the-javascript-type-checker-i-have-been-waiting-for.html

由于length的取值可能为空,因此Flow就不得不在函数体内判断它是否为空值。下面是进行了类型检查的版本:
function length(x) {
  if (x) {
    return x.length;
  } else {
    return 0;
  }
}
 
var total = length('Hello') + length(null);

Flow能够推导出在if体内x不能为空。

这相当巧妙。微软的TypeScript中也有一个类似的新特性。但Flow与TypeScript不同(至少它是这么声称的)。从官方的Flow介绍中可以看到Facebook Flow的本质所在:

Flow的类型检查是可选的——你不再需要在代码中到处进行类型检查了。然而,Flow的底层设计是基于这么一个假设的,即大多数JavaScript的代码其实都是静态类型的;尽管很多时候代码中并没有出现明确的类型,但在开发人员的脑海中它是实际存在的。Flow会尽可能地自动推导出这些类型,这意味着你无需修改代码便能找出里面的类型错误。换句话说,一些严重依赖于反射的JavaScript代码,尤其是框架的代码,通常很难进行静态检查。对于这种骨子里就是动态类型的代码,类型检查就变得不太准确了,因此Flow提供了一种简单的方式来显式地将这些代码置为是可信的,并忽略它们。这个设计在Facebook海量的JavaScript代码库中得到了验证:许多代码都默认归到了静态类型的分类,开发人员无需显式标注这些代码的类型便能找出其中类型错误的问题。

个中三昧

绝大多数JavaScript代码都是隐式的静态类型的

再进一步

JavaScript代码都是隐式的静态类型的

没错!

类型系统深受程序员的喜爱。他们喜欢正式地声明数据的类型,使得这些数据处于一个比较窄的约束下,这样才能确保程序的正确性。这正是静态类型的本质所在:一个设计良好的数据类型更不容易出错。

人们也喜欢将自己的数据结构以一种规范的形式存储到数据库中,这就是为什么SQL如此大行其道的原因,而无schema的数据库的市场份额始终上不去。这其实本质上都是一样的。在无schema的数据库中,其实你的脑子里还是存在一个schema,只是没有进行类型检查而已,并增加了确保程序正确性的负担。

还有一点需要注意的:一些NoSQL的厂商拼了命地在发表这些荒谬不堪的言论,告诉你,其实你根本就不需要schema,其实只是为了给自己的产品找定位,不过这种营销的伎俩很容易识破。无schema与动态类型的真正需求其实,都相当的少。换言之,你上一次在Java程序中通过反射来调用方法是什么时候的事了?很少用到吧。

不过有一样东西是以前静态类型语言所不具备而动态类型语言能做到的:避免代码冗长。这是因为虽然程序员钟爱类型系统以及类型检查,但他们并不喜欢去敲这些代码。

代码冗长才是问题所在,而非静态类型

来看一下Java进化的历史吧:

Java 4
List list = new ArrayList();
list.add("abc");
list.add("xyz");
 
// 为什么我需要这个Iterator呢?
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    // 好吧,我清楚我这里就是String类型,为啥还要类型转换?
    String value = (String) iterator.next();
 
    // [...]
}
Java 5
// 我居然得声明两次泛型!
List<String> list = new ArrayList<String>();
list.add("abc");
list.add("xyz");
 
// 比之前好多了,不过还是得声明是String类型
for (String value : list) {
    // [...]
}
Java 7
// 进步了点
List<String> list = new ArrayList<>();
list.add("abc");
list.add("xyz");
 
for (String value : list) {
    // [...]
}

    // [...]
}
Java 8
// 终于进化成了这样,虽然姗姗来迟了
Stream.of("abc", "xyz").forEach(value -> {
    // [...]
});

顺便提一下,当然了,上述这个功能其实用Arrays.asList()就能完成了。

Java 8还远谈不上完美,但至少是日臻完美了。现在在lambda参数列表中不用声明类型的原因是编译器替我们完成了推导,这点是相当重要的。

看一下Java 8之前类似于这个lambda的话要怎么写(假设那会儿已经有Stream了):

// Yes, it's a Consumer, fine. And yes it takes Strings
Stream.of("abc", "xyz").forEach(new Consumer<String>(){
    // And yes, the method is called accept (who cares)
    // And yes, it takes Strings (I already say so!?)
    @Override
    public void accept(String value) {
        // [...]
    }
});

现在我们拿Java 8跟JavaScript的版本作一个比较:

["abc", "xyz"].forEach(function(value) {
    // [...]
});

在简洁性方面它几乎已经达到和函数式,动态类型的JavaScript一样的水准了,唯一的不同之处就在于,我们(以及编译器)知道这个value的类型是String。还有就是我们知道forEach方法的确存在。我们还知道的是forEach方法接受一个带单个参数的函数。

最后貌似可以得出如下的结论:

像JavaScript与PHP这样的动态类型语言之所以流行的原因是因为它们"能跑起来"。你不需要学习经典静态类型语言里面所有复杂的语法(想想Ada和PL/SQL吧!)。你马上就可以开始写代码了。程序员知道哪些变量包含字符串,因此没有必要写下来。这没错,的确是没有必要什么都写出来。

看一下Scala(或者C#,Ceylon,以及几乎任意的现代编程语言):

val value = "abc"

它除了能是字符串,还能是什么吗?

val list = List("abc", "xyz")

这个除了List[Stirng],还能是别的类型吗?

不过请注意了,如果你需要显式声明变量类型的话,也是可以的——总会有那么些个边缘的用例:

val list : List[String] = List[String]("abc", "xyz")

不过绝大部分的语法都是“可选”的了,它们能由编译器来完成推导。

动态类型语言已死

讲的所有这些的结论就是,一旦语法的冗长及阻碍从静态类型语言中刨掉之后,那么使用动态类型语言就完全没有任何优势了。编译器已经相当快了,部署也很快速。只要使用了合适的工具,静态类型检查所带来的好处是巨大的。(不相信?请读下这篇文章)。

举个例子,SQL也是一门静态类型语言,它的使用障碍主要是语法造成的。没错,很多人都认为它是一门动态类型语言,因为他们是通过JDBC来访问SQL的,比方通过一些无类型的SQL语句的字符串拼接而成。如果你写过PL/SQL,Transact-SQL或者jOOQ写过嵌入式SQL的话,你绝对不会认为SQL是动态类型的,你马上就会感谢PL/SQL,Transact-SQL,以及你的Java编译器给你的SQL语句所做的类型检查了。

那么,让我们摒弃这个由我们一手创造出来的东西吧,因为我们实在是太懒了,不想在代码里声明这些类型。让敲代码变得更愉快吧!

如果你是Java语言的专家组成员,并碰巧看到了这篇文章,请你一定要把var和val关键字,以及流敏感类型添加到Java语言中。我保证一定会爱死你的!

原创文章转载请注明出处:动态语言已死?

英文原文链接

<think>我们正在讨论Go语言的GMP模型。根据引用[1][2][3][4],GMP模型是Go语言运行时(runtime)中的核心机制,用于高效地实现高并发协程(goroutine)调度执行。下面我将详细解释GMP模型的原理和工作机制。 ## 一、GMP模型的基本概念 GMP模型由三个核心组件构成: 1. **G (Goroutine)**:用户级协程,轻量级线程[^4] - 每个G包含函数指针、栈、状态等信息 - 初始栈大小约2KB(可动态扩展) 2. **M (Machine)**:操作系统线程(OS thread)[^4] - 由操作系统调度,与CPU核心绑定 - 真正执行计算的载体 3. **P (Processor)**:逻辑处理器[^4] - 管理本地G队列(LRQ)和运行状态 - 充当G和M之间的调度中介 三者关系可表示为: $$ \text{G} \xrightarrow{\text{绑定}} \text{P} \xrightarrow{\text{关联}} \text{M} $$ ## 二、GMP调度器数据结构 ### 1. G的结构(runtime.g) ```go type g struct { stack stack // 协程栈 sched gobuf // 调度上下文(PC/SP等寄存器值) atomicstatus uint32 // 状态(_Grunnable, _Grunning等) // ...其他字段 } ``` 状态迁移图: ```mermaid stateDiagram-v2 [*] --> _Gidle: 新建 _Gidle --> _Grunnable: 初始化完成 _Grunnable --> _Grunning: 被调度执行 _Grunning --> _Grunnable: 时间片用完 _Grunning --> _Gwaiting: 进入阻塞(如channel操作) _Gwaiting --> _Grunnable: 事件唤醒 _Grunning --> _Gdead: 执行结束 _Gdead --> [*] ``` ### 2. P的结构(runtime.p) ```go type p struct { status int32 // _Pidle, _Prunning等 runqhead uint32 // 本地运行队列头 runqtail uint32 // 本地运行队列尾 runq [256]guintptr // 本地G队列(固定大小循环队列) // ...其他字段 } ``` ### 3. M的结构(runtime.m) ```go type m struct { curg *g // 当前执行的G p puintptr // 关联的P spinning bool // 自旋状态(寻找可运行G) // ...其他字段 } ``` ## 三、GMP调度机制 ### 核心调度流程(go func() 过程) ```mermaid sequenceDiagram participant U as 用户代码 participant R as Runtime participant P as Processor participant M as Machine U ->> R: 执行 go func() R ->> R: 创建新Goroutine(G) alt P的本地队列未满 R ->> P: 将G加入本地运行队列(LRQ) else 本地队列已满 R ->> R: 将G加入全局队列(GRQ) end R ->> M: 唤醒或创建M(若需要) M ->> P: 获取P(通过关联绑定) loop 调度循环 M ->> P: 从LRQ获取G alt LRQ为空 P ->> R: 执行work stealing R ->> P: 从其他P偷取G end M ->> M: 执行G alt G阻塞(如channel操作) M ->> R: 解绑P并挂起G R ->> M: 将P转给其他M end end ``` ### 关键调度策略 1. **Work Stealing机制**(负载均衡)[^4] - 当P的本地队列为空时,尝试: 1. 从全局队列获取一批G(最多$ \frac{len(GRQ)}{GLOBAL\_Q\_BATCH} $) 2. 随机选择其他P,窃取其本地队列中50%的G 2. **Hand Off机制**(防止阻塞)[^4] - 当G阻塞时(如系统调用): 1. M释放绑定的P 2. P转移到空闲M或创建新M 3. 阻塞的G被唤醒后重新进入队列 3. **抢占式调度**(防止饿)[^1] - 监控线程(sysmon)检测运行超过10ms的G - 通过信号(SIGURG)强制中断当前G - 将G放回队列并重新调度 ## 四、资源管理策略 ### 1. P的数量控制 - 默认等于CPU核心数(可通过`GOMAXPROCS`调整)[^4] - 上限:`runtime.GOMAXPROCS(0)`(通常≤256) ### 2. M的数量动态调整 - 初始数量≈CPU核心数 - 上限:10000(可通过`SetMaxThreads`设置) - 创建规则: - 当有可运行G但无空闲P和M时创建新M - 当M空闲超过10分钟被销毁 ### 3. G的队列管理 - **本地队列(LRQ)**:每个P维护一个256容量的无锁队列 - **全局队列(GRQ)**:所有P共享,使用互斥锁保护 ## 五、性能优化设计 1. **无锁化设计**: - 本地队列使用无锁环形缓冲区(CAS操作) - 减少全局队列竞争[^3] 2. **批量操作**: - 从全局队列获取G时批量转移($ \frac{len(GRQ)}{n\_procs + 1} $)[^4] - 减少锁竞争频率 3. **自旋M优化**: - 空闲M先自旋(约10μs)寻找G - 避免频繁线程切换[^2] ## 六、GMP模型与并发编程 ```go // 典型并发模式示例 func worker(id int, jobs <-chan int) { for j := range jobs { fmt.Printf("worker %d processing job %d\n", id, j) } } func main() { jobs := make(chan int, 100) // 启动3个worker(对应3个G) for w := 1; w <= 3; w++ { go worker(w, jobs) } // 发送任务(GMP自动调度到不同P) for j := 1; j <= 9; j++ { jobs <- j } close(jobs) } ``` 在此示例中: 1. 每个`go worker()`创建一个G 2. 根据`GOMAXPROCS`值创建P 3. M由调度器动态管理,绑定P执行G > **核心价值**:GMP模型通过解耦G/M/P,在$O(1)$调度复杂度下实现: > - 协程动态扩缩容 > - 负载自动均衡 > - 阻塞操作无感切换 > 最终达成高并发与高性能的统一[^1][^2]。 --- ### 相关问题 1. 如何通过`GODEBUG`工具观察GMP调度行为?[^4] 2. 为什么说GMP模型比传统线程池更高效?[^2] 3. 系统调用(如文件IO)如何影响GMP调度?[^1] 4. Go的垃圾回收机制如何与GMP调度器协作?[^4] 5. 什么情况下会导致Goroutine泄漏?如何检测?[^3]</think>## Go语言GMP模型详解 GMP模型是Go语言并发调度的核心机制,通过三层抽象实现高效的协程调度。下面从原理、工作机制和关键设计进行详细分析: ### 一、GMP模型核心组件 | 组件 | 说明 | 特性 | |------|------|------| | **G (Goroutine)** | 用户级协程 | 轻量级(初始2KB栈),动态扩展[^4] | | **M (Machine)** | 操作系统线程 | 1:1映射内核线程,真实执行单元[^1] | | **P (Processor)** | 逻辑处理器 | 管理本地G队列,调度中介[^3] | 三者关系:$ \text{G} \xrightarrow{\text{绑定}} \text{P} \xrightarrow{\text{关联}} \text{M} $ ### 二、调度器工作机制 #### 1. 协程创建与执行流程 ```mermaid sequenceDiagram participant U as 用户代码 participant R as Runtime participant P as Processor participant M as Machine U ->> R: 执行 go func() R ->> R: 创建新Goroutine(G) alt P的本地队列未满 R ->> P: 将G加入本地运行队列(LRQ) else 本地队列已满 R ->> R: 将G加入全局队列(GRQ) end R ->> M: 唤醒或创建M(若需要) M ->> P: 获取P(通过关联绑定) loop 调度循环 M ->> P: 从LRQ获取G alt LRQ为空 P ->> R: 执行work stealing R ->> P: 从其他P偷取G end M ->> M: 执行G alt G阻塞(如channel操作) M ->> R: 解绑P并挂起G R ->> M: 将P转给其他M end end ``` #### 2. 关键调度策略 1. **Work Stealing(工作窃取)** - 当P的本地队列为空时,随机选择其他P窃取其50%的G[^4] - 公式:$ \text{窃取数量} = \min(\frac{\text{目标P队列长度}}{2}, \text{剩余可窃取数}) $ 2. **Hand Off(移交机制)** - G阻塞时,M释放P并将其移交给空闲M或新建M[^4] - 避免线程因系统调用而闲置 3. **抢占式调度** - 监控线程(sysmon)检测运行超过10ms的G - 通过信号(SIGURG)强制中断当前G[^1] ### 三、数据结构实现 #### 1. P的本地队列管理 ```go type p struct { runqhead uint32 // 队列头指针 runqtail uint32 // 队列尾指针 runq [256]guintptr // 固定大小循环队列 runnext guintptr // 高优先级插队位置 } ``` - **无锁设计**:通过CAS操作实现并发安全[^3] - **优先级调度**:新创建的G优先放入`runnext` #### 2. 全局队列访问优化 - 减少全局锁竞争:P从全局队列获取G时批量转移($ \frac{\text{全局队列长度}}{n\_procs + 1} $)[^4] - 本地队列溢出时,将本地队列50%的G转移到全局队列 ### 四、资源管理策略 | 资源类型 | 创建规则 | 数量限制 | |----------|----------|----------| | **P (Processor)** | 启动时创建 | `GOMAXPROCS`值(默认CPU核心数)[^4] | | **M (Machine)** | 需要时动态创建 | 上限10000(可配置) | | **G (Goroutine)** | 按需创建 | 理论上无限(受内存限制) | ### 五、性能优化设计 1. **自旋线程优化** - 空闲M先自旋(约10μs)寻找G,避免立即休眠[^2] - 自旋条件:有空闲P且全局队列有G 2. **系统调用优化** - 网络轮询器(netpoller)将IO阻塞转为异步 - 文件IO通过专用线程池处理 3. **内存分配优化** - 每个P维护本地内存缓存(mcache) - 减少全局内存分配器的锁竞争 ### 六、GMP模型价值 $$ \text{高并发} = \frac{\text{轻量级协程}}{\text{线程切换成本}} \times \text{智能调度} $$ 1. **高吞吐**:百万级Goroutine支持 2. **低延迟**:调度延迟控制在微秒级 3. **资源高效**:CPU利用率可达90%+[^2] > **设计哲学**:通过用户态调度、本地化资源管理和智能负载均衡,在开发者效率与系统性能间取得平衡[^2]。 --- ### 相关问题 1. 如何通过`GODEBUG`工具观察GMP调度行为?[^4] 2. 为什么说GMP模型比传统线程池更高效?[^2] 3. 系统调用(如文件IO)如何影响GMP调度?[^1] 4. Go的垃圾回收机制如何与GMP调度器协作?[^4] 5. 什么情况下会导致Goroutine泄漏?如何检测?[^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值