25 November 2013

你能看出下面这段代码有什么问题吗?

struct timeval timeNow;
gettimeofday(&timeNow, NULL);
int64_t timePassed = (timeNow.tv_sec - timeStart.tv_sec)*1000000 + (timeNow.tv_usec - timeStart.tv_usec);
int64_t delay = videoFrame->pts - timePassed;
if (delay > 0) {
    usleep(delay);
}

这是一段用来显示视频的代码,其中隐藏的bug会导致视频画面卡死。在揭示bug之前,先理解一下它做了什么: 计算出从视频开始播放到现在的时间间隔(timePassed),把这个时间与当前视频帧的时间戳(videoFrame->pts)比较; 如果该时间戳大于从开始播放到现在的时间间隔,说明还没到显示这一帧的时候,就等待一会儿(usleep(delay))。

实际执行的时候,视频能正常播放半个小时左右的时间,然后就画面就卡死了。后来发现卡死的原因竟然是程序在长时间的sleep! 也就是说,usleep(delay);这一句里的delay成为了一个巨大的值。为什么会这样的?

我们看到,timePassed的单位是微妙,用int64_t表示应该是足够的。 struct timevaltv_sec域的类型长度是机器字长,在32位机上是int32_t型的,于是timeNow.tv_sec - timeStart.tv_sec也是int32_t型的。 这个差值是从开始播放到现在经过的秒数,用32位整数表示也是够的。但问题在于,当它与1000000相乘的时候, 得到的结果用32位整数表示就不够了。int32_t能表示的最大的正整数为2,147,483,647。 这样当播放的秒数达到2147以上时,(timeNow.tv_sec - timeStart.tv_sec)*1000000就超过了上述值。虽然赋值号左边赋值的对象是int64_t的, 但编译器在计算赋值号右边的值的时候并不会把int32_t提升至int64_t,在溢出时就将结果当成了一个接近-2,147,483,647的负值,并将这个负值赋给了timePassed。 于是delay = videoFrame->pts - timePassed就成为了一个巨大的正值,导致程序开始了长时间的睡眠。

出现这个bug主要是想当然地认为编译器会按照你赋给的变量的类型进行提升,但这种想法是错误的。赋值号右边在计算时是不考虑其左边的类型的,要想进行类型提升,必须显式指定。 将计算timePassed的代码改为如下这样就可以了:

int64_t timePassed = ((int64_t)(timeNow.tv_sec - timeStart.tv_sec))*1000000 + (timeNow.tv_usec - timeStart.tv_usec);