Golang中的slice详解及常见坑总结

Slice在golang中属于常见的数据结构,本文将从各个方面对其进行详细的讲解,文末给出一些在开发过程中容易遇到的坑。

1. 常见的操作

  • 切片
1
2
3
4
5
6
7
8
9
s := []int{1,2,3,4,5,6}
l := s[2:5]
fmt.Println("sl1:", l)
l = s[:5]
fmt.Println("sl2:", l)
l = s[2:]
fmt.Println("sl3:", l)
l = s[:2:3]
fmt.Println("sl3:", l)
  • append

根据append的官方定义,该方法的作用是用于将元素追加到slice的末尾。如果slice有足够的容量,那么将直接追加, 返回新的slice。如果没有足够的容量,将重新分配一片用于存放数组的内存,返回新的slice。所以官方建议的做法是将返回的slice重新赋值给原来的变量。

1
2
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)

可以通过如下的程序来验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
s := []int{1}
fmt.Printf("addr: %p, cap: %d, sliceAddr: %p\n", &s[0], cap(s), &s)
s = append(s, 2)
fmt.Printf("addr: %p, cap: %d, sliceAddr: %p \n", &s[0], cap(s), &s)
s = append(s, 3)
fmt.Printf("addr: %p, cap: %d, sliceAddr: %p\n", &s[0], cap(s), &s)
x := append(s, 4)
fmt.Printf("Saddr: %p, Xaddr: %p, cap: %d, lenS: %d, sliceAddr: %p\n", &s[0], &x[0], cap(s), len(s), &x)
y := append(s, 5)
fmt.Printf("Saddr: %p, Yaddr: %p, cap: %d, lenS: %d, sliceAddr: %p\n", &s[0], &y[0], cap(s), len(s), &y)
}

运行得到的结果如下:

1
2
3
4
5
addr: 0xc0420401d0, cap: 1, sliceAddr: 0xc042046400
addr: 0xc042040200, cap: 2, sliceAddr: 0xc042046400
addr: 0xc042046460, cap: 4, sliceAddr: 0xc042046400
Saddr: 0xc042046460, Xaddr: 0xc042046460, cap: 4, lenS: 3, sliceAddr: 0xc042046480
Saddr: 0xc042046460, Yaddr: 0xc042046460, cap: 4, lenS: 3, sliceAddr: 0xc0420464a0

通过运行结果可以得到如下结论:

  1. 创建初始切片时,容量为1
  2. 追加2之后,因为超出了原有数组的容量,所以扩容为原来的2倍,容量变为2,分配一个新的数组,可以看到地址已经变了。
  3. 追加3之后,因为超出了原有数组的容量,所以扩容为原来的2倍,容量变为4,分配一个新的数组,可以看到地址已经变了。
  4. 追加4之后,因为容量足够,所以直接追加到原有数组的末尾,不重新分配数组,地址不变。
  5. 追加5之后,因为容量足够,所以直接追加到原有数组的末尾,不重新分配数组,地址不变。

这里比较难理解的是4和5,append永远返回新的slice,并不会改变原有切片的值。4,5两步中,s的长度始终为3。
另外的一个比较困惑的点是sliceAddr的值,变量s所指向的slice的地址始终没有发生变化。实际上&s指向的是slice这个结构体的地址。

2. Slice内部实现

slice是对一个数组片段的描述,它包含一个指向数组的指针,该数组的长度以及容量。

1
2
3
4
5
type Slice struct {
ptr *Elem
len int
cap int
}

对数组或者slice进行切片操作并不会进行复制操作,他会创建一个指向原始数组的新的slice,这就使得操作slice像操作数组下标一样有效率。所以修改slice中的元素,或者对slice进行重新切片,会修改原始切片的值。

扩充slice时不能够超过其容量,如果超过了,将会引发一个panic。要增加slice的容量,必须创建一个新的slice,并将其值复回原来的slice

1
2
3
4
5
t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0
for i := range s {
t[i] = s[i]
}
s = t

当然也可以使用copy函数简化操作。

1
2
3
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

参考文献

  1. Go Slices: usage and internals
  2. golang的append()为什么不会影响slice的地址?
  3. Arrays, slices (and strings): The mechanics of ‘append’

【翻译】PostgreSQL anti-patterns: read-modify-write cycles

如果你对基于sql的应用编程不是很熟悉的话,那么我强烈建议你先去读这一篇文章。

它让我想起了另一个sql编程的反模式: read-modify-write cycles。在本篇文章中,我将要解释这个常见的开发错误是什么,怎么识别以及怎么修复。

想象一下你现在需要查找一个用户的账户,如果账户余额减掉100还不是负数的话就将其保存。

下面是一种很常见的写法:

1
2
3
4
SELECT balance FROM accounts WHERE user_id = 1;
-- in the application, subtract 100 from balance if it's above
-- 100; and, where ? is the new balance:
UPDATE accounts SET balance = ? WHERE user_id =1;

对于开发者来讲代码似乎运行的很好。然而,这段代码错的很离谱,只要不同的session操作同时操作同一个用户的账户就会出现故障。

想象一下两个并发的session,每一个都从用户的账户余额中减掉100,从初始余额300开始。

Session 1 Session 2
SELECT balance FROM accounts
WHERE user_id = 1; (returns 300)
SELECT balance FROM accounts WHERE
user_id = 1; (also returns 300)
UPDATE balance SET balance = 200
WHERE user_id = 1; (300 – 100 = 200)
UPDATE balance SET balance = 200 WHERE
user_id = 1; (300 – 100 = 200)

现在余额变成了200,但是你从有300余额的账户中扣掉了200,所以有100消失了。
大部分的测试和开发是在单独的session和单独的服务器上开发的,所以除非你做非常严格的测试,否则这样的错误知道生产环境上线之前是不会被发现的,而且也非常的难调试。知道这些对你进行防御型编程是非常重要的。

事务不能阻止么?

我经常在stackoverflow上面看到有人问”难道事务不能阻止吗?”。不幸的是,事务虽然很伟大,但是并不是你进行简单的并发编程的装饰。唯一能够让你忽视并发问题的操作就是在事务开始之前锁住每一个你将要操作的表(你甚至需要以同样的顺序加锁以避免死锁的发生)。

加了事务之后,执行的过程和上面是一样的:

Session1 Session 2
BEGIN; BEGIN;
SELECT balance FROM accounts WHERE user_id = 1; (returns 300)
SELECT balance FROM ac counts WHERE user_id = 1; (also returns 300)
UPDATE balance SET balance = 200 WHERE user_id = 1; (300 – 100 = 200)
UPDATE balance SET balance = 200 WHERE user_id = 1; (300 – 100 = 200)
COMMIT; COMMIT;

解决方案

幸运的是PostgreSQL有很多的工具还可以帮助你,还有一些应用程序层面的解决方案。
一些比较流行的解决方案如下:

  • 使用calculated update来避免read-modify-write
  • 使用行级锁select ... for update
  • 使用序列化的事务
  • 乐观并发控制,也就是使用乐观锁

避免read-modify-write

最好的方式就是使用sql来解决,彻底解决read-modify-write-cycle。

Session1 Session 2
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; (sets balance=200)
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; (sets balance=100)

上面的代码在并发的事务中也是可以工作的,因为第一个获取行锁之后,第二个就开始等待,知道第一个完成提交或者回滚。 transaction isolation文档的READ COMMITED部分有详细的解释。

这个解决方案只在不是很复杂的情形下有效,当在复杂的业务逻辑下,比如需要基于当前的账户余额来判断是否需要执行。这个操作在可用的时候是最简单和快速的。

需要注意的是非默认的SERIALIZABLE隔离也会阻止该错误,但是处理方式不同。让我们来看一下关于SERIALIZABLE的讨论。

行级锁

修复已经存在缺陷的应用的最简单的方式就是加上行级锁。

使用SELECT balance FROM accounts WHERE user_id = 1 FOR UPDATE来代替SELECT balance FROM accounts WHERE user_id = 1。这使用了行级锁。所有其它的试图更新该行锁或者使用select .. for update 或者 select ... for share的操作都将等待前一个事务提交或者回滚。

上面的例子中,在PostgreSQL的默认事务隔离级别中,第二条select语句直到第一条语句执行update并且commit之后才会返回。然后第一个事务将会继续,但是select将会返回200而不是300,所以会产生正确的数据。

Session1 Session 2
BEGIN; BEGIN;
SELECT balance FROM accounts WHERE user_id = 1 FOR UPDATE; (returns 300)
SELECT balance FROM accounts WHERE user_id = 1 FOR UPDATE; (gets stuck and waits for transaction 1)
UPDATE balance SET balance = 200 WHERE user_id = 1; (300 – 100 = 200)
COMMIT;
(second transaction’s SELECT returns 200)
UPDATE balance SET balance = 100 WHERE user_id = 1; (200 – 100 = 100)
COMMIT

PostgreSQL官方文档中关于explicit lock的部分有详细的解释。

需要注意的是上面的方法只有在read-modify-write在一个事务中才有用,因为锁只存在于事务的生命周期中。

SERIALIZABLE事务

如果read-modify-write总是存在于单独的事务中,并且你使用的PostgreSQL版本大于9.1,那么你可以使用SERIALIZABLE事务来代替显式的select ... for update

在这种情况下,所有的selectupdate语句将会正常的执行。第一个事务将会COMMIT,然后当你COMMIT第二个事务的时候,将会退出并且抛出一个serialization error。你将需要从头开始执行失败的事务。

Session1 Session 2
BEGIN ISOLATION LEVEL SERIALIZABLE; BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE user_id = 1; (returns 300)
SELECT balance FROM accounts WHERE user_id = 1; (also returns 300)
UPDATE accounts SET balance = 200 WHERE user_id = 1; (300 – 100 = 200)
UPDATE accounts SET balance = 200 WHERE user_id = 1; (gets stuck on session1’s lock and doesn’t proceed)
COMMIT – succeeds, setting balance=200
(UPDATE continues, but sees that the row has been changed and aborts with a could not serialize access due to concurrent update error)
COMMIT converted into forced ROLLBACK, leaving balance unchanged

SERIALIZABLE隔离级别在一个大的事务退出或者有冲突的时候将会强制应用重复很多工作。这对于在尝试使用行级锁可能会造成死锁的复杂情况下,将会很有用。

PostgreSQL官方文档中关于并发控制和事务隔离级别的描述是关于SERIALIZABLE隔离级别的最好资源。如果你还不习惯于思考并发问题,那么你将需要多读几遍,并且尝试一下文档中给出的例子。

乐观并发控制

乐观并发控制通常是在应用端来实现的处理并发的功能,比如hibernate一类的ORM工具。

在这个模式下,所有的表都会一个版本号或者最近更新的事件戳,并且所有的update语句都有一个额外的where语句来保证自该行被读之后就没有被更新。应用程序将会检查是否有被update影响到的行,如果没有,将会被视为一个错误并且放弃事务。

在这个demo中,需要添加一个新的列:

1
ALTER TABLE accounts ADD COLUMN version integer NOT NULL DEFAULT 1;

然后这个例子就变成了:

Session1 Session 2
BEGIN; BEGIN;
SELECT balance, version FRO M accounts WHERE user_id = 1; (returns 1, 300)
SELECT version, bala nce FROM accounts WHERE user_id = 1; (also returns 1, 300)
COMMIT; COMMIT;
BEGIN; BEGIN;
UPDATE accounts SET balance = 200, version = 2 WHERE user_id = 1 AND version = 1; (300 – 100 = 200. Succeeds, reporting 1 row changed.)
UPDATE accounts SET balance = 200, version = 2 WHERE user_id = 1 AND ve rsion = 1; (300 – 100 = 200). Blocks on session 1’s lock
COMMIT;
(UPDATE returns, matching zero rows because it sees version=2 in the WHERE clause)
ROLLBACK; because of error detected

因为实现这个功能非常的繁琐,所以乐观并发控制通常是通过ORM或者查询构造器来使用。
和SERIALIZABLE隔离级别不同的是,它在自动提交模式或者语句在不同的事务中也是有效的。基于这个原因,这通常在给用户很长思考时间的web应用或者用户在交易中途就退出。因为它不需要可能造成性能问题的长时事务。

如果你使用出发起来出发乐观并发控制规则为,乐观并发控制可以和传统的基于锁的实现协同使用。否则,它通常和其它的显示并发控制方式单独使用。

该选哪一个?

最合适的选择取决于你在干什么,你的确定的需求,书写retry循环来处理失败的事务的难度等。

没有一个适用于所有人的答案,如果有,那么将会只有一种方案,而不是多种。

但是有一个一定是错误的方式,那就是忽略并发问题,并且期待数据库能够正确的处理。

参考文献

Event Sourcing explained - part 1

事件源模式使用追加记录的方式来记录发生在一个领域中针对数据的操作,而不是仅仅只存储当前的状态,所以能够物化领域对象。通过这种方式能够简化一些需要同步数据模型和业务领域的复杂领域,能够提高性能,可扩展性以及响应性,保证事务型数据的一致性,对需要提供补偿操作的动作提供完整的记录。

大部分的应用都是需要和数据进行交互的,通常的做法是当用户操作数据时维护数据的当前的状态。例如在传统的CRUD模型中,一个典型的数据操作流程是从存储中读取数据,修改数据,存储修改后的状态-通常使用事务来锁住数据。

但是CRUD模型有一定的限制:

  • CRUD系统中执行更新操作时可能会影响性能和响应性,由于操作需要的额外的开销也限制了可扩展性。
  • 在一个需要和众多的并发用户交互的领域模型中,由于数据的更新发生在单一的数据记录上,因此更容易发生冲突。
  • 除非有额外的审计机制来记录每一个操作,否则历史数据将会丢失。

使用事件源机制的最重要的好处就是它内建了审计机制来保证交易数据和审计数据的一致性,因为二者是同样的数据。通过事件来展示数据使得能够及时的在任何时间重建任何对象的状态。

下面是使用事件源机制带来的额外的好处。

  • 性能。因为事件是不可变的,所以当你保存事件的时候可以使用追加的方式。事件同时也是简单的、单一的对象。相比较使用复杂的关系模型,所有的这些因素能够促成更好的性能和可扩展性。
  • 简单。事件是描述系统中发生了什么的简单对象。通过只存储事件能够避免存储福大领域对象到关系型数据库中所带来的复杂性。
  • 审计跟踪。事件是不可变的,并且存储了系统中状态的所有的历史记录,所以能够系统中发生了什么的完整的详细的记录。
  • 和其它的子系统集成。事件提供了同其它子系统进行交互的方便的方式。事件能够向其它对系统状态变迁感兴趣的子系统发布事件, 同时记录这些发事件。
  • 从事件历史中得到额外的业务数据。通过存储事件,你就可以通过查询相应的事件来得到系统在这之前的任意一个时刻的状态。如果你存储了事件,你将不会丢弃将来可能会有用的数据。
  • 生产环境故障诊断。 你可以通过拷贝一份生产系统中的事件记录并且在测试环境中重放来诊断生产环境中的故障。如果你知道生产环境中产生故障的时间,那么你将能够通过事件重放很容易的观察到系统中究竟发生了什么。
  • 修复错误。 你可能发现造成计算数据不准确的编码错误。你可以通过修复代码并且重放事件流而不是通过人工冒险修复已经存在的数据。
  • 测试。聚合中的所有的状态的变迁都会被存储为事件,因此可以很容易的测试聚合中的一条命令是否符合期望。

事件溯源并不是一个顶层的架构,它应该被应用于正确的地方,例如交易系统等。如果一整个系统都应用事件溯源,就变成了反模式。

参考文献

React中的props和state的比较

Props和state是相关的,一个组件的state经常可以变成子组件的props。props可以通过父组件的render方法的第二个参数传递给子组件,如果使用jsx,则可以通过这种方式。<MyChild name={this.state.childsName} />。父亲组件的state的值变成了自组件的this.props.name,从子组件的角度来看,name prop是不可改变的,如果它需要被改变,父组件必须只改变内部的state。this.setState({ childsName: 'New name' });,然后react会自动的将改变后的值传递给子组件,如果子组件需要改变他自己的name prop呢?这通常是通过子组件的事件和父组件的回调来实现的。子组件向外暴露一个时间,那么父组件就可以通过传递一个回调的handler来订阅这个事件。

1
<MyChild name={this.state.childsName} onNameChanged={this.handleName} />

子组件将新的名字作为一个参数传递给事件回调函数,然后父组件会在事件处理器中使用这个值来更新state。

1
2
3
handleName: function(newName) {
this.setState({ childsName: newName });
}

参考文献

Postgres's database VS schema

一个PostgreSQL数据库集簇中包含一个或更多命名的数据库。用户和用户组被整个集簇共享,但没有其他数据在数据库之间共享。
任何给定客户端连接只能访问在连接中指定的数据库中的数据。一个集簇的用户并不必拥有访问集簇中每一个数据库的权限。
用户名的共享意味着不可能在同一个集簇中出现重名的不同用户,例如两个数据库中都有叫joe的用户。但系统可以被配置为只允许joe访问某些数据库。

实际上可以认为一个模式包含一组表,而一个数据库包含一组模式。

为什么需要模式

和数据库不同,模式并不是被严格的隔离,如果用户被赋予足够的权限,他就能够访问数据中所有模式内的对象。

首先,模式允许多个用户使用一个数据库并且不会被干扰。其次,模式可以将数据库对象组织成逻辑组以便更容易管理。
最后,第三方应用的对象可以放在独立的模式中,这样它们就不会与其他对象的名称发生冲突。

一个例子,你有一个web应用来收集用户的信息并且需要将这些信息存储到数据库中,这些数据被称作交易数据。
还有一些数据使用来做展示用的,这类数据被称作配置性数据。所以我们需要创建两个模式一个用来存储交易数据,
一个用来存储配置数据,通过这种配置开发者就无法获取交易数据,从而保证数据的安全。

如果需要创建一个SAAS的应用,那么可以针对每一个用户创建一个模式。但是在用户数过多(>50)的情况下,可能会造成性能问题。

参考文献

Statement completion values

最近Paul Irish在Twitter上提了一个JavaScript的小问题,觉得很有趣,所以写篇文章来记录一下。

在浏览器的console中运行如下的JavaScript片段:

1
'omg'; var x = 4;

结果返回omg,看到这种情况,最先想到的就是变量提升,所以写了下面的代码:

1
var x; 'omg'; x = 4;

结果返回4,这就让人很迷惑了。于是请教了下公司的前端,得到的答案是 var x = 4被提升了,但是我并不认同。
因为变量提升仅仅是声明语句提前了,但是变量初始化仍然在原来的位置。var x = 4实际上包含两条语句, var x; x = 4,
其中var x;是声明语句,首先JavaScript引擎会找到所有的声明,并用合适的作用域使之关联起来,这一阶段是在编译时完成的,初始化或者赋值语句
将在运行时执行,所以x = 4不会被提升。直到代码运行到其所在的位置时才会被赋值。所以写了下面的代码加以验证:

1
console.log(y, y === undefined); 'omg'; var y = 4;

结果输出undefined true,返回omg,事实证明只有var y被提升了。
最后Javascript的创建者Brendan Eich回答了这个问题:

It’s not a return value, rather a statement completion value.

但是什么是statement completion value,和return value又有什么区别呢?

statement completion value

直观的看, statement completion value是执行一段代码块所得到的值,例如:

1
2
3
4
5
6
>Math.random()
<0.853512441173391
>a = 5
<5
>var b
<undefined

实际上,目前捕获statement completion value的唯一方式是使用eval:

1
2
3
4
5
6
>eval("Math.random()")
<0.03151934138190282
>eval("a = 5")
<5
>eval("var b")
<undefined

需要指出的是并不是所有的语句都有completion value, 例如for(var x = 42;false;);

那么JavaScript又是如何处理statement lists的呢?ECMA规范有下面一段话:

The value of a StatementList is the value of the last value producing item in the StatementList.

依据上面的规范,下面的语句列表都会返回 1:

1
2
3
eval("1;;;;;")
eval("1;{}")
eval("1;var a;")

依此可以得到结论:一个statement lists总是返回最后一个非空语句的值。所以就能够很好的解释本文开头提出的问题了。
omg;completion valueomg, var x = 4completion valueundefined,他们组合到一起就返回omg

ES7已经有提案要支持statement completion value的捕获了,具体的语法是:

1
2
3
var x = do {
...
}

参考文献

C语言static揭秘

在C语言中,static是一个保留字,它被用来控制生命周期和可见性,它可以被分为static全局变量,static局部变量和static函数。

static 全局变量

static对于全局变量来讲会自动加上,例如

1
2
3
4
5
int a;
static int b;
void main() {

}

以上a和b有同一个storage class。

static 局部变量

在局部变量中,static修饰的变量被存储静态分配的内存中,而静态分配的内存通常在编译时就被保存在数据段,动态分配的内存通常被保存在一个临时的栈中。static局部变量在编译时就被初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

void test() {
int a = 1;
static int b = 1;
a += 1;
b += 1;
printf("a = %d, b = %d\n", a, b);
}

void main() {
for (int i = 0; i < 5; i++)
{
test();
}
}

输出

1
2
3
4
5
a = 2, b = 2
a = 2, b = 3
a = 2, b = 4
a = 2, b = 5
a = 2, b = 6

static局部变量的初始值为0,并且在整个生命周期内只会被初始化一次。

static 函数

static应用与函数的作用是控制改函数的可见性,一旦被static修饰,就不能被外部文件引用。

总结

  • static全局变量: 被static关键字修饰的全局变量只在当前文件下有效。
  • static局部变量: 被static关键字修饰的局部变量是静态分配的,它再程序的整个生命周期中保持自己的内存地址,也就是说每次调用都会保持static修饰的局部变量的值。
  • static函数: 被static修饰的函数只在当前文件下有效,对其它的文件不可见。

Ruby 进程fork初探

元旦放假期间,又捡起CSAPP看了下,主要是看异常处理这一段,感觉之前看的东西忘了不少,所以翻译一段关于Ruby的进程Fork的文章加深理解。(主要基于Forking Ruby Processes or How to fork Ruby,但是不完全翻译,某些地方进行了扩展)

Forking是一个进程复制的UNIX术语,意味这从父进程复制出一个子进程出来,他们有相同的地址空间,相同的本地变量值,相同的堆,相同的全局变量值以及相同的代码。
在很多情况下,他们共享内存,直到其中的一个进程对内存进行了修改,这被称作CoW(Copy On Write)。因为子进程和父进程都是独立的进程,所以他们都有自己独立的私有地址空间,他们对其中的变量所做的更改都是独立的,不会反应到另一个进程的存储器中。

简单例子

我们知道了UNIX术语的含义,那么我们如何在Ruby中实现呢?如何使用Ruby来fork一个进程呢?

1
2
3
puts "This is the first line before the fork (pid #{Process.pid})"
puts fork
puts "This is the second line after the fork (pid #{Process.pid})"

output:

1
2
3
4
5
This the first line before the fork (pid 2284)
2285
This is the second line after the fork (pid 2284)

This is the second line after the fork (pid 2285)

让我们来看看发生了什么,首先第一行的输出是非常清晰的,那么第二行又是什么鬼?这是fork函数返回的PID值,当然是子进程的PID。接下来让我们来看一下接下来的两行:一行是父进程执行的结果,另一行是子进程执行的结果。

Blocks

开发者想执行一个独立的进程通常的做法是传递一个代码块给fork函数,就像下面这样:

1
2
3
4
5
puts "You can also put forked code in a block pid: #{Process.pid}"
fork do
puts "Hello from fork pid: #{Process.pid}"
end
puts "The parent process just skips over it: #{Process.pid}"

上面的代码输出三行结果,在我的机子上的运行结果是这样的:

1
2
3
you can also put forked code in a block pid: 3465
The parent process just skips over it: 3465
Hello from fork pid: 3466

注意代码块内的PID和外面的PID是不一样的,因为代码块内的代码是通过fork函数派生出来的进程执行的。

多核CPU测试

让我们来快速的验证一下fork在多核环境下带来的一个主要的好处。让我们写一个ruby程序来呈现这一优势。

1
2
3
4
5
6
7
8
9
10
def cpu_intensive_process
puts "Pid: #{Process.pid}"
x = 0
10000000.times do |i|
x = i + x
end
end

fork
cpu_intensive_process

当上面的代码运行时,将会充分的利用两个CPU核,如果机器有多余两个CPU核心,那么只有两个CPU得到充分的利用。然后,额外的fork调用将会创建更多的进程,知道占满你的CPU核心数。

内存测试

那么内存分配的情况又是怎么样的呢?很明显ruby1.9.3并没有CoW的特性,也就是会所每一个进程的内存分配将会是一样的。下面的程序能够一定程度上说明这一点。

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
 hash = Hash.new #load up the memory a little

1000000.times do |i|
hash[i] = "foo"
end

puts "Hash contains #{hash.keys.count} keys"

def show_memory_usage(whoami)
pid = Process.pid

mem = `pmap #{pid}`

puts "Memory usage for #{whoami} pid: #{pid} is: #{mem.lines.to_a.last}"

sleep #keep the process alive
end

puts "Now lets fork this process and see what memory is allocated to the child"

puts "Before..."

if fork
show_memory_usage("parent")
else

puts "After..."

1000000.times do |i| #change the values in the child memory allocation
hash[i] = "bar"
end

show_memory_usage("child")
end

下面是这个测试的输出:

1
2
3
4
5
6
7

Hash contains 1000000 keys
Now lets fork this process and see what memory is allocated to the child
Before…
After…
Memory usage for parent pid: 10291 is: total 62592K
Memory usage for child pid: 10293 is: total 73164K

孤儿进程

最后让我们来测试一下孤儿进程并且看一下它们的表现,运行以下小程序看一下:

1
2
3
4
5
6
7
fork do
5.times do
sleep 1
puts "I'm an orphan!"
end
end
abort "Parent process died..."

运行的结果像下面这样:

1
2
3
4
5
6
Parent process died...
😈 : ~/work I'm an orphan!
I'm an orphan!
I'm an orphan!
I'm an orphan!
I'm an orphan!

这里又发生了什么?父进程运行完毕所以在终端输出。但是我被5个子进程(孤儿进程)打断了,因为他们顺序的隔一段时间就执行代码!

参考文献

go init()解析

每一个go文件都可以定义自己的init()函数来设置自己的状态,实际上一个go文件可以有多个init()方法,
这些init()方法按照定义的先后顺序依次执行。init()方法的调用在所有被导入的包中的变量被初始化之后再开始被调用,在main()方法之前。

下面用一段代码来进行说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

var Name = getName()

func getName() string {
return "jack"
}

func init() {
Name += "Nie"
}

func main() {
fmt.Println("Hello", Name)
}

运行以上程序,将会输出Hello Jack Nie
首先初始化Name变量,然后执行init()函数,最后执行main()函数。

参考文献

DNS预读取

用户访问一个网页时,经常会遇到有很多域名需要解析的情况,而解析这些域名是需要时间的。
根据网络情况的好坏耗费的时间从数ms到数s不等。DNS预读取技术的出现就是为了解决这种问题的,
它使得用户再点击一个链接之前就能在服务端将域名解析出来。这项技术利用了计算机的常规DNS解析机制,
不需要额外的连接。对于需要加载的图片很多或者页面组件很多的网站,DNS预读取技术能够显著的减少页面加载的
时间。

浏览器端预读取控制

通常情况下,针对DNS预读取不需要进行任何动作,但是有时候用户需要关闭DNS预读取,那么可以进行如下操作;

Firefox

1
network.dns.disablePrefetch = true

默认情况下,启用了https的网站文档中嵌入的链接是不会进行DNS预读取的,如果想要对其进行预读取,则需要进行下面的操作:

1
network.dns.disablePrefetchFromHTTPS = false

服务端预读取控制

服务端可以通过设置x-dns-prefetch-control头来控制预读取的开关,也可以在页面中嵌入如下代码:

1
<meta http-equiv="x-dns-prefetch-control" content="off">

还可以通过link的rel属性来精确的控制某一特定域名的DNS预读取:

1
<link rel="dns-prefetch" href="http://www.example.com">

参考文献