2012年3月9日 星期五

nPython的標準輸出與緩存 緩衝問題

Python的標準輸出與緩存

油螞蚱發表於2011-02-13 最近,我的一位同事遇到了一個奇怪的Python問題,他的程序需要create兩個Python腳本來處理某個問題,這兩個Python腳本通過一個管道互相連接起來,其中一個Python腳本從標準輸入實時的讀取另一個Python腳本的標準輸出,但是那個Python腳本的輸出在另一個Python腳本眼裡看起來卻不是實時的,輸出文本直到腳本結束才能看到。
起初我以為這是生成進程或管道的那部分代碼有問題,但很快就排除了,因為我的同事將那個輸出文本的Python腳本用Shell腳本來實現就不存在問題了。顯然是Python將標準輸出給完全緩存住了。這個問題當然很好解決,在每一個輸出的調用後面加上flush調用就可以了。那為什麼Shell腳本就可以,Python腳本就不行,難道是Python的io緩存有bug?
為了進一步了解這個問題,我分別寫了一個Shell腳本與Python腳本來實驗。Shell腳本如下:
1#!/bin/sh
2i=0
3while [ $i - lt 3 ]; do
4    printf "%d\n" $i
5    sleep 1
6    i=$((i+1))
7done
Python腳本如下:
1import sys
2import time
30
4while i < :
5    sys.stdout.write( '%d\n' i)
6    time.sleep( )
7    1
在Shell下直接運行(如下調用)都能實時看到輸出。
$ bash test.sh
$ python test.py
但當通過管道重定向後(如下調用),Shell腳本還能實時輸出,Python腳本則直到腳本結束才看到輸出。
$ bash test.sh | cat -
$ python test.py | cat -
看來當Python的標準輸出sys.stdout連接的是終端時,採用了行緩存模式,而重定向到管道時則變成了完全緩存。翻遍了Python的在線文檔也沒有找到這樣的說明,我想Python的標準輸出與緩存實現應該基於的是libc庫的stdio,莫非libc就是這麼做的。翻看libc的stdio的手冊頁,果然裡面有這麼一段解釋:
At program startup, three text streams are predefined and need not be opened explicitly — standard input (for reading conventional input), stdard output (for writing conventional input), and standard error (for writing diagnostic output). These streams are abbreviated stdin ,stdout and stderr. When opened, the standard error stream is not fully buffered; the standard input and output streams are fully buffered if and only if the streams do not to refer to an interactive device .
為了驗證,又寫了一個C程序:
01#include <stdio.h>
02#include <unistd.h>
03 
04int main( void )
05{
06    int i;
07    for (i = 0; i < 3; ++i) {
08        printf "%d\n" , i);
09        sleep(1);
10    }
11    return 0;
12}
果然運行該程序並通過管道重定向標準輸出,也存在這個問題。那為什麼Shell腳本不存在這個問題呢,顯然是因為Shell腳本實現文本輸出的printf命令執行時是一個單獨的進程,進程結束後自然會flush輸出。如果將Python腳本里的文本輸出不用sys.stdout.write實現,而是用os.system(“printf '%d\\n'” % i)生成一個單獨的進程來實現,也不存在這個問題。
這一番折騰得來的教訓是:
  1. 任何時候都不要想當然得認為別人的代碼有問題(尤其是懷疑Python這樣的程序),自己非常有把握的知識有時候恰恰是不准確的。
  2. Python的很多庫的實現原理其實在libc與系統調用那兒,不要只把目光局限在Python的文檔上。
回到本文最初的那個問題,除了添加flush調用外,還有其它的解決辦法:
  1. 使用-u 選項調用Python腳本,即python -u test.py。該選項將強制stdout,stdin與stderr使用非緩存模式
  2. 設置PYTHONUNBUFFERED環境變量,作用與-u 選項類似。
  3. 重新設置sys.stdout,例如:sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

沒有留言:

張貼留言