16

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

 3 years ago
source link: https://blog.popkx.com/c-language-traps-and-tips-section-6-code-encapsulated-as-functions-is-not-available/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

8ecc94f0c93712f28a0de735a4a9fa85.png

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

不过,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 行代码了。

756585696811d040d15e7de08b77f754.png

程序员小明想到了将上面略微繁琐的“超时”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;
}
c0feb930356315beabc364f29090f94f.png

小明定义的 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");
01d91aef98fbfeb0413c81dd960e4e5c.png

修改后的C语言代码的确更加简洁了,但是好用吗?我们编译这段代码并执行:
# gcc t.c -lpthread
# ./a.out 
timeout
exit

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

“小明的困惑”解析

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

781b25194d3348a0e4a0af40be4293e3.png

那上面的“超时”C语言代码就不能封装,想用时,就只能一行一行写了?当然不是,将“超时”代码封装为函数不合适,还可以封装成宏:
 #define cond_timeout(__cond, __timeout) \
 ({                                  \
     long __mtime = get_cur_ms();        \
     while( !(__cond) ){                 \
         usleep(200);                \
         if(get_cur_ms()-__mtime >= __timeout) \
             break;  \
     }                   \
     (!(__cond));            \
})
3691cadaa76d8faad4fd4acae6cd2a39.png

上面这段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语言代码如下,请看:

181e9237fbf8f94b0cff2cd3df998859.png

编译并执行这段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;
 }
3dcdb3f8f5f0b900ac7be4991d65fe19.png

编译并执行修改后的C语言代码,得到如下结果:
# gcc t.c -lpthread
# ./a.out 
timeout
exit

一切与预期一致。

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

# gcc -E t.c

得到如下结果:

06fe8e4186ab9989644d283646b0489e.png

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK