Closure: Cache pomocí uzávěr v JS a PHP

Díky uzávěrám (Closure) můžete v JavaScriptu dělat celou řadu věcí, které by jinak nebyly možné. Navíc přibližně od roku 2010 již prohlížeče dokáží práci z uzávěrami optimalizovat, takže nedochází k (větším) únikům paměti (Memory Leaks) a jejich použití je (většinou) bezpečné i pro začátečníky.

A od verze PHP 5.3 můžete uzávěry (resp. anonymní funkce, které uzávěry podporují klíčovým slovem use) používat i v PHP víceméně stejným způsobem.

Jedním z případů, kdy můžete uzávěry použít k optimalizaci kódu je načítání dat z cache a databáze.

Přímá cache

Vezměme si příklad s načítáním dat z Cache: potřebujete získat nějaká data, která máte buď v lokální cache nebo je musíte vytáhnout z databáze, pokud ještě v cache nejsou.

Samozřejmě můžete postupovat jednoduše tak, že data vytáhnete buď z cache nebo DB:

var get = function(key) {
    if (!app.cache[key]) {
        $.ajax({
            url:'/db/get/' + key,
            async: false,
            success: function(data) {
                app.cache[key] = data;
            }
        });
    }
    return app.cache[key];
}
var data = [], i;
for (i = 0; i < 1000; ++i) {
    data[i] = get(i);
}

Funkce success obsahuje uzávěru na proměnné key. AJAX se provádí synchronně, aby bylo možno vrátit hodnotu přímo z funkce – to v JS není běžné, ale odpovídá stejnému řešení v PHP a demonstruje nevhodnost tohoto přístupu.

$cache = array();
function get($key) {
    global $cache;
    if (!array_key_exists($key, $cache) {
        $cache[$key] = DB->get(key);
    }
    return $cache[$key]
}
$data = array();
for ($i = 0; $i < 1000; ++$i) {
    $data[$i] = get(i);
}

Nevýhoda obou přístupů je v tom, že pokud není hodnota v cache, musí program čekat na stažení AJAXu resp. vytažení data z databáze. A jelikož se přístup k AJAX/DB provádí pro každou položku, může to trvat opravdu hodně dlouho (připojení k serveru, sestavení dotazu, zpracování dotazu na server, sestavení odpovědi, …).

Uzavřená cache

Mnohem vhodnější řešení je pro data, která v cache nejsou, zapamatovat si jejich klíč a pak se na všechny dotázat najedou, až bude jasno, které hodnoty je potřeba přes AJAX nebo z DB stáhnout.

var toLoad = [];
var get = function(key, callback) {
    if (app.cache[key]) {
        callback(key, app.cache[key]);
        return;
    }
    toLoad[key] = callback;
}
var loadAjax = function() {
    var keys = [], i;
    for (i in cache) {
        keys.push(i);
    }    
    $.ajax({
        url:'/db/getAll/' + keys.join(','),
        success: function(data) {
            for (i in data) {
                app.cache[i] = data[i];
                toLoad[i](i, data[i]);
            }
        }
    });
}
var data = [], i;
for (i = 0; i < 1000; ++i) {
    get(i, function(key, value) {
        data[key] = value;
    });
}
if (toLoad.length) {
    loadAjax();
}
$cache = array();
$toLoad = array();
function get($key, $callback) {
    global $cache;
    if (array_key_exists($key, $cache) {
        $callback($cache[$key]);
        return;
    }
    $toLoad[$key] = $callback;
}
function loadDB() {
    global $toLoad, $cache;
    $data = $DB->getAll(array_keys($toLoad));
    foreach ($data as $key => $value) {
        $cache[$key] = $value;
        $toLoad[$key]($value);
    }
}

$data = array();
for ($i = 0; $i < 1000; ++$i) {
    get(i, function($value) use ($i, &$data) {
         $data[$i] = $value;
    });
}
if (count($toLoad)) {
    loadDB();
}

Upravené kódy fungují tak, že v případě, že danou hodnotu nemají v cache, pouze si uloží klíč a callback, který k němu přísluší. Po skončení cyklu pak zavolají funkci, která stáhne všechny potřebné klíče v jednom dotazu a pomocí callbacků uloží stažená data tam, kam je potřeba.

Nevýhoda tohoto přístupu je v tom, že data nejsou okamžitě k dispozici, ale obrovská výhoda je v rychlosti, jakou se data načítají do cache. Vždy totiž probíhá jen jeden dotaz přes AJAX nebo do databáze a tudíž odpadá čas potřebný na spojení se serverem a zpracováním opakovaných dotazů.

Jen jeden callback

Dalšího urychlení dosáhnete, pokud si callback připravíte dopředu a nebudete ho vytvářet až v cyklu. Vytvoření jedné funkce je totiž rychlejší než vytvoření tisíce různých funkcí. Ale je potřeba dát pozor na to, že v některých případech je potřeba mít tisíc různých funkcí, protože každá může dělat něco jiného.

var toLoad = [];
var get = function(key, callback) {
    if (app.cache[key]) {
        callback(key, app.cache[key]);
        return;
    }
    toLoad[key] = callback;
}
var loadAjax = function() {
    var keys = [], i;
    for (i in cache) {
        keys.push(i);
    }    
    $.ajax({
        url:'/db/getAll/' + keys.join(','),
        success: function(data) {
            for (i in data) {
                app.cache[i] = data[i];
                toLoad[i](i, data[i]);
            }
        }
    });
}
var data = [], i;
var callback = function(key, value) {
    data[key] = value;
};
for (i = 0; i < 1000; ++i) {
    get(i, callback);
}
if (toLoad.length) {
    loadAjax();
}
$cache = array();
$toLoad = array();
function get($key, $callback) {
    global $cache;
    if (array_key_exists($key, $cache) {
        $callback($key, $cache[$key]);
        return;
    }
    $toLoad[$key] = $callback;
}
function loadDB() {
    global $toLoad, $cache;
    $data = $DB->getAll(array_keys($toLoad));
    foreach ($data as $key => $value) {
        $cache[$key] = $value;
        $toLoad[$key]($key, $value);
    }
}

$data = array();
$callback = function($key, $value) use (&$data) {
    $data[$key] = $value;
}
for ($i = 0; $i < 1000; ++$i) {
    get($i, $callback);
}
if (count($toLoad)) {
    loadDB();
}

U PHP je potřeba dát pozor na to, že u definice use musí být uveden znak & tam, kde potřebujeme měnit původní hodnotu a tedy předat ji odkazem místo vytvoření její kopie! V JavaScriptu je pole zároveň objekt, takže se odkazem předává pokaždé.

V uvedeném PHP kódu je také vidět rozdíl mezi jedním a tisícem callbacků. V případě, kdy máme jen jeden, musíme hodnotu $key předávat do callbacku jako parametr. V prvním případě, kdy máme samostatnou funkci pro každé volání, můžete hodnotu $key předat přes use v okamžiku vytvoření callbacku – funkce, která callback volá, již nemusí vědět, do kterého indexu je potřeba hodnotu uložit, protože každý callback je spojen vždy s jedním daným indexem $key.

Callback svázaný s hodnotou

Alternativou by bylo použití funkce Closure::bind(), pomocí které můžete do uzávěry předat pokaždé jiný objekt:

//Nejprve vytvoříme pomocnou třídu pro
//uložení klíče, protože bind() funguje
//jen s objekty a ne s primitivními typy
class Key {
    public $key;
    public function __construct($key) {
        $this->key = $key;
    }
}
//pak vytvoříme callback, který umí
//s pomocnou třídou pracovat
$callback = function($value) use (&$data) {
    $data[$this->key] = $value;
}
//a před předáním callbacku ho spojíme
//s vytvořeným objektem, který klíč uloží
for ($i = 0; $i < 1000; ++$i) {
    $key = new Key($i);
    $c = Closure::bind($callback, $key);
    get($i, $c);
}

Automatické stažení

V Javascriptu pak můžete ještě kód vylepšit tím, že funkce loadAjax() se bude volat automaticky po určité době:

var timeout = null;
var toLoad = [];
var get = function(key, callback) {
    if (app.cache[key]) {
        callback(key, app.cache[key]);
        if (!timeout) {
            timeout = setTimeout(loadAjax, 100);
        }
        return;
    }
    toLoad[key] = callback;
}
var loadAjax = function() {
    var keys = [], i;
    for (i in cache) {
        keys.push(i);
    }    
    $.ajax({
        url:'/db/getAll/' + keys.join(','),
        success: function(data) {
            for (i in data) {
                app.cache[i] = data[i];
                toLoad[i](i, data[i]);
            }
        }
    });
    timeout = null;
}
var data = [], i;
var callback = function(key, value) {
    data[key] = value;
};
for (i = 0; i < 1000; ++i) {
    get(i, callback);
}

V tomto případě se funkce zavolá automaticky, pokud je potřeba a není tak potřeba pamatovat na to, volat ji ručně. Navíc tímto postupem zajistíte, že se na data nebude čekat příliš dlouho, protože funkce se zavolá nejpozději 100ms od prvního dotazu na data. A pokud by byla potřeba další data, naplánuje se nový dotaz (díky tomu, že funkce loadAjax() vymaže proměnnou timeout).

V PHP by tento přístup byl možný pouze při použití vláken, ale pokud používáte PHP klasickým (špagetovým) způsobem, musíte funkci volat ručně. Nicméně i v případě vlákna by bylo potřeba zavolat $thread->join(), což odpovídá zavolání loadDB().

$cache = array();
$toLoad = array();
$thread = null;
function get($key, $callback) {
    global $cache;
    if (array_key_exists($key, $cache) {
        $callback($key, $cache[$key]);
        if (!thread) {
            thread = new loadDB();
            thread->start();
        }
        return;
    }
    $toLoad[$key] = $callback;
}
class loadDB extends Thread {
  public function run() {
    $this->wait(100);
    global $toLoad, $cache;
    $data = $DB->getAll(array_keys($toLoad));
    foreach ($data as $key => $value) {
        $cache[$key] = $value;
        $toLoad[$key]($key, $value);
    }
  }
}

$data = array();
$callback = function($key, $value) use (&$data) {
    $data[$key, $value];
}
for ($i = 0; $i &lt; 1000; ++$i) {
    get(i, callback);
}
if (thread) {
    thread->join();
}

PHP kód demonstruje jednoduché použití vlákna, nicméně neřeší problém se spuštěním dalšího vlákna, pokud to první již doběhlo, podobně jako to dělá JS kód.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *

Tato stránka používá Akismet k omezení spamu. Podívejte se, jak vaše data z komentářů zpracováváme..