Linux下的同步与异步

http://blog.sina.com.cn/s/blog_5e0d222e0100kvqq.html

在总结进程通讯的问题时,我考虑再三。似乎逃离不了一个概念。同步与异步。因此,暂且先讨论一下进程的同步与异步。

概念

       如前面总结所述,进程的概念,一定存在于多任务分时操作系统中。当然这也不是非常准确。因为如果多个CPU上同时运行不同的进程,则似乎不存在分时的问题。因此这里修正一下,假设,我们讨论的所有问题,均是在一个CPU上进行。此时,还是需要分时来完成多个进程的各自工作。同时,这种微观上分时完成不同工作的操作机制从宏观上看,每个工作又可等效于同时进行。

       由于进程的概念导致,独立讨论一个进程是毫无意义的。而多个进程在工作时,如果这些进程之间存在一定联系(当然可能毫无联系),则此时会有两种情况。这就是本文要讨论的。同步和异步。

        在我初学阶段, 一直以为同步是同时进行的。这是个错误的概念。曾经看到有人举例,打电话是同步,相对于发消息,我觉得似乎不妥。打电话,如果你说你的,我说我的,则就不是同步,是标准的异步模式,而发消息,比如短信,一问一答,OK,这是标准的同步模式。有人说,阻塞是同步模式,非阻塞是异步模式,这样的解释似乎有点问题。同步不一定需要阻塞。自然非阻塞也不一定是异步。
         那么,谈论进程之间的同步和异步的最根本的区别是什么呢?我用个文绉绉的描述如下:
     当一个进程A存在一个必须执行的操作点,同时该操作点,与另一个进程B的某个操作点存在因果时序关系,则A相对B为同步。反之为异步。
这里需要注意几点,在其他讨论同步和异步问题中所没有或完全不一的概念:
1、 A相对B同步,未必B相对A同步
         最明显的例子是,客户段请求和对反馈的处理是相对于服务端同步的。在服务段接受客户段信息后,到服务段数据没有完全给予客户端之前,这段时间客户端需要等待,也就是阻塞,以同服务器端运行同步。而服务器端并不存在这个问题。如果这个客户端不给服务器发送请求,服务器会转向响应其他客户端的请求。则服务器端相对客户端并不是同步的。
之所以出现这种不对称的更本原因,是 A,B进程工作目标,或工作性质决定的。
2、 AB两个进程之间必须存在操作点之间的因果关系,才叫同步。如果说两个进程抢打印机,则不叫同步。虽然可能存在某个操作点上的相互影响。虽然他们之间可能出现阻塞。但和同步异步没联系。
这一条,实际上也是说扩展说明了。讨论到同步异步,均是指:AB协同完成一个整体任务,缺一不可的情况下。如果两个毫无联系,毫无因果关系,老死不相往来的进程,则不存在同步和异步的讨论。虽然你可以说他们是绝对异步。但没有必要通过同步和异步的分析方法来处理和约束这两个进程的设计。
3、 A相对B同步,则一定存在一个严格按照{ 请求-->等待-->接受 }的时序过程进行。 这种工作在软件设计时是显式存在的(即你在代码中可以看到)。也就是说,存在因果关系的阻塞才是同步。
例如,机器A向机器B传输数据。采用对信息分块,并打包,包前缀含有同步信息的时候,或许有人说。此时不需要等待,因为存在同步头。这种观点本身没有错。如果仅放在传输之段执行时间内来看。但考虑一下传输前的工作。机器A可以在任何情况下,直接传递信息吗?不需要建立信道(通过握手,或查询缓冲区是否清空的各种方式)?考虑一下握手动作,A发送请求REQ,B接受,返回ACK。这就是请求,等待,接受,而采用缓冲方式,A要查询缓冲区是否空(即上一次信息是否被B全部获取),这实质是个等待过程,而发现缓冲区为空,这实质是通过缓冲区的情况,来实现机器B向机器A发送接受信息。
4、 同步和异步都是相对的。在不同的尺度下,可能产生变化。而同步代码的设计和异步代码的设计存在本质差异,因此,分割尺度形成模块化设计,才是程序设计的大学问。至于采用什么函数,什么调用方式,这本身不是一个程序员的价值所在。你能背下所有的C的标准库的函数及参量不代表你能成为一个合格的程序员。

     分析同步和异步,既要考虑任务切割后,每个子部分的工作特性,又要考虑每个子部分及整体的运行参数和目标要求。

我们做个例子来分析同步和异步

      进程A完成外部键盘读入,并存在一个缓冲区。而对读入的字符的分析,由进程B执行。当缓冲区满时,一次性传递给B,由B处理。虽然你按一个键可以在任意时间发生,但A的缓冲区的内容必须等满后才能传递给B,而同时,只有B不忙时,才能接收数据。如果A的缓冲区满了。必须等待B接收完数据,才能继续运行。因此从整体上来看,A相对B而言是同步的。
      可以反证,假设A 相对B是异步的,则A的运行和B没有关系。但是,A在缓冲区满时,无法继续运行,因此假设不成立。
     但B相对A是同步吗?
假设B的工作如下:
      如果键盘请求存在,则接收键盘信息处理(这和上面的工作是一致的),但如果键盘没有信息传递,则检测鼠标是否存在信息传入。此时, B进程,并不会因为A的数据没到位而停止工作。也就是说。无论A是什么情况,B还有很多其他事情要做。同时B该做什么做什么,等有空了,在看是否A有数据需要处理。
因此,B的工作,不会因为A的状态,而导致阻塞。


     上面的讨论,说明了。同步不是相互依赖存在的。

     那么将上面的例子修改一下,假设A的缓冲区只要有数据,就传递给B操作。同时假设缓冲区足够大,B的处理时间足够快。那么此时,A相对B又变成了异步。为什么呢?
      因为,B在处理A缓冲区的内容的时间足够快,以至于A不会因为外部键盘输入把缓冲区填满导致A的停滞。可以说,A的工作和B没有关系。B的工作和A也没有关系。
     可能有人会说,上面例子,一会是同步,一会是异步,其根本原因在于,A缓冲区是否填满才向B传递数据。如果这样理解,则是不完备的。如果这么去设计工程,必然会导致一个潜在风险(假设当作异步模式设计,但A被填满同时阻塞,由于认为是异步模式,所以A采用丢后不管的方式,而阻塞下,A会漏掉输入信息,导致系统出错)。
实际上,上面的同步和异步的差异,还存在于一个假设,即B处理的速度足够快,A的缓冲区足够大。这种情况下,才能完全认为A,B是异步模式。当你发现你的工程的使用目标不能满足上面的假设(B处理速度足够快),则一定要小心处理,当缓冲溢出的代码。按照同步设计的思想进行处理。

     讨论到这里,你也可以发现, 绝对同步是存在的,绝对异步是不存在的。除非两个任务丝毫没有联系。但不代表异步工作毫无意义。 UDP与TCP就可以看作,在针对消息传递这个工作上,前者是异步模式,后者是同步模式。很多情况下,无法做到完全同步(这需要硬件支持),也有些情况下,不需要做到完全同步(例如广播消息,没有收到的信息,并不是重要的,不会对系统产成影响)
      下面讨论一下同步和异步在设计,或规划中的问题。
      首先,我们假设用模块来替代进程。这样可以更好的讨论问题。那么模块的划分,直接影响到属于同步还是异步。
例如,上面的例子,假设A进程实际上内部有两个模块,一个模块是读取键盘信号,转换成字符。而另一个是管理缓冲区,并向B发送消息。
       整体看进程A相对B是同步模块。但内部读取键盘信号的工作和缓冲区管理则是异步的。那么是否可以将两个模块合并,并形成一个程序代码依次执行呢?假设硬件不存在缓冲,通过键盘中断获取消息。这样完全可以。但是这样会使得代码逻辑混乱。因为,可能处理缓冲区的工作中,被键盘中断信息打断。这样会有一个可能的错误。
我们假设BUF足够大,传递数据足够快,同时我们在A模块中,采用双BUF方式,确保任意按键被记录。则通常存在如下代码

   if (buf.size >=MAX_BUF_SIZE){//当缓冲区满
   switch_buf(buf,send_buf);//将接受缓冲区和写出缓冲区进行切换。
    buf.size =0;//A的缓冲区清0
   send_data(send_buf);//向B发送数据,这里面假设最简单的传送方式,B存在个对等的缓冲区,而只是个简单的COPY工作。同时不考虑B的缓冲区的数据冲突问题。
   }
  如果中断发生在switch_buf(buf,send_buf);之后,buf.size = 0;之前,很显然,通过
    buf.data[buf.size ] = c;的方式记录按键会出错。当然上述代码可以修改成如下模式
   if (buf.size >=MAX_BUF_SIZE){
      switch_buf(buf,send_buf);
      send_data(send_buf);
      send_buf.size = 0;
   }
      那么如果中断发生在if (buf.size >= ...)之后,switch_buf之前,或switch_buf内部,如何处理呢?通常系统设计,为了杜绝这种问题,采用了原子操作,其实就是关中断,处理些事情,再开中断。保证这些事情在处理时中断禁止执行。则上面代码得修改如下
   cli();//关中断
   if (buf.size >=MAX_BUF_SIZE){
         
      switch_buf(buf,send_buf);
   sti();//开中断
      send_data(send_buf);
      send_buf.size = 0;
   }else{
      sti();//开中断
   }

       这样的写法,保证了模块A的正确性。但是这样的系统设计有些不妥(得承认设计本身没有问题,执行也没有问题),不妥在哪?cli和sti因为什么而导致要关闭?写代码的人和系统规划作设计书的人可能知道,但后者很难详细描述这个细节。看代码的人,则可能一头雾水。甚至在添加,调整代码时,将这个正确设计给改错了。那么不妨如下设计。
   if (buf.size >=MAX_BUF_SIZE){
    g_switch =1;
      switch_buf(buf,send_buf);
    if (back_c){
      buf.data[buf.size ] = back_c;back_c =0;
    }
      g_switch = 0;
      send_data(send_buf);
    

你可能感兴趣的:(linux编程)