C语言陷阱与技巧第6节,代码封装为函数就不可用了?

在C语言程序开发中,如果某个函数需要阻塞等待某些信息,最好将其加上超时限制,否则该函数可能会“无限等待”,将整个线程卡死,这一点上一节已经通过实例较为详细的讨论。但是C语言并没有提供“超时”语法,需要C语言程序员自己实现一套“超时”机制。

避免“繁琐”的代码,小明的困惑

不过,C语言中的“超时”机制并不难实现,上一节利用 usleep() 函数就建立了一套非常简易的“超时”功能,相关的C语言代码这里再写一次,请看:

long otime = get_cur_ms();
while( !ready && get_cur_ms()-otime < 5000)
       usleep(100);
if(get_cur_ms()-otime >= 5000)
    printf("time out\n");
// get_cur_ms() 函数可以获取当前时间毫秒数

上述C语言代码将阻塞等待 ready 位,但是并不会无限等待下去,而是最多等待 5000ms(即 5 秒)。这么处理虽然比较粗糙,但是的确能够解决“无限等待”问题,只不过仅仅等待一个 ready 位就需要写 3 行代码,如果需要做“超时”处理的地方比较多,整个C语言代码看起来就显得非常啰嗦了。

要是算上“超时”判断语句 if(get_cur_ms()-otime >= 5000)的话,就需要至少 4 行代码了。


程序员小明想到了将上面略微繁琐的“超时”C语言代码封装成函数,他想:封装后,以后若是想使用“超时”功能的话,只需一行函数调用就可以了,于是写出了类似下面这样的C语言代码:

int cond_timeout(int cond, long timeout)
{
    long otime = get_cur_ms();
    while( !cond && get_cur_ms()-otime < timeout)
           usleep(100);
    if(get_cur_ms()-otime >= timeout)
        return 1;
    else
        return 0;
}


小明定义的 cond_timeout() 函数接收两个参数:cond 参数表示需要等待的条件,timeout 参数表示最多等待的时间(单位ms)。如果在 timeout 时间内 cond 条件仍然没有成立,则 cond_timeout() 函数返回 1 表示“等待 cond 已超时”,否则返回 0 表示“成功等待到了 cond 条件”。

定义好 cond_timeout() 函数后,小明将上一节等待 ready 位的“超时”C语言代码:

long otime = get_cur_ms();
while( !ready && get_cur_ms()-otime < 5000)
       usleep(100);
if(get_cur_ms()-otime >= 5000)
    printf("time out\n");

修改为:

if(cond_timeout(ready, 5000))
         printf("timeout\n");


修改后的C语言代码的确更加简洁了,但是好用吗?我们编译这段代码并执行:

# gcc t.c -lpthread
# ./a.out 
timeout
exit

奇怪,thread() 线程函数明明在 2 秒后就将 ready 置位了,怎么还是输出了 “time out”呢?小明对此困惑不已。

“小明的困惑”解析

其实小明遇到的问题很像脑筋急转弯,如果读者和小明一样感到困惑,一定是因为“没反应过来”。cond_timeout() 函数没有像小明的预期一样工作的原因很简单:cond 参数只是 cond_timeout() 函数被调用的时候的状态,之后线程函数 thread() 无论如何修改 ready,也不会影响到 cont_timeout() 函数里的 cond。

那上面的“超时”C语言代码就不能封装,想用时,就只能一行一行写了?当然不是,将“超时”代码封装为函数不合适,还可以封装成宏:

 #define cond_timeout(__cond, __timeout) \
 ({                                  \
     long __mtime = get_cur_ms();        \
     while( !(__cond) ){                 \
         usleep(200);                \
         if(get_cur_ms()-__mtime >= __timeout) \
             break;  \
     }                   \
     (!(__cond));            \
})


上面这段C语言代码比较简单,值得说明的一个小技巧是将 {} 放入 (),如此一来,整个 cond_timeout 宏就相当于一条语句,这是 Linux 内核中相当常用的宏定义方法。

cond_timeout 宏 __cond 条件成立,或者等待__cond 条件成立时间超过__timeout,都会到达 (!(__cond))这一行 此时:

cond_timeout(cond, timeout)
// 就相当于
!cond

显然,如果这时 cond 成立了,cond_timeout 宏返回的就是 0 表示“等待 cond 条件没有超时”,否则 cond_timeout 宏返回 1 表示“未能等到 cond 条件成立,超时了”。

现在再将 cond_timeout 写入 main 函数,测试其是否可以正常工作,修改后的C语言代码如下,请看:

编译并执行这段C语言代码,得到如下结果:

# gcc t.c -lpthread
# ./a.out 
exit

因为 thread() 函数 2 秒后将 ready 置位了,所以 cond_timeout 宏没有返回超时。现在将 thread() 函数里的 sleep(2) 改为 sleep(6),相关C语言代码如下,请看:

void *thread(void *p)
{
     sleep(6);      // 修改为睡眠 6 秒
     ready = 1;
     return NULL;
 }


编译并执行修改后的C语言代码,得到如下结果:

# gcc t.c -lpthread
# ./a.out 
timeout
exit

一切与预期一致。

小结

从本节可以看出,define 宏定义有时可以做到函数无法做到的事情。其实想想也能够明白,define 宏定义只是将C语言代码暂时“打包”,如果宏被调用,编译器就将包裹展开,这么看来,define 宏定义实际上只是将若干行代码取了一个名字而已。我们使用 gcc -E 命令获取编译器预处理后的C语言代码:

# gcc -E t.c

得到如下结果:

能够看出,define 宏定义 cond_timeout 本身并没有生成相关的预处理代码,反而在其被调用的地方,编译器将宏的代码直接展开了,这一点和函数是不同的。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif