软件日志

在一个健全的软件中,日志系统是必不可少的,因为软件发布到各个地方你并不总是有条件去下断点然后去调试。而且如果你的软件中大量使用多线程,那么即使你有条件断点恐怕也是无济与事,因此最古老也是最有效的方法就是为软件实现一个健全的日志模块。

但是日志过多是会影响效率的,日志过少出问题时又难以用于分析。因此我们可以为日志分级。日志模块可以导出接口如下:


int log_init(void);
int log_set_level(int level);
int log_add(int level, const char *format, ...);
int log_free(void);

日志分为0, 1, 2级.
0级log只打印出导致函数执行失败情况下的信息.
1级log除了打印0级log外还要打印出关键函数内部的执行情况及其他对于软件整体运行至关重要的执行信息.
2级log除了包括0级和1级log之后要打印出每个函数的参数及最终执行结果.

日志级别写入软件的配置文件中, 而不要在程序中使用log_set_level写入一个固定值, 那么当发布给客户时默认级别为0级, 如果用户使用过程中碰到我们未发现的异常, 我们可以根据情况, 修改配置文件中的日志级别, 当现象再次重现时, 我们就可以从日志上得到足够的信息来Debug.

当然这跟断点调试还是有很大差距的, 所以平时写代码不能过度依赖调试器, 有时候也要练习一下人脑debug, 这也要求我们在写代码时一定要考虑全面思维严谨.

代码正交性的一种实现

《unix编程艺术》里面讲到在写代码时尽可能的让代码保持正交性, 但是其实在有些情况下正交性也不是这么容易达到的, 下面举例两个函数, 虽然功能不是很实用, 但是表现的现象在项目中还是经常会碰到的:

int sum_a(const int a[], const int b[], int cnt)
{
int i;
int sum;

sum = 0;

for (i = 0; i < cnt; i++) /*--------------------------line1--------------------------*/ sum += a[i] + b[i]; /*--------------------------line2--------------------------*/ return sum; } int sum_b(const int a[], const int b[], int cnt) { int i; int sum; sum = 0; for (i = 0; i < cnt; i++) /*--------------------------line1--------------------------*/ sum += a[i] / 2 + b[i] / 2; /*--------------------------line2--------------------------*/ return sum; }

从上面可以看到其实这两个函数从结果上来看做的完全不同的事, 但是其接口是一样的, 甚至连大部分代码也是一样的, 最关键的是他们不一样的代码是在line1与line2之间, 这样便不可以把line1之上的代码封装一个函数,因为line1之上的代码与line2之下的代码是有很强的关联性的. 因此我惟一能想到的还是回调函数. 将代码改为如下:

static int handler_a(int a, int b)
{
return a + b;
}

static int handler_b(int a, int b)
{
return a / 2 + b / 2;
}

static int do_sum(const int a[], const int b[], int cnt, int (*handler)(int a, int b))
{
int i;
int sum;

assert(handler);

sum = 0;
for (i = 0; i < cnt; i++) sum += handler(a[i], b[i]); return sum; } int sum_a(const int a[], const int b[], int cnt) { return do_sum(a, b, cnt, handler_a); } int sum_b(const int a[], const int b[], int cnt) { return do_sum(a, b, cnt, handler_b); }

从上面来看改为了正交性之后其实通用性更强, 如果有很多类似sum_a, sum_b之类的函数的情况下, 代码量会明显降低, 但是如果如例子所示只有sum_a, sum_b函数的话, 这么做代码将明显增多, 由代码行数与bug成正比关系得出这其实是不划算的, 另外这种做法完全依赖于编译的优化能力, 如果编译器不进行优化的话在调用handler函数里会压栈很频繁, 对于执行效率来说这么做是不可取的, 幸运的是我测试了几次发现编译器对于这种函数指针的调用的会优化的, 尤其是当函数指针传参为常量的情下.

另外可以看出函数的正交性其实是有一个度的, 这个度没有一个固定标准, 只能靠自己的经验去衡量取得最佳值.

关于MFC的MVC一些想法

在写MFC的时候, 很少是一个对话框能够搞定的, 一般都需要多个对话框交互, 如要是DoMudule的对话框也还好, 因为这种对话框一般不用操作父窗口或者与他平级的窗口, 但是如果是类似在一个 PropertySheet或者更复杂时, 就会出现要多个对话框进行交互. 类似下面一种情况:
1
A区域是一个全局区域(如一个树形控件), B区域根据A区域的选择是用来显示不同的对话框, C域可能是一个Log Window, D区域可能是一些全局按钮(它的操作影响到所有会在B区域出现的对话框), 所有在B区域出现的对话框都要在C区域打印log.
还有最重要的一点, 在B区域中设置一个对话框B1后, 下次显示另一个对话框B2时可能会根据B1的设置状态来做不同的显示.试想一下如果B区域有5个对话框B1, B2, B3, B4, B5.而他们的依赖关系如下:
B1 –> B2, B3, B4;
B2 –> B1, B3, B5;
B3 –> B1, B2, B4;

那么仔细看一下就会发现各种循环依赖, 但是我个人对于循环依赖是有洁癖的.
—————————————————————————————
所以在最近的一次MFC项目中我采用如下方法:
首先我抽象出一个模块E, 这个模块是用来处理纯数据, 他对应于B1~B5对所有状态及实现函数. B1~B5可以设置模块E中属于自己的属性, 可以调用属于自己操作, 至于内部状态怎么去切换是E模块的事, E模块可以用C来实现也可以用C++ 的单例/件模式来实现, 这样他们的依赖关系就成了下面这样:
B1 –> E;
B2 –> E;
B3 –> E;
B4 –> E;
B5 –> E;
如果B区域操作想要对于些全局窗口进行操作如C, 那么只在主对话框在OnInitialDialog中设置一下对于C操作的callback函数即可,这样B1~B5在调用属于自己的函数时, 模块E来决定是不是要使用callback来控制区域C, 还有其它B区域中对话框的数据.

可以看到到目前为止把B区域的对话框当作Module的话, 那么模块E就是Control了.

其实显示即View我没有太好的办法:
我从CDialogEx继承了一个CViewDlg出来然后加了void RefreshUI(void), void RefreshUIData(void)两个纯虚函数, 并重写了OnShowWindow函数差在OnShowWindow中判断当这个对话框在显示时调用RefreshUI, 在被隐藏时调用RefreshUIData.
这样所有B区域的对话框全从CViewDlg中派生出来, 强迫所有B区域对话框来实现RefreshUI来从E模块中获得自己的属性并设置相应的UI内容, 实现RefreshUIData在被隐藏时设置自己的属性到模块E中,以便其他模块在ShoWindow时可能会要用到.
到此View也分开了.

这是我目前有想到的惟一解决循环依赖并差不多实现MVC方法的一种实现, 其实可以看到虽然这种思路可以在写MFC时通用,但是其实做法并不通用尤其是View部分的实现如果是其他的UI设计可能要重新设计View部分的机制才行.

锁对于性能的影响

很偶然的机会发现了无锁队列,然后又很偶然的接触到并行编程,虽然还没弄明白内存屏障等问题,但是历史遗留下一小段测试代码,在这段测试代码中我用分别用lock-free或lock-based方式将一个变量进行自增, 效果竟然差的惊人, 肉眼都能感觉到效率不止差了至少四倍之多。下面看代码:

#include <Windows.h>

#define	LOCK_FREE	1

int cnt;
HANDLE hMutex;

static DWORD WINAPI work_thread(void *param)
{
	int i;
	for (i = 0; i < 1000000; i++) {
		#if LOCK_FREE
		//WaitForSingleObject(hMutex, -1);
		InterlockedIncrement((unsigned long *)&cnt);
		//ReleaseMutex(hMutex);
                #else
		WaitForSingleObject(hMutex, -1);
		cnt++;
		ReleaseMutex(hMutex);

                #endif
	}

        return 0;
}

HANDLE thread_begin(int a)
{
        HANDLE hThread = CreateThread(NULL, 0, work_thread, (void *)a, 0, NULL);

	return hThread;

        return 0;
}


int main(int argc, _TCHAR* argv[])
{
	HANDLE h1, h2;

	hMutex = CreateMutex(NULL, FALSE, NULL);

	h1 = thread_begin(0);
	h2 = thread_begin(1);

	WaitForSingleObject(h1, -1);
	WaitForSingleObject(h2, -1);

	printf("%dn", cnt);

	return 0;
}

在这里我使用LOCK_FREE宏来进行切换lock-free方式还是lock-based方式,虽然没有计算时间,但是肉眼都能大概比较出效率至少差了四倍。另外有一点要说的是,这里只是开了两个线程,而且只做了变量自增而已,试想在其他高并发的场合,那么lock-based效率或许将远低于现在的测试情况,所以锁是效率的大敌之一。当然我现在还不太敢用无锁,因为我还没有搞懂什么时间用内存屏障。

————————————————————————–
为了证明是由于资源冲突而不是函数调用拖效率的后腿,可以将main函数中的两句改为如下:

h1 = thread_begin(0);
Sleep(1000);
h2 = thread_begin(1);

Sleep(1000)是为了等第一个线程跑完,这样两个线程跑的次数一样,但是将不再会产生资源冲突, 而且可以看到就算我们延时了1s,但效率远高于两个线程并发执行.

关于配置文件协议制定

今天关于某些UI设置怎么保存以便下次加载时重现上次UI设置开会做了讨论(一共就3个人^-^!),我提出使用文本化协议将UI设置存储为文本协议,当下次点开UI时首先去解析设置文本,然后重现用户的设置。
没想到的是,我理由都没提出就被两人集体反对。反对理由很简单,他们想用二进制方式来存储,这样一个fread就可以将一个structure读出来,连解析都省了。
那么我就在这里陈述一下理由好了。
其实理由也很简单:
1. 因为设置不多, 就算用C来解析字符串,也费不了多大功夫,效率不是问题
2. 如果以后UI设置增加操作项,那么这个structure势必会被增加字段,那么对旧的配置文件将是灾难性的.下面举例说明:
——————————————————————————————————————-
使用二进制结构配置形式如下:
假设有这样一个结构体:

struct a {
wchar_t a[32];
int b;
}

然后在配置文件里面存了struct a aa[5];这么一个数组。乍一看,存成二进制挺好, 一下就全读出来了,不用解析效率还高。
那么假设这么一种情况,因为UI变动或其他原因,必须要在 struct a;增加一个字段int c;那么别人用旧版软件保存的设置都将作费,因为所有的变量都会依次偏掉sizeof(int)个byte.
————————————-
如果使用文本将会是如下方式:
txt文档中以 a, b n的形式存储, 以n作为一项的结尾。
如果UI变动或其他原因, 必须要加一项c, 那么只需要将每一行改为a, b, c n即可, 如果旧版软件保存的配置文件中没有c字段,那么置默认值就好,这样最起码不会说造成解析错误之类的缺陷。
另外保存为txt档,对其他编辑工具是开放的,用户设置可以手动编辑这个文档来减少UI操作,有类似脚本功能。