RSE(Remote Script Executor):一个基于SSH的远程脚本执行器

RSE是本人的第一个开源项目。

立项理由:1)集中编写维护、自动分发同步运维过程中用到的各个脚本;2)集中管理各个脚本产生的运行时信息(包括错误信息、日志等);3)提供对现有程序无侵入式的管理方式。

RSE实现很简单,目前代码行数在700行左右,本人利用元旦放假时间完成。以Apache License 2.0协议发布,代码可在https://github.com/xuchaoqian/rse查看。目前的实现要求:控制机安装好PHP5.3+,集群内各机器(包括自己)安装好BASH,且控制机和各机器均已做好ssh信任关系。

设计简图如下:

如图所示,所有的脚本都被集中管理起来了,已经不需要先手动分发到各远程机器再来执行了。现在只需在控制机上编写,存放到RSE指定的目录(可配置),然后调用RSE执行就可以了。

透明性是RSE在设计和实现时重点考虑的因素,对用户来说,脚本的编写、修改是在控制机,脚本的执行也好像是在控制机。RSE会自动进行脚本分发、脚本同步(如果脚本过期了或者被意外删除了)、脚本日志记录(所有的脚本执行日志都在控制机可查)。而且,RSE把分发过去的脚本放到远程机器上的特定的目录里(默认为/tmp/rse),不会侵入远程机器上的应用程序。

命令帮助:

NAME
rse – Remote Script Executor.
SYNOPSIS
rse [host:]script[ arg1 arg2 ... argn]
DESCRIPTION
rse connects and logs into the specified host and executes the runnable script with the args. If host is not given it’s connected to the localhost. If args is not given it’s assigned empty array.

命令使用示例:

Shell
1
2
3
# The content of echo.sh: echo "$@";
# The response: a"${b}'"\ c\"d 2012
rse echo.sh $'a"${b}\'\"\\' 'c\"d' 2012

有兴趣的朋友可以直接用ssh执行一个远程命令来打印上述参数,看麻不麻烦?呵呵,用RSE就不需要自己关心转义的事了。

另外,考虑到在脚本编程中,调用其它脚本(更宽泛地说,是依赖其它的资源)是很正常的事,所以,RSE增加了对#include指令的支持,现在只需要在脚本中include所依赖的文件就可以了。

API编程示例:

PHP
1
2
3
4
5
6
7
try {
$executor = new executor();
echo $executor->run($host, $script, $args);
} catch (exception $e) {
echo $e->format_stack_trace();
exit(-1);
}

详细的,就不再赘述了,感兴趣的朋友可以自己翻代码看。

—The end.

Posted in Opensource, Programming | Tagged , , | 53 Comments

谈谈C++的智能指针与自动资源管理(包括自动垃圾回收)

为什么要使用智能指针?简单来说,就是希望能对程序中的资源(不仅仅是内存,还可以是文件描述符、连接、锁等)进行自动化的管理。智能指针实际上是“C++栈对像(不考虑寄存器对象)的自动析构特性(由编译器保证在作用域的任何退出点都调用析构函数)以及C++对操作符重载、转换函数、成员函数模板、友元等特性的支持”的产物。

本文将从4个方面来阐述观点:1)传统C风格资源管理方法的问题;2)支持解引用和成员访问表达式、布尔表达式、多态语境(也要考虑和标准库容器对象交互的问题)等内建指针特性的智能指针的实现原理;3)智能指针的空间和时间代价;4)引入智能指针后,如何设计系统的API(暂时空缺)。

先来看看C风格的程序是如何管理资源的:

例子1:

C++
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
#include <cstdio>
#include <cstdlib>
int test_malloc_free() {
int rc = 0;
int *pi = NULL;
float *pf = NULL;
char *ps = NULL;
pi = (int *)malloc(sizeof(int));
if (pi == NULL) {
rc = -1;
goto out;
}
printf("%d\r\n", *pi);
pf = (float *)malloc(sizeof(float));
if (pf == NULL) {
rc = -2;
goto out;
}
printf("%f\r\n", *pf);
ps = (char *)malloc(sizeof(char) * 128);
if (ps == NULL) {
rc = -3;
goto out;
}
printf("%s\r\n", ps);
out:
free(ps);
free(pf);
free(pi);
return rc;
}
int main(void) {
if (int rc = test_malloc_free()) {
printf("rc: %d", rc);
return -1;
}
return 0;
}

C风格管理方法强调的是遵守“谁创建,谁释放”(更准确一点来说,应该是“谁触发创建,谁触发释放”,这个“谁”是指某一个作用域)的原则,资源生命期完全由程序员来管理,而这显然增加了程序员的负担,从而降低了开发效率。在一个串行的程序世界里,这种方法只是“麻烦”,而如果遇到并行或并发的应用场景,这种方法就会变得“很麻烦”。

看个例子。我们经常会遇到这样的应用场景:在一个C/S通讯模型中,服务器端IO线程在收到客户端的一个消息(注:这是资源)后,交给另外的Worker线程去处理。在这种情况下,资源最后应该由谁释放?如果还遵守“谁创建,谁释放”的原则,那要么采用复制消息、消除共享的方法(如果消息太大,则会降低性能),要么采用复杂的机制以通知IO线程释放资源的方法。

换个思路,如果采用“谁最后使用,谁释放”的原则会怎么样?不用复制了,也不用通知了,但程序员需要小心翼翼地跟踪资源的轨迹,确保在最后一次使用资源后释放。显然,这方法“非常麻烦”。能不能把“跟踪资源的轨迹”这个“人工操作”封装起来?这个其实不难做到,把资源包一层,加个引用计数的功能就可以了(有个例子:Jansson Reference Count)。不过,这个方法仍然需要程序员明确地触发引用计数增减操作,还是“很麻烦”。

到底有没有一种简洁方便的管理资源的方法?有,这就是本文要谈的“把资源拖管给栈对象,并利用栈对象自动析构特性来管理资源”的方法。看个例子:

例子2:

C++
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
#include <iostream>
using namespace std;
template<typename T>
class res_box {
private:
T *ptr_;
public:
res_box(T *ptr = NULL) : ptr_(ptr) {}
T *get() {
return ptr_;
}
~res_box() {
delete ptr_;
}
};
void test_res_box() {
res_box<int> pi(new int(0));
cout << *pi.get() << endl;
res_box<float> pf(new float(0.0));
cout << *pf.get() << endl;
res_box<string> ps(new string("hello, world!"));
cout << *ps.get() << endl;
}
int main(void) {
test_res_box();
return 0;
}

对比一下“int test_malloc_free();”和“void test_res_box();”这两个函数,是不是觉得后者很简洁(注:功能是一样的)?是的,即使发生导常,后者的资源也能正确释放。而对于上文提到的并发场景,我们只要对托管对象稍加改造(即引入引用计数功能),也能轻松解决资源管理问题。看以下例子(为了简单起见,去掉多线程同步代码):

例子3:

C++
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
#include <iostream>
using namespace std;
template<typename T>
class res_box {
private:
struct inner_res_box {
int ref_count;
T *ptr_;
inner_res_box(T *ptr) : ref_count(1), ptr_(ptr) {}
~inner_res_box() {
delete ptr_;
}
};
inner_res_box *irb;
public:
res_box(T *ptr = NULL) : irb(new inner_res_box(ptr)) {}
res_box(const res_box &rhs) : irb(rhs.irb) {
++irb->ref_count;
}
res_box &operator=(const res_box &rhs) {
if (irb == rhs.irb) {
return *this;
}
if (--irb->ref_count == 0) {
delete irb;
}
irb = rhs.irb;
++irb->ref_count;
return *this;
}
T *get() {
return irb->ptr_;
}
~res_box() {
if (--irb->ref_count == 0) {
delete irb;
}
}
};
void test_res_box() {
res_box<int> pi(new int(0));
cout << *pi.get() << endl;
res_box<float> pf(new float(0.0));
cout << *pf.get() << endl;
res_box<string> ps(new string("hello, world!"));
cout << *ps.get() << endl;
}
int main(void) {
test_res_box();
return 0;
}

是不是非常简单?即使是分发给多个Worker线程处理也同样简单。呵呵,先别激动太早,本文的主角还没出场,采用智能指针,我们将获得更为简洁的程序。看看采用智能指针后,例子2会变成什么样:

例子4:

C++
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
#include <iostream>
using namespace std;
template<typename T>
class smart_ptr {
private:
T *ptr_;
public:
smart_ptr(T *ptr = NULL) : ptr_(ptr) {}
T *operator->() const {
return ptr_;
}
T &operator*() const {
return *ptr_;
}
T *get() const {
return ptr_;
}
~smart_ptr() {
delete ptr_;
}
};
void test_smart_ptr() {
smart_ptr<int> pi(new int(0));
cout << *pi << endl;
smart_ptr<float> pf(new float(0.0));
cout << *pf << endl;
smart_ptr<string> ps(new string("hello, world!"));
cout << *ps << endl;
}
int main(void) {
test_smart_ptr();
return 0;
}

可以看到,引入操作符重载后,对资源托管容器的访问和对内建指针的访问,形式上统一了。这种貌似内建指针、又提供更多功能的对象就称为智能指针(Smart Pointer)。到目前为止,关于智能指针的话题,才刚刚开始。先想想,指针能用来干嘛?它可以用在:1)解引用或成员访问表达式;2)布尔表达式上下文(如if(ptr){}、while(ptr){}等)当中;3)还可以用于多态语境当中。而智能指针如何做到?

接下来的讨论都是围绕着如何让智能指针更加貌似内建指针,也就是具备上文提到的3点特性(这应该可以作为智能指针实现质量的衡量标准)。在例子4中,我们已经实现了解引用和成员访问表达式,现在我们来看看,如何让智能指针优雅且安全地支持布尔表达式(注:从一篇关于Safe Bool Idiom的讨论中学到了不少知识)。常见的作法有以下4种:

例子5

C++
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#include <iostream>
using namespace std;
template<typename T>
class smart_ptr {
private:
T *ptr_;
public:
smart_ptr(T *ptr = NULL) : ptr_(ptr) {}
operator bool() const {
if (ptr_) {
return true;
} else {
return false;
}
}
~smart_ptr() {
delete ptr_;
}
};
template<typename T>
class smart_ptr2 {
private:
T *ptr_;
public:
smart_ptr2(T *ptr = NULL) : ptr_(ptr) {}
bool operator!() const {
if (ptr_) {
return false;
} else {
return true;
}
}
~smart_ptr2() {
delete ptr_;
}
};
template<typename T>
class smart_ptr3 {
private:
T *ptr_;
public:
smart_ptr3(T *ptr = NULL) : ptr_(ptr) {}
operator void*() const {
return ptr_;
}
~smart_ptr3() {
delete ptr_;
}
};
template<typename T>
class smart_ptr4 {
private:
T *ptr_;
public:
smart_ptr4(T *ptr = NULL) : ptr_(ptr) {}
class nested_class;
operator nested_class*() const {
return ptr_ != NULL ? reinterpret_cast<nested_class*>(ptr_) : NULL;
}
~smart_ptr4() {
delete ptr_;
}
};
template<typename T>
class smart_ptr5 {
private:
T *ptr_;
typedef T *(smart_ptr5::*bool_type)() const;
void operator==(smart_ptr5 const&) const;
void operator!=(smart_ptr5 const&) const;
public:
smart_ptr5(T *ptr = NULL) : ptr_(ptr) {}
operator bool_type() const {
return ptr_ != NULL ? &smart_ptr5::get : NULL;
}
T *get() const {
return ptr_;
}
~smart_ptr5() {
delete ptr_;
}
};
void test_smart_ptr() {
smart_ptr<int> p0;
smart_ptr<int> p1;
if (p0) {
cout << "p0 is valid" << endl;
} else {
cout << "p0 is invalid" << endl;
}
p0 << 1;
cout << "p0 << 1: " << p0 << endl;
int i = p1;
cout << "int i = p1: " << i << endl;
if (p0 == p1) {
cout << "p0 == p1" << endl;
} else {
cout << "p0 != p1" << endl;
}
}
void test_smart_ptr2() {
smart_ptr2<int> p0;
smart_ptr2<int> p1;
if ( !! p0) {
cout << "p0 is valid" << endl;
} else {
cout << "p0 is invalid" << endl;
}
/*
g++: error: no match for ‘operator<<’ in ‘p0 << 1’
g++: error: cannot convert ‘smart_ptr2<int>’ to ‘int’ in initialization
p0 << 1;
cout << "p0 << 1: " << p0 << endl;
int i = p1;
cout << "int i = p1: " << i << endl;
*/
/*
g++: error: no match for ‘operator==’ in ‘p0 == p1’
if (p0 == p1) {
cout << "p0 == p1" << endl;
} else {
cout << "p0 != p1" << endl;
}
*/
}
void test_smart_ptr3() {
smart_ptr3<int> p0;
smart_ptr3<int> p1;
if (p0) {
cout << "p0 is valid" << endl;
} else {
cout << "p0 is invalid" << endl;
}
if (p0 == p1) {
cout << "p0 == p1" << endl;
} else {
cout << "p0 != p1" << endl;
}
/*
g++: error: no match for ‘operator<<’ in ‘p0 << 1’
g++: error: invalid conversion from ‘void*’ to ‘int’
p0 << 1;
cout << "p0 << 1: " << p0 << endl;
int i = p1;
cout << "int i = p1: " << i << endl;
*/
/*
g++: warning: deleting ‘void*’ is undefined
g++: warning: deleting ‘void*’ is undefined
delete p0;
delete p1;
*/
}
void test_smart_ptr4() {
smart_ptr4<int> p0;
smart_ptr4<int> p1;
if (p0) {
cout << "p0 is valid" << endl;
} else {
cout << "p0 is invalid" << endl;
}
if (p0 == p1) {
cout << "p0 == p1" << endl;
} else {
cout << "p0 != p1" << endl;
}
/*
g++: error: no match for ‘operator<<’ in ‘p0 << 1’
g++: error: invalid conversion from ‘smart_ptr4<int>::nested_class*’ to ‘int’
p0 << 1;
cout << "p0 << 1: " << p0 << endl;
int i = p1;
cout << "int i = p1: " << i << endl;
*/
/*
g++: warning: possible problem detected in invocation of delete operator:
g++: warning: invalid use of incomplete type ‘struct smart_ptr4<int>::nested_class’
g++: warning: declaration of ‘struct smart_ptr4<int>::nested_class’
g++: note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined.
delete p0;
delete p1;
*/
}
void test_smart_ptr5() {
smart_ptr5<int> p0;
smart_ptr5<int> p1;
if (p0) {
cout << "p0 is valid" << endl;
} else {
cout << "p0 is invalid" << endl;
}
/*
g++: error: ‘void smart_ptr5<T>::operator==(const smart_ptr5<T>&) const [with T = int]’ is private
g++: within this context
g++: could not convert ‘p0.smart_ptr5<T>::operator== [with T = int](((const smart_ptr5<int>&)((const smart_ptr5<int>*)(& p1))))’ to ‘bool’
if (p0 == p1) {
cout << "p0 == p1" << endl;
} else {
cout << "p0 != p1" << endl;
}
*/
/*
g++: error: no match for ‘operator<<’ in ‘p0 << 1’
g++: error: cannot convert ‘smart_ptr5<int>’ to ‘int’ in initialization
p0 << 1;
cout << "p0 << 1: " << p0 << endl;
int i = p1;
cout << "int i = p1: " << i << endl;
*/
/*
g++: error: type ‘class smart_ptr5<int>’ argument given to ‘delete’, expected pointer
g++: error: type ‘class smart_ptr5<int>’ argument given to ‘delete’, expected pointer
delete p0;
delete p1;
*/
}
int main(void) {
test_smart_ptr();
test_smart_ptr2();
test_smart_ptr3();
test_smart_ptr4();
test_smart_ptr5();
return 0;
}

总结一下,要想智能指针优雅且安全支持布尔表达式,需要注意以下两点:
1、只能隐式转换成bool值。
例子5是怎么做到的?它利用C++的两个特性:
a)A pointer to member cannot be converted to a void*(这里有解释:C++ FQA Lite: Pointers to member functions)。
b)An rvalue of pointer to member type can be converted to an rvalue of type bool。

2、拒绝支持==、!=关系表达式。
这些关系表达式对于两个并非指到同一个数组的指针来说没有意义。例子5通过操作符重载把==、!=这两个关系表达式声明成私有的达到了目的。

好,现在已经可以很好地支持布尔表达式了,接下来看看如何支持继承和多态语境。看以下例子:

例子6:

C++
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <iostream>
#include <vector>
using namespace std;
template<typename T>
class shared_count {
template<class U> friend class smart_ptr;
int ref_count;
T *ptr_;
shared_count(T *ptr) : ref_count(1), ptr_(ptr) {}
~shared_count() {
delete ptr_;
}
};
template<typename T>
class smart_ptr {
private:
template<class U> friend class smart_ptr;
shared_count<T> *pn;
public:
smart_ptr(T *ptr = NULL) : pn(new shared_count<T>(ptr)) {}
smart_ptr(smart_ptr const& rhs) : pn(rhs.pn) {
++pn->ref_count;
}
template<typename U>
smart_ptr(smart_ptr<U> const& rhs)
: pn(reinterpret_cast<shared_count<T> *>(rhs.pn)) {
++pn->ref_count;
}
smart_ptr &operator=(const smart_ptr &rhs) {
if (pn == rhs.pn) {
return *this;
}
if (--pn->ref_count == 0) {
delete pn;
}
pn = rhs.pn;
++pn->ref_count;
return *this;
}
T *operator->() const {
return pn->ptr_;
}
T &operator*() const {
return *pn->ptr_;
}
T *get() {
return pn->ptr_;
}
~smart_ptr() {
if (--pn->ref_count == 0) {
delete pn;
}
}
};
class class_base {
public: virtual void f(void) {
cout << "in base" << endl;
}
};
class class_derived : public class_base {
public: virtual void f(void) {
cout << "in derived" << endl;
}
};
class class_derived2 : public class_derived {
public: virtual void f(void) {
cout << "in derived2" << endl;
}
};
class class_derived3 : public class_derived2 {
public: virtual void f(void) {
cout << "in derived3" << endl;
}
};
int main(void) {
vector<smart_ptr<class_base> > sps;
sps.push_back(smart_ptr<class_base>(new class_base()));
sps.push_back(smart_ptr<class_derived>(new class_derived()));
sps.push_back(smart_ptr<class_derived2>(new class_derived2()));
sps.push_back(smart_ptr<class_derived3>(new class_derived3()));
for(vector<smart_ptr<class_base> >::iterator iter = sps.begin();
iter < sps.end(); ++iter) {
(*iter)->f();
}
return 0;
}

OK,这是如何做到的?要支持多态行为,要点有:
1、只要T*能隐式转换成U*,那么smart_ptr< T>就应该能隐式转换成smart_ptr< U>。
这是利用C++的成员函数模板(Member Function Template)实现的:

template< typename U>
smart_ptr(smart_ptr< U> const& rhs) : pn(reinterpret_cast< shared_count< T> *>(rhs.pn)) {
++pn->ref_count;
}

要注意,即使成员函数模板能生成和拷贝构造函数一样的函数实例,编译器在需要的时候仍然会生成默认拷贝函数,而在例子6当中是不会生成的,因为采用Memberwise方式(也就是一个成员一个成员的复制,如此递归下去)初始化就可以了,显然默认行为是不符合预期的,所以,我们必须明确提供一个:

smart_ptr(smart_ptr const& rhs) : pn(rhs.pn) {
++pn->ref_count;
}

2、为了让smart_ptr< T>能访问smart_ptr< U>的私有成员(例中的pn指针),必须引入友元:

template< class U> friend class smart_ptr;

3、为了在拷贝的过程中不会出问题(如Double Delete问题),我们必须引入引用计数器。

把例子5和例子6综合起来,基本上就是一个可用的智能指针的初稿实现了(精细实现自然需要考虑跨平台、并发等因素了)。实现一个智能指针并不是本文的目的,本文主要是想讨论智能指针的实现原理,以及在实际开发中如何去使用该技术。Boost库里有很棒的智能指针实现:scoped_ptr, shared_ptr, weak_ptr等等,其中shared_ptr已经进入C++11的TR1,未来就是标准库的一部份了(可见该技术经实践证明是可行的),其实现原理就如上文所述,有兴趣的读者建议直接读源代码。

可以看到,智能指针是相当好的东西,有了它以后,对资源的管理就非常方便了。在智能指针面前,带GC的语言的吸引力就减弱了(甚至没有了吸引力)。为什么?不知道大家有没有彻夜调虚拟机GC的经历,整个过程几乎可以用“杯具“两个字来形容,因为再成熟的虚拟机在GC时也总会遇到一些问题,系统的结构也因此需要调整。使用智能指针,内存使用是可控的,同时又降低了程序员的负担,可谓一箭双雕。其实,还有一个好处,那就是资源的统一处理:带GC的语言对栈内存、堆内存是自动释放的,但是对于文件描述符、网络连接、锁等资源则需要用户自己释放;而借助智能指针(栈对象),C++做到了所有资源的自动释放,是的,尽管资源的类型不一样,申请的方式不一样,但利用栈对象的自动析构特性,各种资源都能自动释放了。这就是统一。

正所谓天下没有免费的午餐,智能指针带来资源管理便利性的同时,自然也会带来一些空间和时间上的代价:
1、空间代价
非引用计数智能指针的空间代价为0,如scoped_ptr;
引用计数类智能指针的空间代价为多了一个计数器(如int),就是多了sizeof(计数器)个字节(这里不考虑边界对齐问题),按8字节来算,1000000个对象多占用不到8M的空间。

2、时间代价
引用计数类智能指针,在构造、拷贝构造、赋值的时候会多一些操作;
不管是哪种智能指什,在进行成员操作的时候,都会多了一次间接引用操作。

和C风格中一大堆眼花撩乱的判断、跳转比起来,时间代价可以忽略不计;至于8M的空间代价,就现代的大部份设备来看,我认为也不是问题。

至于引入智能指针后,程序的API如何设计,后续有时间再来补充。

—The end.

Posted in Programming | Tagged , , , | Leave a comment

是返回值(错误码、特殊值),还是抛出异常?说说我的选择

昨晚翻了翻《松本行弘的程序世界》这本书,看到他对异常设计原则的讲述,觉得颇为赞同。近期的面试,我有时也问类似的问题,但应聘者的回答大都不能令人满意。有必要理一理,说说我是怎么理解的,以及在编程实践中如何做出合适的选择。当然这只是一家之言,未必就是完全正确的。

在行文之前,我有一个观点需要明确:错误码和异常,这两者在程序的表达能力上是等价的,它们都可以向调用者传达“与常规情况不一样的状态”。因此,要使用哪一种,是需要从API的设计、系统的性能指标、新旧代码的一致性这3个角度来考虑的。本文主要从API的设计着手,试图解决两个问题:1)为什么要使用异常?2)什么时候应返回特殊值(注:这不是错误码)而不是抛出异常?

好,先来看一个使用返回错误码的例子:

C++
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
#include <iostream>
using namespace std;
int strlen(char *string) {
if (string == NULL) {
return -1;
}
int len = 0;
while(*string++ != '\0') {
len += 1;
}
return len;
}
int main(void) {
int rc;
char input[] = {0};
rc = strlen(input);
if (rc == -1) {
cout << "Error input!" << endl;
return -1;
}
cout << "String length: " << rc << endl;
char *input2 = NULL;
rc = strlen(input2);
if (rc == -1) {
cout << "Error input!" << endl;
return -2;
}
cout << "String length: " << rc << endl;
return 0;
}

与之等价的使用异常的程序是:

C++
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
#include <iostream>
using namespace std;
int strlen(char *string) {
if (string == NULL) {
throw "Invalid input!";
}
int len = 0;
while(*string++ != '\0') {
len += 1;
}
return len;
}
int main(void) {
char input[] = {0};
cout << "String length: " << strlen(input) << endl;
char *input2 = NULL;
cout << "String length: " << strlen(input2) << endl;
return 0;
}

从以上两个程序片段的对比中,不难看出使用异常的程序更为简洁易懂。为什么?

原因是:返回错误码的方式,使得调用方必须对返回值进行判断,并作相应的处理。这里的处理行为,大部份情况下只是打一下日志,然后返回,如此这般一直传递到最上层的调用方,由它终止本次的调用行为。这里强调的是,“必须要处理错误码“,否则会有两个问题:1)程序接下来的行为都是基于不确定的状态,继续往下执行的话就有可能隐藏BUG;2)自下而上传递的过程实际上是语言系统出栈的过程,我们必须在每一层都记下日志以形成日志栈,这样才便于追查问题。

而采用异常的方式,只管写出常规情况下的逻辑就可以了,一旦出现异常情况,语言系统会接管自下而上传递信息的过程。我们不用在每一层调用都进行判断处理(不明确处理,语言系统自动向上传播)。最上层的调用方很容易就可以获得本次的调用栈,把该调用栈记录下来就可以了。因此,使用异常能够提供更为简洁的API。

上述的例子还不是最绝的,因为错误码和常规输出值并没有交集,那最绝的情况是什么呢?错误码侵入了或者说污染了常规输出值的值域了,这时只能通过其它的渠道返回常规输出了。如:

C++
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
#include <iostream>
using namespace std;
int get_avg_temperature(int day, int *result) {
if (day < 0) {
return -1;
}
*result = day;
return 0;
}
int main(void) {
int rc;
int result;
rc = get_avg_temperature(1, &result);
if (rc == -1) {
cout << "Error input!" << endl;
return -1;
}
cout << "Avg temperature: " << result << endl;
rc = get_avg_temperature(-1, &result);
if (rc == -1) {
cout << "Error input!" << endl;
return -2;
}
cout << "Avg temperature: " << result << endl;
return 0;
}

当然,如果能忍受低效率,也可以把错误码和常规输出捆到一个结构里再返回,如下:

C++
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
#include <iostream>
using namespace std;
typedef struct {
int rc;
int result;
} box_t;
box_t get_avg_temperature(int day) {
box_t b;
if (day < 0) {
b.rc = -1;
b.result = 0;
return b;
}
b.rc = day;
b.result = 0;
return b;
}
int main(void) {
box_t b;
b = get_avg_temperature(1);
if (b.rc == -1) {
cout << "Error input!" << endl;
return -1;
}
cout << "Avg temperature: " << b.result << endl;
b = get_avg_temperature(-1);
if (b.rc == -1) {
cout << "Error input!" << endl;
return -2;
}
cout << "Avg temperature: " << b.result << endl;
return 0;
}

与之等价的使用异常的程序是:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
int get_avg_temperature(int day) {
if (day < 0) {
throw "Invalid day!";
}
return day;
}
int main(void) {
cout << "Avg temperature: " << get_avg_temperature(1) << endl;
cout << "Avg temperature: " << get_avg_temperature(-1) << endl;
return 0;
}

哪一个丑陋,哪一个优雅,我想应该不用我多说了。异常机制虽好,但要是使用不当,设计出来的API反而会比较难用。举个例子:

C++
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
#include <iostream>
#include <string>
#include <map>
using namespace std;
class database {
private:
map<string, int> store;
public:
database() {
store["a"] = 100;
store["b"] = 99;
store["c"] = 98;
}
int get(string key) {
map<string, int>::iterator iter = store.find(key);
if (iter == store.end()) {
throw "No such user!";
}
return iter->second;
}
};
int main(void) {
database db;
try {
cout << "Score: " << db.get("a") << endl;
} catch (char const *&e) {
cout << "No such user!" << endl;
} catch (...) {
cout << e << endl;
}
try {
cout << "Score: " << db.get("d") << endl;
} catch (char const *&e) {
cout << "No such user!" << endl;
} catch (...) {
cout << e << endl;
}
return 0;
}

这个例子也使用了异常,但却是不恰当的使用。因为,“找”这个操作只有两个结果:要么“找到”,要么“没找到”。换句话说,“没找到“也是一种常规输出值。一旦抛出常规输出值,那在调用链上的所有层次里都需要捕获该异常并进行处理,那么使用异常的初衷和好处也就消失了。实践中,在这种查找类的功能里,如果没找到相应记录,一般是通过返回一个特殊的值来告知调用方,比如:NULL、特殊的对象(如iterator)、特殊的整数(如EOF)等等(为什么?一是使用异常没带来什么好处,二是逻辑统一可能为后续处理带来便利)。因此,上述例子可以改造为:

C++
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
#include <iostream>
#include <string>
#include <map>
using namespace std;
class database {
private:
map<string, int> store;
public:
database() {
store["a"] = 100;
store["b"] = 99;
store["c"] = 98;
}
map<string, int>::iterator get(string key) {
return store.find(key);
}
inline map<string, int>::iterator end_iterator() {
return store.end();
}
};
int main(void) {
database db;
map<string, int>::iterator iter;
iter = db.get("a");
if (iter == db.end_iterator()) {
cout << "No such user!" << endl;
} else {
cout << "Score: " << iter->second << endl;
}
iter = db.get("d");
if (iter == db.end_iterator()) {
cout << "No such user!" << endl;
} else {
cout << "Score: " << iter->second << endl;
}
return 0;
}

接下来再举一些例子:

使用特殊值的例子:
1、检索数据时,对应某一键不存在相应的记录的情况。
2、判断是与否。

使用异常的例子:
1、读取文件时,文件不存在的情况。
2、修改用户资料时,用户不存在的情况。
3、参数出错。
4、数组越界。
5、除0错。
6、入栈,栈满;出栈,栈空。
7、网络错误。

综上所述,本文的结论是:

1、异常能提供更为简洁的API,并且能更早地发现隐藏的BUG。如有可能,要尽量采用。
2、不要抛出原本属于返回值值域里的值,要直接返回特殊值。经典使用场景是查找和判断。

—The end.

Posted in Programming | Tagged , , | 32 Comments

主机迁移到Linode(Tokyo机房),域名更换为xuchaoqian.com(Godaddy),博客(WordPress)升级到3.2.1

本来想自己攒一台服务器,然后托管给国内某个ISP提供商。在行动的过程中,恰逢Linode新增亚太机房,本机房在北京的Ping值在100ms左右,这就提供了可接受的访问速度。再考虑到一些众所周知的原因,我很快就拍下了Linode 512型的主机(选择的操作系统是Ubuntu 10.04 LTS 64bit)。

然后,修改原先longker.org的域名解析,以指到新的主机地址(生效时间在1个小时左右),再依次安装好MySQL(5.1.58)、PHP(5.3.8)、Nginx(1.0.6)等软件,接着修改Nginx的虚拟主机配置,再加上一些其它的折腾。。。域名解析生效后,安装好WordPress3.2.1(最新版),再写个程序把老系统的数据导出(老系统的导出程序有问题,只好自己写程序),然后把数据导入到新系统当中。

至此,迁移完毕。整个迁移过程还算比较顺利,除了博客的数据迁移费了一些周折,原因是老博客系统(2008年的)太老了,导出程序没法用,再加上新系统数据结构上有一些变更,最后选择了一个代价较小的方案:根据老系统的数据,生成符合新系统导入格式的XML文件,再在后导执行import操作。顺便赞一下新版WordPress,改进不少(比较明显的是widgets和plugins两块),值得升级。

另外,我启用了新的域名http://xuchaoqian.com/。选择的注册商是Godaddy,为什么不在国内注册,原因也是众所周知的。我就提两点,所有权管理更方便(转移注册商、过户等)、域名解析生效时间更快。原来的域名http://www.longker.org/,目前还可以使用(但做了跳转),不过往后可能不再维护,所以,请有兴趣的朋友订阅新地址。

从实际的使用经验来看,Linode VPS还可以,但由于机房不在国内,有时候可能会出现The connection was reset或The connection has timed out的问题,至于为什么,我想你也是懂的。有心迁移的朋友可能需要评估这个问题带来的影响。

Posted in Daily Life | 36 Comments

2010,上半年志

DAL2.2发布。
这是一个较为成熟的大版本。在我心里,我的使命到此也就结束了。

离开手机之家。
最终还是做了这个选择。现在这个项目由增禄接手。我有理由相信增禄能够继续推动DAL向前发展,并取得更大的成绩。
手机之家曾经是我心灵的归宿。我想,不管以后身在何方,手机之家都将是我美好的记忆。

登了一回泰山。
登泰山的想法由来已久,却一直不能实现。这回终于圆梦了。遗憾的是没有看到日出,更糟的是还被淋了一身雨。是的,人生本来就是充满着缺憾的。

去了一趟武汉。
毕业几年后,放下忙碌的工作和紧张的生活,突然有一种被掏空的感觉。也许,只有重新回到起点,才能看清自己的终点。去了学校,还去了东湖,依然是那样得美好。

加入百度。
这个选择,我想几年过后再来评价,会更加客观准确。

小外甥考上山东大学。
金榜题名时,很高兴,呵呵。在这祝愿他学业有成吧。大学只是刚刚开始,不要虚度光阴。

也学车了。
参加驾校培训一个多月了。再过一个多月,就能拿到c1驾照了。学车的理由很简单,待2012到来时,自己能跑得快些。

。。。。。。

结束了,也开始了,下半年会有哪些变化、提高和突破?拭目以待。

Posted in Daily Life | 36 Comments

数据库技术大会结束,<<数据访问层开发实践>>演讲PPT下载

Posted in Daily Life | 71 Comments

Dal2.2.8发布了,开始支持分布式事务(遵循XA规范)

在团队全体成员经过3个月的努力后,Dal2.2系列的一个重要迭代版本Dal2.2.8终于发布了。

在这要说一声,增禄和大庆,你俩辛苦了。

为了支持分布式事务,我们着实费了一点功夫。因为Dal向外提供的特性必须都是语言中立、数据库中立的。所以,我们需要考虑不支持join/suspend/resume等子句的数据库。

MySQL就是这样一个对XA仅提高有限支持的数据库:
For XA START, the JOIN and RESUME clauses are not supported;
For XA END, the SUSPEND [FOR MIGRATE] clause is not supported.
详见:http://dev.mysql.com/doc/refman/5.1/en/xa-restrictions.html

同时,我们还要充份考虑因各种原因造成的状态扰乱问题。还好,最后我们通过嵌套事务扁平化、XA START/XA END命令配对、数据库链接作用域去重叠及链接事务范围内独占来解决这一切问题。

至此,Dal不但继续擅长于提升web2.0+系统的承载能力,而且也适用于保证那些重要系统的关键数据的完整性和安全性。

晚些时候,大庆做了个简单的benchmark,发现在系统复杂度上升、dal-core接口更加易用的情况下,仍然保持和Dal2.1系列一样的性能表现。虽然有些小小的失落(预期是更好),但是也算还好吧。

明天继续灌数据,看看在缓存数据量增大(以触发JVM进行垃圾回收)的情况下的表现吧,顺便profile看看。

Posted in Daily Life | 31 Comments

4月3号参加数据库技术会议,演讲主题是:数据访问层开发实践

话题还是和Dal有关。

Dal2.0发布到现在已经有半年的时间了。这半年来,我们对数据访问层的认识有了不少的变化,Dal整个软件的结构和特性也已经发生了变化。

这次话题,主要是和大家分享和探讨过去的经验、现在的见解及未来的规划等。望有兴趣的同行前往批评指正。

Posted in Daily Life | 31 Comments

用Hudson+Subversion+Maven搭建持续集成(Continuous Integration, CI)环境

为了让接下来的研发工作能更顺利地开展,花了点时间给新团队搭建了个持续集成环境。这里用到的工具主要有:HudsonSubversionMaven等等。

关于持续集成这个概念,有兴趣的可以参考Martin Fowler的一篇文章:Continuous Integration

之前,对那些JAVA编写的服务程序,我们采用的构建方式很原始:手动写Shell脚本,把SVN中的JAVA源码编译成JAVA字节码。应该说,在团队规模较小(不需要太多的协同工作)、部署环境单一(我们之前只部署在Linux环境当中)的时候,这种方式还是不错的,起码两年以来,我们并没有遇到什么问题。

那现在为什么要来搭这个环境?持续集成带来的好处有很多。。。在这我只说说我们这么做的主要原因:
一、进度控制力度增强了,需要快速迭代,也需要更快地看到结果。
二、可测试性要求提高了,所有的代码必须是自测试的。
三、对外发布的不再是源代码,而是可执行文件了。
四、给团队成员一个全新的体验,走正规化开发道路。
。。。

以下是搭建好的持续集成环境的结构图:
Continuous Integration

—The end.

Posted in Daily Life | 25 Comments

试用Google Closure Compiler的API

原来的JS压缩服务采用的是YUI Compressor,一年下来表现还算稳定。

今天老高说想尝试一下Google Closure Compiler,翻了翻文档,看看有没API能直接用的:传一个原始字符串,返回一个压缩后的字符串。找了半天,没有此类的例子(看来设计者认为像我们这样直接采用API而不是使用命令行的用户不多)。

查看JavaDoc,定位到com.google.javascript.jscomp.Compiler,有如下描述:
Compiler (and the other classes in this package) does the following:
* parses JS code
* checks for undefined variables
* performs optimizations such as constant folding and constants inlining
* renames variables (to short names)
* outputs compact javascript code
看来就是这个了。

相中方法: Result compile(JSSourceFile extern, JSSourceFile input, CompilerOptions options) 。
input和options容易理解,extern是什么?其实类的描述里也稍微提了下:
External variables are declared in ‘externs’ files. For instance, the file may include definitions for global javascript/browser objects such as window, document.
很显然可以没有extern(但不能为NULL,设置方法参看底下的代码)。

按道理,编译后返回的result里应该就有我想要的结果了吧,结果出乎意料。Result只有编译状态的描述。难道此路不通?

又从JAR包的META-INF看到入口类是CompilerRunner,一路追踪下去。。。中间省略文字几千字。绕了半天,得出的结论是:和CompilerRunner相关的几个类很难复用,想用的方法要么是不可见,要么是包可见,要么是子类可见。代理来继承去,眼看就OK了,又发现很多状态居然是static的,这使得程序的状态是迭加的。看来此路也不通。。。又绕回Compiler。晕,原来Compiler有一个方法toSource()就是返回压缩化的代码的。

这下好办了。主要代码如下:
final ByteArrayOutputStream err = new ByteArrayOutputStream();
final PrintStream errWrapper = new PrintStream(err);
final Compiler compiler = new Compiler(errWrapper);

final ByteArrayInputStream bais = new ByteArrayInputStream(codeBytes);

final CompilerOptions options = new CompilerOptions();
final CompilationLevel level = CompilationLevel.SIMPLE_OPTIMIZATIONS
level.setOptionsForCompilationLevel(options);

final Result status = compiler.compile(JSSourceFile.fromCode("extern", ""), JSSourceFile.fromInputStream("input", bais), options);
System.out.println(compiler.toSource);

把代码布上后,测试通过。本来这是一件小事,没必要以此作为BLOG的内容。但是这件事也是给了我几个感触,觉得有必要说一说:
一、文档很重要。简单的几句话,也许就能让用户少走很多弯路。DAL后面一定要多写文档,特别是使用手册。
二、API设计要符合直觉。不符合直觉的设计会造成大量的困扰。当年Lucene的delete操作,用脚跟想想就应该放在IndexWriter里,可设计者却把它放在IndexReader里,结果邮件列表里经常出现此类的问题。当然,后续版本已经做了修改(IndexWriter有了delete方法)。用户不会关心你为什么这么做;新用户要抛弃你只要点一下鼠标就可以了;而老用户选择离开只需要皱一下眉头。
三、时间管理很重要。任何事情都应该在任务框架里进行;否则,很多东西可能会失控。

—The end.

Posted in Daily Life | 50 Comments