Zade's Weblog

程序人生

Category Archives: Programming

线程同步和日志

当主线程退出的时候,一般的逻辑是要所有的子线程join,这样以来不至于使得所有的子线程被强行的中断。
我现在的程序的逻辑是一旦主线程退出,我需要某种渠道告诉子线程,如果你处理完了所有的任务,那么你也退出吧。在所有的线程

退出以后,我还要通过日志记载资源释放的情况。
大体的代码架构如下:
main_thread()
{
run_when_recieve_term_signal;
send_term_tasks_to_all_sub_threads;
all_sub_threads_join;
}
sub_thread()
{
run_handle_tasks_when_receive_term_task;
exit_thread;
}
main_function()
{
//some other process
main_thread();
log_when_resources_are_released;
}
我现在遇到的问题是,一旦主程序退出,资源释放的日志并没有写入。问题在什么地方?
我简单的分析了一下,因为主线程向子线程发送结束任务的以后,然后再让所有的子线程join,但是这个时候可能子线程已经收到结

束任务的命令,并且已经退出,在这个卡当,会不会产生微妙的问题?因为资源释放的日志都是在主线程发出的。
所以我的策略是让子线程join以前,主线程休眠一段时间:
main_thread()
{
run_when_recieve_term_signal;
send_term_tasks_to_all_sub_threads;
sleep;
all_sub_threads_join;
}
我的理解不知道对不对,因为我也找不到相关的知识,但是结果如我所愿了。

parallel and Sequential Consistency

今天看了一篇文章,有一些收获,但是也有一些疑问.

首先,SC要保证什么?

(1)每个线程内部的指令都是按照程序规定的顺序(program order)执行的(单个线程的视角)
(2)线程执行的交错顺序可以是任意的,但是所有线程所看见的整个程序的总体执行顺序都是一样的(整个程序的视角)

第一个比较容易理解,第二个的意思是保证编译器对并行的代码不要进行乱序优化(乱序优化是单核时代的一个主要的优化手段).作者还说"C++程序员就需要等待C++0x中的atomic operation来保证"SC的正确性.

我的疑问是:

(1)乱序优化是编译器内部的一种实现手段,作为代码者,我完全没有必要知道这个.当然,如果我要了解这个也可以,但是让我在代码上保证这个就完全是"强人所难".

(2)就我的知识,atomic operation不是保证避免乱序优化,而是为了保证原子状态的读写操作,在很大程度上式为了避免从缓存读写内容,而是从代价比较昂贵的内存读写.说明atomic operation最好的例子是两个线程对一个int型的变量累加的操作,如果不使用类似于atomic operation的操作,那么最后的结果可能是变量只是累加1,而不是累加2(因为是两个线程).

异常处理的一个实例

因为异常处理符合中立原则, 还有许多的其他好处, 因而在实践中大行其道; 使用错误返回值的处理错误的方式已经不多见了.

我在实践中走了一条这样的路: 开始的时候使用异常处理错误;因为某些原因改回返回值的方式;现在又想使用异常的方式. 这样的一个思想的过程, 说明了处理错误的复杂性,以及异常和返回值方式的优缺点.

我觉得这个过程很有意思, 能够说明很多的问题, 现记录下来, 以备后用.

开始我遇到的问题是通过URI加载图层,当加载失败的时候抛出异常. 在这里我们把加载数据失败定义为一个错误, 使用异常的方式处理它.

然后继续我们的问题: 很多图层合并为为一个图层集合, 成为我们工程文件的一部分. 为了支持持久化, 我们通过文本文件的方式存储工程属性,这样用户就可以在以后的某个时间打开工程文件,继续以前的工作.

我们使用伪码说明我们的具体实现:

void load_project(string file_path){

try{

   parse_project_file(file_path);

}catch{exception e}{

      MsgBox(e.what());

}

}

void parse_project_file(string file_path){

   for_each(read layer_uri from file_path){

     load_layer_from_uri(layer_uri);

  }

}

layer load_layer_from_uri(string uri){

  if(bad_uri(uri)){

     throw BadUriException();

  }

  return real_load_layer(uri);

}

我们看到, 在中间代码函数load_layer_from_uri中抛出异常, 在中间代码函数parse_project_file中对异常中立, 在最终代码函数load_project中处理异常.这就是我们日常的代码生活.

假定我们有3个layer_uri,其中第一个发生了错误(例如文件名被意外的修改了), 那么后面两个的layer也不能加载了(即便他们是正确的), 最终用户得到的是一个异常消息框.

这样做是用户希望的吗? 也许是, 也许不是. 例如在ArcGIS中, 如果发生上面的错误, 那么用户见到的是两个正确加载的图层, 一个发生错误, 没有正确加载的图层, 并且用户可以在以后某个时间修正这个错误图层的属性(例如把文件名修改回来),然后再正确的加载这个图层.这样处理错误的方式也许是用户更喜欢的.

假如我们采用和ArcGIS相同的方式,我们的代码会有什么影响? 应该是以下的样子:

void load_project(string file_path){

parse_project_file(file_path);

for_each(layer in layer_set){

   if(layer.is_loaded()){

      layer.display_success();

  }else{

    layer.dispaly_failed();

  }

}

}

void parse_project_file(string file_path){

   for_each(read layer_uri from file_path){

     load_layer_from_uri(layer_uri);

  }

}

layer load_layer_from_uri(string uri){

  if(bad_uri(uri)){

     return layer.with_failed_flag();

  }

  return real_load_layer(uri);

}

我们看到, 不同的用户需求还是很大程度上影响了我们的代码组织和实现.

第一种方式简单, 符合我们日常编码的经验; 第二种方式复杂, 但是也许是最终用户希望的.

假如我们使用第一种方式, 那么用户如何修改错误呢? 可以通过工具修复工程文件, 也可以直接的修改工程文件.

我现在比较认可第一种方式.

中立原则-函数异常和函数返回值

传统的编程语言处理错误的方式是使用函数返回值, windows API有大量这样的实例. 在OO兴起以后, 异常处理成了处理错误的经典方式.

异常处理有很多的好处, 例如异常不能被忽略, 异常传递自动进行, 异常使得代码书写非常简洁等.

有的时候我们还是使用函数返回值, 例如查找函数find,如果没有找到则返回-1, 而不是使用异常表示没有找到. 当然我们也可以非常哲学的说, 查找失败并不是一个错误, 所以不会使用异常.这样的逻辑也即意味着:在一般情况下,错误要使用异常处理的方式. 这可以定义一个我们日常编程的一个原则.

这个原则当然是对的; 这是因为我们前面提到的异常处理错误的各种益处. 但是我们这里要说的是, 异常处理还符合了编程的另外一个原则:中立原则. 我很少见到有人提到这个原则, 但是我认为这个原则太重要了.

为了说明这个问题, 我们把日常的代码分成两类: 中间代码和最终代码.

所谓中间代码即是被复用的代码, 我们常见的代码很多都是中间代码,例如所有的库代码都是中间代码, 这包括C++的STL,以及各种开源的代码库等.

所谓的最终代码即不被复用的代码, 例如我们代码中的处理各种用户消息的代码.

我们代码的90%几乎都是中间代码, 中间代码也生成了所谓的中间件. 具体到错误处理, 严格的说,中间代码并不知道如何处理错误. 这正是Bjarne Stroustrup指出的:知道错误发生的人(中间代码书写者)不知道怎么处理它, 知道怎么处理错误的人(最终代码书写者)不知道它什么时候发生.

中间代码不知道如何处理错误的原因是: 在不同的环境下, 人们看待错误的角度不同, 所以处理方式也不一样; 也就是说, 只能交给最终的代码去处理. 从中间代码的角度来看, 只能是对错误”视而不见”,保持沉默和中立.

异常处理的机制使得对错误保持中立非常的简单; 反过来说, 处理错误就麻烦一些(使用大量的try/catch).

异常处理仅是中立原则的一个体现, 还有很多的方式体现中立原则.