iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 6
4
Modern Web

成為 Modern PHPer系列 第 6

Day 06: yield 的使用

「PHP 不會 Memory Leak。」--某知名電商技術工。

前言

PHP 有自己的 Zend VM,正如同 JVM 一般,我們可以從 php.ini 中設計其使用記憶體上限。

目前記憶體上限的預設值是 128M,如果超過這個所設定的值,便會丟出 PHP Fatal Error。

yield

簡介

yield 的存在定位類似於 return,但與 return 不同的是它可以在一個函式區塊中存在一個或多個都會被執行到的位置。

function ret()
{
    return 0;
    return 1;
}

上述程式的 return 1 是永遠不會被執行到的,因為在上面的 return 0 整個函式就結束了。

function gen()
{
    yield 0;
    yield 1;
}

foreach (gen() as $value) {
    echo $value;
}

當我在 foreach 中第一次執行 gen() 時,它將會 return 0;在 foreach 中第二次執行 gen() 時,它將會 return 1

回傳值

具有 yield 的 function,其回傳值將是 PHP 內建的類別 Generator

所以可以這樣寫的(也建議這麼寫)

function gen(): Generator
{
    yield 0;
    yield 1;
}

於生成器函式中取用其它來源

利用 yield from 即可在既有的生成器函式中,取得其它的來源。

function gen1(): Generator
{
    yield 0;
    yield 1;
    yield from gen2();
}

function gen2(): Generator
{
    yield 2;
    yield 3;
}

yield from 不僅支援 Generator,同時也可用於 arrayTraversable object

function gen_to_ten(): Generator
{
    yield 0;
    yield from [1, 2, 3, 4];
    yield from new ArrayIterator([5, 6, 7, 8]);
    yield from nine_ten();
}

function nine_ten(): Generator
{
    yield 9;
    yield 10;
}

yield 的優點

yield 是在每一次執行時才使用當次所需記憶體,故當資料量較大時,記憶體消耗會遠低於直接用 array 塞資料。

// 錯誤案例
function makeRange(int $length): array
{
    $data = [];
    for ($i = 0; $i < $length; $i++) {
        $data[] = $i;
    }
    
    return $data;
}

$range = makeRange(1000000);
foreach ($range as $i) {
    echo $i.PHP_EOL;
}

在上述錯誤的案例中,將會造成 $data 使用過多的記憶體(PHP_INT_SIZE * 1000000)。

// 生成器的使用
function makeRange(int $length): Generator
{
    for ($i = 0; $i < $length; $i++) yield $i;
}

foreach (makeRange() as $i) {
    echo $i.PHP_EOL;
}

在上述案例中,每次進入 makeRange() 時只會使用當次所佔用的 int 記憶體(還有 Generator 物件所需的記憶體)。

實際案例

部份影片服務提供上傳的 API,它們有些服務會要求開發者將影片分成多個相同大小的 block 然後上傳。

因為影片的大小通常都不小,超過 PHP 預設的 128M 限制是很常見的,所以應該用 yield 處理比較妥當。

function sliceFile(string $file)
{
    $file = fopen($file, 'rb');
    
    // foreach (sliceArray($file) as $chunk) {
    //     upload($chunk);
    // }
    
    foreach (sliceGenerator($file) as $chunk) {
        upload($chunk);
    }
    
    fclose($file);
}

function sliceArray(resource $file): array
{
    $slices = [];
    $i = 0;
    while ($data = stream_get_contents($file, 1024, 1024 * $i++)) {
        $slices[] = $data;
    }
    
    return $slices;
}

function sliceGenerator(resource $file): Generator
{
    $i = 0;
    foreach ($data = stream_get_contents($file, 1024, 1024 * $i++) {
        yield $data;
    }
}

註:在 sliceFile() 中,看起來好像是多次執行 sliceGenerator() 這個函式,但實際上它僅多次執行 sliceGenerator() 中的 foreach,所以在 $i 並不會每次都被歸 0。

後記

[問卦]有沒有發文會得罪人的八卦

引言中提到的那句話,是我前陣子去某知名電商面試時,對方技工主管跟我說的(對不起,我真的沒辦法把這種人稱為「工程師」)

這句話搭配上他們公司拿來炫耀交易額的 Dashboard,看著特別諷刺。

那次面試前我剛好在處理優酷影片 upload 的工作,當時我寫了一套非官方的 SDK:youku-php-sdk,當時為了方便起見把影片分割成 array 後上傳,但實際用下去發現這樣會 Memory Leak 與 Out of Memory,所以我很篤定地說「PHP 一定有可能 Memory Leak。」

註:目前這個 youku-php-sdk 還是使用 array 去做分割,後續的修正僅在公司內部套用,後來因為公司的 API Key 過期且該專案中止,故沒辦法繼續開發下去。

ps. 之後這間公司可能還會再出現哦 我會不會被告


上一篇
Day 05:密碼儲存的實踐
下一篇
Day 07:善用預定義的 interface 及 class
系列文
成為 Modern PHPer30

1 則留言

0
kawa0710
iT邦新手 5 級 ‧ 2020-01-16 17:25:47

有實際案例,讚!/images/emoticon/emoticon12.gif

我要留言

立即登入留言