Többszálú programozás PHP-ben Pthreads Translation segítségével. Többszálú számítástechnika PHP-ben: pthreads A Jedi útja - a PCNTL kiterjesztéssel

Nemrég kipróbáltam a pthread-eket, és kellemesen meglepődtem – ez egy olyan kiterjesztés, amely lehetővé teszi, hogy több valódi szálal dolgozzunk PHP-ben. Nincs emuláció, nincs varázslat, nincs hamisítvány – minden valódi.



Ilyen feladaton gondolkodom. Van egy csomó feladat, amelyeket gyorsan kell elvégezni. A PHP-nek más eszközei is vannak a probléma megoldására, ezekről itt nincs szó, a cikk pthreadekről szól.



Mik azok a pthreadek

Ez minden! Nos, szinte mindent. Valójában van valami, ami felzaklathatja a kíváncsi olvasót. Ezek egyike sem működik az alapértelmezett beállításokkal fordított szabványos PHP-n. A többszálú használat élvezetéhez engedélyeznie kell a ZTS-t (Zend Thread Safety) a PHP-ben.

PHP beállítás

Ezután PHP ZTS-sel. Ne figyelj ilyen nagy különbségre a végrehajtási időben a ZTS nélküli PHP-hez képest (37,65 vs 265,05 másodperc), nem próbáltam közös nevezőre redukálni PHP beállítások. ZTS nélküli esetben például engedélyeztem az XDebug-ot.


Amint látható, 2 szál használatakor a program végrehajtási sebessége körülbelül 1,5-szer nagyobb, mint lineáris kód esetén. 4 szál használata esetén - 3 alkalommal.


Megjegyzendő, hogy bár a processzor 8 magos, a program végrehajtási ideje 4-nél több szál használata esetén szinte változatlan maradt. Úgy tűnik, ez annak köszönhető, hogy a processzorom 4 fizikai maggal rendelkezik.Az érthetőség kedvéért a lemezt diagram formájában ábrázoltam.


Összegzés

A PHP-ben a pthreads kiterjesztéssel meglehetősen elegánsan lehet dolgozni a többszálú kezeléssel. Ez észrevehető termelékenységnövekedést eredményez.

Címkék: Címkék hozzáadása

Néha szükségessé válik több művelet egyidejű végrehajtása, például az egyik adatbázistábla változásainak ellenőrzése és egy másik módosítása. Sőt, ha az egyik művelet (például a változások ellenőrzése) sok időt vesz igénybe, nyilvánvaló, hogy a szekvenciális végrehajtás nem biztosítja az erőforrás-kiegyenlítést.

Az ilyen jellegű problémák megoldására a programozás többszálú kezelést használ – minden művelet egy külön szálba kerül, hozzárendelt mennyiségű erőforrással, és azon belül működik. Ezzel a megközelítéssel minden feladatot külön-külön és függetlenül hajtanak végre.

Noha a PHP nem támogatja a többszálú feldolgozást, számos módszer létezik az emulációra, amelyekről kb majd beszélünk lent.

1. A szkript több példányának futtatása – műveletenként egy példány

//woman.php if (!isset($_GET["szál"])) ( system("wget"http://localhost/woman.php?thread=make_me_happy"); system("wget>http: //localhost/ woman.php?thread=make_me_rich"); ) elseif ($_GET["szál"] == "make_me_happy") ( make_her_happy(); ) elseif ($_GET["szál"] == "make_me_rich" ) (keress_másik_egyet( ); )

Amikor ezt a szkriptet paraméterek nélkül hajtjuk végre, automatikusan lefut magából két másolatot, műveleti azonosítókkal ("thread=make_me_happy" és "thread=make_me_rich"), amelyek elindítják a szükséges függvények végrehajtását.

Így érjük el a kívánt eredményt - két műveletet hajtunk végre egyszerre -, de ez természetesen nem többszálú, hanem egyszerűen mankó a feladatok egyidejű végrehajtásához.

2. A Jedi útvonala - a PCNTL kiterjesztéssel

A PCNTL egy olyan bővítmény, amely lehetővé teszi a folyamatokkal való teljes körű munkát. A kezelés mellett támogatja az üzenetek küldését, az állapotellenőrzést és a prioritások beállítását. Így néz ki az előző, PCNTL-t használó szkript:

$pid = pcntl_fork(); if ($pid == 0) ( make_her_happy(); ) elseif ($pid > 0) ( $pid2 = pcntl_fork(); if ($pid2 == 0) ( keres_másik_egyet(); ) )

Elég zavarosnak tűnik, nézzük végig soronként.

Az első sorban „elágazzuk” az aktuális folyamatot (a fork egy folyamatot másol, miközben megőrzi az összes változó értékét), felosztva két párhuzamosan futó folyamatra (aktuális és gyermek).

Hogy megértsük, hol vagyunk Ebben a pillanatban, egy gyermek vagy anya folyamatban a pcntl_fork függvény 0-t ad vissza a gyermek számára és a folyamatazonosítót az anya esetében. Ezért a második sorban a $pid-t nézzük, ha az nulla, akkor a gyermek folyamatban vagyunk - végrehajtjuk a függvényt, ellenkező esetben az anyában vagyunk (4. sor), majd létrehozunk egy másik folyamatot és hasonlóan végezze el a feladatot.

Szkript végrehajtási folyamat:

Így a szkript további 2 gyermekfolyamatot hoz létre, amelyek a másolatai és ugyanazokat a változókat tartalmazzák hasonló értékkel. A pcntl_fork függvény által visszaadott azonosító segítségével pedig megtudjuk, melyik szálban vagyunk éppen, és végrehajtjuk a szükséges műveleteket.

Úgy tűnik, PHP fejlesztők a párhuzamosságot ritkán használják. A szinkron kód egyszerűségéről nem beszélek, az egyszálú programozás természetesen egyszerűbb és áttekinthetőbb, de néha a párhuzamosság kis kihasználása is érezhető teljesítménynövekedést hozhat.

Ebben a cikkben megvizsgáljuk, hogyan érhető el többszálas működés a PHP-ben a pthreads kiterjesztéssel. Ehhez telepíteni kell a PHP 7.x ZTS (Zend Thread Safety) verzióját, valamint telepített bővítmény pthreads v3. (A cikk írásakor a PHP 7.1-ben a felhasználóknak a fő ágból kell telepíteniük a pthreads tárolóban – lásd a harmadik féltől származó kiterjesztést.)

Egy kis pontosítás: a pthreads v2 a PHP 5.x-hez készült, és már nem támogatott, a pthreads v3 a PHP 7.x-hez való, és jelenleg is folyik a fejlesztés.

Egy ilyen kitérő után térjünk is közvetlenül a lényegre!

Egyszeri feladatok feldolgozása

Néha az egyszeri feladatokat többszálas módon szeretné feldolgozni (például valamilyen I/O-kötött feladat végrehajtása). Ilyen esetekben használhatja a Thread osztályt új szál létrehozására, és egy külön szálon futtathat feldolgozást.

Például:

$task = új osztály kiterjeszti a szálat ( privát $válasz; public function run() ( $content = file_get_contents("http://google.com"); preg_match("~ (.+)~", $tartalom, $egyezések); $this->response = $egyezik; ) ); $task->start() && $task->join(); var_dump($task->response); // string (6) "Google"

Itt a futási metódus a mi feldolgozásunk, amely egy új szálon belül kerül végrehajtásra. A Thread::start meghívásakor egy új szál jön létre, és meghívódik a futtatási metódus. Ezután visszakapcsoljuk a gyermekszálat a főszálhoz a Thread::join meghívásával, amely addig blokkol, amíg a gyermekszál végrehajtása be nem fejeződik. Ez biztosítja, hogy a feladat végrehajtása befejeződjön, mielőtt megpróbálnánk kinyomtatni az eredményt (amely a $task->response mappában van tárolva).

Előfordulhat, hogy nem kívánatos egy osztályt az áramlási logikához kapcsolódó további felelősségekkel szennyezni (beleértve a futtatási módszer meghatározásának felelősségét). Az ilyen osztályokat úgy tudjuk megkülönböztetni, hogy örököljük őket a Threaded osztálytól. Ezután egy másik szálon belül futtathatók:

Az osztályfeladat kiterjeszti a szálakat ( publikus $válasz; nyilvános függvény someWork() ( $content = file_get_contents("http://google.com"); preg_match("~ (.+) ~", $content, $matches); $ this->response = $egyezik; ) ) $task = new Task; $thread = new class($task) kiterjeszti Thread ( privát $feladat; public function __construct(Threaded $task) ( $this->task = $task; ) public function run() ( $this->task->someWork( ); ) ); $thread->start() && $thread->join(); var_dump($feladat->válasz);

Minden olyan osztály, amelyet külön szálban kell futtatni kellörökölni a Threaded osztályból. Ennek az az oka, hogy biztosítja a szükséges képességeket a különböző szálakon történő feldolgozáshoz, valamint implicit biztonságot és hasznos interfészeket (például erőforrás-szinkronizálást).

Vessünk egy pillantást a pthreads kiterjesztés által kínált osztályhierarchiára:

Menetes (átjárható, gyűjthető) Thread Worker Volatile Pool

A Thread és a Threaded osztályok alapjait már leírtuk és megtanultuk, most vessünk egy pillantást a másik háromra (Worker, Volatile és Pool).

Szálak újrafelhasználása

Egy új szál indítása minden párhuzamosítandó feladathoz meglehetősen költséges. Ennek az az oka, hogy a közös-semmi architektúrát kell megvalósítani a pthread-ekben, hogy a PHP-n belül többszálú legyen. Ez azt jelenti, hogy a PHP értelmező aktuális példányának teljes végrehajtási környezetét (beleértve minden osztályt, interfészt, tulajdonságot és függvényt) át kell másolni minden létrehozott szálhoz. Mivel ennek észrevehető hatása van a teljesítményre, az adatfolyamot mindig újra fel kell használni, amikor csak lehetséges. A szálak kétféleképpen használhatók fel újra: Workers vagy Pools használatával.

A Worker osztály számos feladat szinkron végrehajtására szolgál egy másik szálon belül. Ez úgy történik, hogy létrehoz egy új Worker-példányt (ami egy új szálat hoz létre), majd a feladatokat a különálló szál veremébe helyezi (a Worker::stack használatával).

Íme egy kis példa:

Az osztályfeladat kiterjeszti a szálakat ( privát $érték; nyilvános függvény __construct(int $i) ( $this->value = $i; ) public function run() ( usleep(250000); echo "Task: ($this->value) \n"; ) ) $dolgozó = new Dolgozó(); $dolgozó->start(); for ($i = 0; $i verem(new Task($i)); ) while ($worker->collect()); $dolgozó->leállítás();

A fenti példában 15 feladat egy új $worker objektumhoz kerül a verembe a Worker::stack metóduson keresztül, majd azokat a leküldés sorrendjében dolgozza fel. A fent látható Worker::collect metódus a feladatok azonnali megtisztítására szolgál, amint azok végrehajtása befejeződött. Ezzel egy while cikluson belül blokkoljuk a főszálat, amíg a veremben lévő összes feladat be nem fejeződik és törlődik – mielőtt meghívnánk a Worker::shutdown parancsot. Egy dolgozó korai leállítása (azaz amíg vannak még végrehajtandó feladatok) továbbra is blokkolja a főszálat mindaddig, amíg az összes feladat be nem fejezi a végrehajtását, csak hogy a feladatokat nem gyűjtik össze (ami memóriaszivárgást jelent).

A Worker osztály számos más módszert is kínál a feladatveremhez, beleértve a Worker::unstacket az utolsó halmozott feladat eltávolításához és a Worker::getStackedet a végrehajtási veremben lévő feladatok számának lekéréséhez. A dolgozói verem csak azokat a feladatokat tartalmazza, amelyeket végre kell hajtani. Ha egy feladat a veremben elkészült, azt eltávolítjuk, és egy külön (belső) verembe helyezzük szemétgyűjtés céljából (a Worker::collect módszerrel).

A szálak több feladaton keresztüli újrafelhasználásának másik módja egy szálkészlet használata (a Pool osztályon keresztül). A szálkészlet Workers csoportot használ a feladatok végrehajtásának lehetővé tételére egyidejűleg, amelyben a párhuzamossági tényező (az általa használt készletszálak száma) a készlet létrehozásakor van beállítva.

Alkalmazzuk a fenti példát a dolgozók csoportjának használatához:

Az osztályfeladat kiterjeszti a szálakat ( privát $érték; nyilvános függvény __construct(int $i) ( $this->value = $i; ) public function run() ( usleep(250000); echo "Task: ($this->value) \n"; ) ) $pool = new Pool(4); for ($i = 0; $i submit(new Task($i)); ) while ($pool->collect()); $pool->shutdown();

Van néhány figyelemreméltó különbség a pool és a dolgozó használatakor. Először is, a készletet nem kell manuálisan elindítani, hanem azonnal megkezdi a feladatok végrehajtását, amint elérhetővé válnak. Másodszor, mi Küld feladatokat a medencéhez, nem rakd fel őket egy veremre. Ezenkívül a Pool osztály nem örökli a Threaded osztályt, ezért nem adható át más szálaknak (ellentétben a Workerrel).

Hogyan jó gyakorlatok A dolgozók és a csoportok esetében mindig meg kell tisztítani a feladataikat, amint befejezték, majd maguknak kell leállítaniuk azokat. A Thread osztály használatával létrehozott szálakat is csatolni kell a szülőszálhoz.

pszálak és (im)változatlanság

Az utolsó osztály, amelyet érinteni fogunk, a Volatile, a pthreads v3 új kiegészítése. A megváltoztathatatlanság fontos fogalommá vált a pszálakban, mert enélkül a teljesítmény jelentősen csökken. Ezért alapértelmezés szerint a Threaded osztályok tulajdonságai, amelyek maguk is Threaded objektumok, immár megváltoztathatatlanok, és ezért nem írhatók felül a kezdeti hozzárendelésük után. Az ilyen tulajdonságok explicit mutációja jelenleg előnyben részesített, és továbbra is elérhető az új Volatile osztály használatával.

Nézzünk egy példát, amely bemutatja az új megváltoztathatatlansági korlátozásokat:

Az osztályfeladat kiterjeszti a Threaded // a Threaded osztályt ( nyilvános függvény __construct() ( $this->data = new Threaded(); // A $this->data nem írható felül, mivel egy szálas osztály Threaded tulajdonsága) ) $task = new class(new Task()) kiterjeszti a szálat ( // egy szálas osztály, mivel a szál kiterjeszti a szálas nyilvános függvényt __construct($tm) ( $this->threadedMember = $tm; var_dump($this->threadedMember-> data); // object(Threaded)#3 (0) () $this->threadedMember = new StdClass(); // érvénytelen, mivel a tulajdonság egy szálas osztály Threaded tagja ) );

A volatile osztályok szálas tulajdonságai viszont változtathatók:

Az osztályfeladat kiterjeszti a Volatile-t ( nyilvános függvény __construct() ( $this->data = new Threaded(); $this->data = new StdClass(); // érvényes, mivel volatile osztályban vagyunk ) ) $task = new class(new Task()) kiterjeszti a szálat ( nyilvános függvény __construct($vm) ( $this->volatileMember = $vm; var_dump($this->volatileMember->data); // object(stdClass)#4 (0) () // továbbra is érvénytelen, mivel a Volatile kiterjeszti a Threaded-et, tehát a tulajdonság még mindig egy Threaded osztály Threaded tagja $this->volatileMember = new StdClass(); ) );

Láthatjuk, hogy a Volatile osztály felülírja a szülő Threaded osztály által előírt megváltoztathatatlanságot, hogy lehetővé tegye a Threaded tulajdonságok (valamint az unset()) megváltoztatását.

Van még egy vita tárgya a variabilitás és a volatile osztály – tömbök témájával. A pthreadekben a tömbök automatikusan átkerülnek a volatile objektumokhoz, ha a Threaded osztály egy tulajdonságához vannak hozzárendelve. Ennek az az oka, hogy egyszerűen nem biztonságos több PHP kontextusból álló tömb manipulálása.

Nézzünk még egyszer egy példát, hogy jobban megértsünk néhány dolgot:

$tömb = ; $task = new class($tömb) kiterjeszti Thread ( privát $adat; nyilvános függvény __construct(array $tömb) ( $this->data = $tömb; ) public function run() ( $this->data = 4; $ this->data = 5; print_r($this->data); ) ); $feladat->start() && $feladat->join(); /* Kimenet: illékony objektum ( => 1 => 2 => 3 => 4 => 5) */

Látjuk, hogy az illékony objektumokat úgy lehet kezelni, mintha tömbök lennének, mert támogatják a tömbműveleteket, például (amint fentebb látható) a subset() operátor. A Volatile osztályok azonban nem támogatják az olyan alapvető tömbfüggvényeket, mint az array_pop és az array_shift. Ehelyett a Threaded osztály beépített metódusként biztosít számunkra ilyen műveleteket.

Bemutatóként:

$adat = új osztály kiterjeszti Volatile ( nyilvános $a = 1; nyilvános $b = 2; nyilvános $c = 3; ); var_dump($adat); var_dump($adat->pop()); var_dump($adat->shift()); var_dump($adat); /* Kimenet: object(class@anonymous)#1 (3) ( ["a"]=> int(1) ["b"]=> int(2) ["c"]=> int(3) ) int(3) int(1) object(class@anonymous)#1 (1) ( ["b"]=> int(2) ) */

Egyéb támogatott műveletek közé tartozik a Threaded::chunk és a Threaded::merge .

Szinkronizálás

A cikk utolsó részében a pthread-ekben történő szinkronizálást nézzük meg. A szinkronizálás egy olyan módszer, amely lehetővé teszi a megosztott erőforrásokhoz való hozzáférés szabályozását.

Például valósítsunk meg egy egyszerű számlálót:

$számláló = új osztály kiterjeszti a szálat ( public $i = 0; public function run() ( for ($i = 0; $i i; ) ) ); $számláló->start(); for ($i = 0; $i i; ) $counter->join(); var_dump($számláló->i); // 10 és 20 közötti számot nyomtat

Szinkronizálás nélkül a kimenet nem determinisztikus. Több szál ír ugyanarra a változóra ellenőrzött hozzáférés nélkül, ami azt jelenti, hogy a frissítések elvesznek.

Javítsuk ki, hogy az időzítés hozzáadásával a helyes 20-as kimenetet kapjuk:

$számláló = new class kiterjeszti Thread ( public $i = 0; public function run() ( $this->synchronized(function () ( for ($i = 0; $i i; ) )); ) ); $számláló->start(); $számláló->szinkronizált(függvény ($számláló) ( for ($i = 0; $i i; ) ), $számláló); $counter->join(); var_dump($számláló->i); // int(20)

A szinkronizált kódblokkok a Threaded::wait és Threaded::notify (vagy Threaded::notifyAll) metódusokkal is kommunikálhatnak egymással.

Itt van egy alternatív növekmény két szinkronizált while ciklusban:

$számláló = új osztály kiterjeszti Thread ( public $cond = 1; public function run() ( $this->synchronized(function ()) ( for ($i = 0; $i notify();); if ($this->cond === 1) ( $this->cond = 2; $this->wait(); ) ) )); ) ); $számláló->start(); $counter->synchronized(function ($counter) ( if ($counter->cond !== 2) ( $counter->wait(); // várja meg, míg a másik indul először ) for ($i = 10; $i notify(); if ($counter->cond === 2) ( $counter->cond = 1; $counter->wait(); ) ) ), $counter); $counter->join(); /* Kimenet: int(0) int(10) int(1) int(11) int(2) int(12) int(3) int(13) int(4) int(14) int(5) int( 15) int(6) int(16) int(7) int(17) int(8) int(18) int(9) int(19) */

További feltételeket is észlelhet a Threaded::wait hívás körül. Ezek a feltételek kritikusak , mert lehetővé teszik a szinkronizált visszahívás folytatását , ha értesítést kapott , és a megadott feltétel igaz . Ez azért fontos, mert az értesítések más helyekről is érkezhetnek, mint a Threaded::notify meghívásakor. Így ha a Threaded::wait metódus hívásai nem lennének feltételekkel zárva, akkor végrehajtjuk hamis ébresztők, ami kiszámíthatatlan kódviselkedéshez vezet.

Következtetés

Megnéztük a pthreads csomag öt osztályát (Threaded, Thread, Worker, Volatile és Pool) és az egyes osztályok használatát. Megvizsgáltuk a pthreadek megváltoztathatatlanságának új koncepcióját is, és rövid áttekintést adtunk a támogatott szinkronizálási lehetőségekről. Ha ezekkel az alapokkal a helyükön van, elkezdhetjük megvizsgálni, hogyan használhatók a pthreadek valós esetekben! Ez lesz a következő bejegyzésünk témája.

Ha érdekel a következő bejegyzés fordítása, jelezd: kommentelj a közösségi oldalakon. hálózatokon, szavazzon pozitívan, és ossza meg a bejegyzést kollégáival és barátaival.

  • Programozás,
  • Párhuzamos programozás
  • Nemrég kipróbáltam a pthread-eket, és kellemesen meglepődtem – ez egy olyan kiterjesztés, amely lehetővé teszi, hogy több valódi szálal dolgozzunk PHP-ben. Nincs emuláció, nincs varázslat, nincs hamisítvány – minden valódi.



    Ilyen feladaton gondolkodom. Van egy csomó feladat, amelyeket gyorsan kell elvégezni. A PHP-nek más eszközei is vannak a probléma megoldására, ezekről itt nincs szó, a cikk pthreadekről szól.



    Mik azok a pthreadek

    Ez minden! Nos, szinte mindent. Valójában van valami, ami felzaklathatja a kíváncsi olvasót. Ezek egyike sem működik az alapértelmezett beállításokkal fordított szabványos PHP-n. A többszálú használat élvezetéhez engedélyeznie kell a ZTS-t (Zend Thread Safety) a PHP-ben.

    PHP beállítás

    Ezután PHP ZTS-sel. Ne bánja a nagy különbséget a végrehajtási időben a ZTS nélküli PHP-hez képest (37,65 vs 265,05 másodperc), nem próbáltam általánosítani a PHP beállítását. ZTS nélküli esetben például engedélyeztem az XDebug-ot.


    Amint látható, 2 szál használatakor a program végrehajtási sebessége körülbelül 1,5-szer nagyobb, mint lineáris kód esetén. 4 szál használata esetén - 3 alkalommal.


    Megjegyzendő, hogy bár a processzor 8 magos, a program végrehajtási ideje 4-nél több szál használata esetén szinte változatlan maradt. Úgy tűnik, ez annak köszönhető, hogy a processzorom 4 fizikai maggal rendelkezik.Az érthetőség kedvéért a lemezt diagram formájában ábrázoltam.


    Összegzés

    A PHP-ben a pthreads kiterjesztéssel meglehetősen elegánsan lehet dolgozni a többszálú kezeléssel. Ez észrevehető termelékenységnövekedést eredményez.

    Címkék:

    • php
    • pszálak
    Címkék hozzáadása

    Nemrég kipróbáltam a pthread-eket, és kellemesen meglepődtem – ez egy olyan kiterjesztés, amely lehetővé teszi, hogy több valódi szálal dolgozzunk PHP-ben. Nincs emuláció, nincs varázslat, nincs hamisítvány – minden valódi.



    Ilyen feladaton gondolkodom. Van egy csomó feladat, amelyeket gyorsan kell elvégezni. A PHP-nek más eszközei is vannak a probléma megoldására, ezekről itt nincs szó, a cikk pthreadekről szól.



    Mik azok a pthreadek

    Ez minden! Nos, szinte mindent. Valójában van valami, ami felzaklathatja a kíváncsi olvasót. Ezek egyike sem működik az alapértelmezett beállításokkal fordított szabványos PHP-n. A többszálú használat élvezetéhez engedélyeznie kell a ZTS-t (Zend Thread Safety) a PHP-ben.

    PHP beállítás

    Ezután PHP ZTS-sel. Ne bánja a nagy különbséget a végrehajtási időben a ZTS nélküli PHP-hez képest (37,65 vs 265,05 másodperc), nem próbáltam általánosítani a PHP beállítását. ZTS nélküli esetben például engedélyeztem az XDebug-ot.


    Amint látható, 2 szál használatakor a program végrehajtási sebessége körülbelül 1,5-szer nagyobb, mint lineáris kód esetén. 4 szál használata esetén - 3 alkalommal.


    Megjegyzendő, hogy bár a processzor 8 magos, a program végrehajtási ideje 4-nél több szál használata esetén szinte változatlan maradt. Úgy tűnik, ez annak köszönhető, hogy a processzorom 4 fizikai maggal rendelkezik.Az érthetőség kedvéért a lemezt diagram formájában ábrázoltam.


    Összegzés

    A PHP-ben a pthreads kiterjesztéssel meglehetősen elegánsan lehet dolgozni a többszálú kezeléssel. Ez észrevehető termelékenységnövekedést eredményez.



    
    Top