Problém producenta a spotřebitele

Problém producenta a spotřebitele je jedním z klasických příkladů problémů programování souběžných procesů. Producent vyrábí určitá data a dává je do zásobníku o omezené velikosti. Spotřebitel je zase ze zásobníku odebírá. Producent nemůže přidávat data, pokud je zásobník plný, a spotřebitel nemůže odebírat data, pokud v zásobníku žádná data nejsou. Problém se řeší tak, že se producent uspí, jakmile se zásobník naplní, spotřebitel ho pak probudí, jakmile odebere data ze zásobníku. Stejně tak se postupuje u spotřebitele, když je zásobník prázdný. Při nevhodném řešení může nastat stav, kdy oba usnou a už se nikdy nevzbudí. Dojde tak k deadlocku.

Proč používat vzor producenta a spotřebitele

Vzor producenta a spotřebitele umožňuje lépe souběžně běžet cyklickým procesům, které pracují v různých rychlostech. Díky použití zásobníku jako úložného prostoru může producent vytvářet data nezávisle na rychlosti spotřebitele. Pokud je možnost tvorby dat rychlá, může producent tyto data vytvářet napřed a spotřebitel je pak bude zpracovávat ve svém vlastním tempu.Je to obzvláště vhodné například při komunikaci po síti, kdy fungují dva procesy v různých rychlostech. První proces může neustále přijímat pakety ze sítě, zatímco druhý přijaté pakety analyzuje. V tomto příkladě je první proces producentem a druhý spotřebitelem. S použitím zásobníku mohou být pakety přijaté rychleji, než jsou analyzovány. Komunikace je tak velmi efektivní a minimalizuje ztrátu dat.

Zde je ukázka kódu, ve kterém může dojít k deadlocku:

int pocetPolozek = 0;
producent() {    while (true) {        polozka = vytvoritPolozku();        if (pocetPolozek == VEL_ZASOBNIKU) {            spat();        }        vlozPolozkuDoZasobniku(polozka);        pocetPolozek = pocetPolozek + 1;        if (pocetPolozek == 1) {            vzbudit(spotrebitel);        }    }}
spotrebitel() {    while (true) {       if (pocetPolozek == 0) {           spat();       }              polozka = vyjmoutPolozkuZeZasobniku();       pocetPolozek = pocetPolozek – 1;              if (pocetPolozek == VEL_ZASOBNIKU - 1) {           vzbudit(producent);       }       spotrebujPolozku(polozka);    }}


Za běžných okolností nedojde k žádnému problému. Rozběhnou se oba procesy, spotřebitel, pokud je počet položek nulový, usne. Když běží producent a zásobník není plný, vloží do něj položku, a probudí spotřebitele. A tak vše běží dál.Problém nastává v této situaci: spotřebitel zjistí, že v zásobníku nejsou žádné položky. Chystá se usnout. Pokud se v tu chvíli ale dostane na řadu producent, který přidá položku a zavolá signál probuzení spotřebitele, spotřebitel ji nepřijme, protože ještě nespí. Pak usne, po krátké době producent naplní zásobník a usne také. Nikdy se však už ani jeden neprobudí.Problém se dá vyřešit pomocí semaforů. Semafor je speciální celočíselný datový typ, asociovaný se dvěma funkcemi:

  1. signal() – jeho zavoláním se celočíselná hodnota semaforu zvedne o 1, nahrazuje volání funkce vzbudit()
  2. cekat() – zavolani funkce cekat() sníží hodnotu semaforu o 1; pokud je hodnota semaforu již před zavoláním 0, nahrazuje toto volání funkci spát(), dokud někdo nezavolá signal() a nezvýší tím hodnotu semaforu o 1

Obě funkce signal() a cekat() jsou nedělitelné atomické operace.Použitím semaforu nahradíme proměnnou pocetPolozek. Zde je kód, který problém řeší vhodným způsobem:

int volne = VEL_ZASOBNIKU;  // semafor, počet volných slotů v zásobníkuint obsazene = 0;// semafor, počet obsazených slotů v zásobníku
producent() {       while (TRUE) {               polozka = vytvoritPolozku();               cekat(volne);               vlozPolozkuDoZasobniku(polozka);               signal(obsazene);       }}
spotrebitel() {        while (TRUE) {                cekat(obsazene);                polozka = vyjmoutPolozkuZeZasobniku();                signal(volne);                spotrebujPolozku(polozka);        }}

Celočíselné semafory zde vlastně fungují jako zásobníky volání na probuzení, takže se žádný z nich neztratí.

Související články

Externí odkazy