Elöl állsz végre
egy sorban? Lendületből
előz a másik.
Érdekes túlkínálati válság alakult ki Kínában a 90-es években. A termelés hihetetlen iramban nőtt, a fizetőképes kereslet is jelentősen növekedett, egyetlen dolog hiányzott ahhoz hogy a belső piac hatalmas ütemben bővülni kezdjen. A szabadidő. Ugyanis a feszített munkatempó, a hatnapos munkahét és hivatalos ünnepek hiánya miatt a dolgozók nem tudták elkölteni amúgy szépen gyarapodó megtakarításaikat. A párt persze lépett és 1995-ben bevezették az ötnapos munkahetet, majd 2000-ben az arany hetek néven ismert 3 egyhetes nemzeti ünnepet.
No valami hasonló állapotot sikerült előállítani nemrég egy rosszul sikerült consumer/producer implementációval nem olyan régen.
Az implementáció 3 buffert használt ami körkörösen körbe voltak láncolva, az egyik szál töltögette őket sorjában, a másik pedig ürítette (volna) őket egymás után. Minden buffernek saját szemaforja van, amit producer és consumer szál is kizárólagosan lefoglal az töltés/ürítés idejére. Melékelem a kódrészleteket is a könnyebbség kedvéért.
#include <stdlib.h>
Na mármost, a rendszer kiválóan működött egészen addig míg a consumer és producer száll párhuzamosan tudott futni és a consumer szál a szemafor miatt várakozásra nem kényszerült. Akkor pihenőpályára került a lezárt szemafor miatt, a producer szál pedig vidáman töltögette a buffert. Az elvárt működés szerint a producer szál a buffer megteltével átugrik a következő bufferre, a consumer szál pedig felébred csipkerózsika álmából és szépen elkezdené fogyasztani az éppen befejezett buffert. Igenám, csakhogy a meglehetősen gyors töltődés miatt, mire a consumer szál hármasban öblögetve kivánszorgott a boxutcából és lefoglalta volna a frissen elengedett szemafort, a nagy lendülettel köröző producer szál egy teljes kör megtétele után a rázókövön előzve újra lefoglalta a nemrég általa elengedett szemafort. Következésképpen teleírta a még ki nem ürített buffert, aminek persze elszállás lett a vége. Levonható a tanulság ami a POSIX szabvány ismeretében kézenfekvő, de némiképp ellenkezik a mindennapi tapasztalatainkkal, tehát a kezdő simán megszívja.
#include <semaphore.h>
#include <pthread.h>
pthread_t read_from_buffer_thread;
typedef struct buffer
{
sem_t semaphore;
char* data;
size_t offset;
struct buffer* nextBuffer;
} buffer;
#define NUMBER_OF_BUFFERS 3
buffer buffers[NUMBER_OF_BUFFERS];
buffer* currentBuffer = NULL;
void write_to_buffer(char* data, size_t size)
{
if((currentBuffer->offset + size) > BUFFER_MAX_SIZE || file != currentBuffer->file)
{
// switch buffer
sem_post(&(currentBuffer->semaphore));
sem_wait(&(currentBuffer->nextBuffer->semaphore));
currentBuffer = currentBuffer->nextBuffer;
}
fill_buffer_with_data(currentbuffer, data, size);
}
void* read_from_buffer_thread_function(void* parameters)
{
buffer* buffer = &(buffers[0]);
while(active)
{
sem_wait(&(buffer->semaphore));
extract_data_from_buffer(buffer->data, buffer->offset);
buffer->offset = 0;
sem_post(&(buffer->semaphore));
buffer = buffer->nextBuffer;
}
}
void init_buffers(size_t bufferMaxSize)
{
int i = 0;
for(i = 0; i < NUMBER_OF_BUFFERS; i++)
{
currentBuffer = &(buffers[i]);
sem_init(&(currentBuffer->semaphore), 0, 1);
currentBuffer->offset = 0;
currentBuffer->nextBuffer = ( i < NUMBER_OF_BUFFERS-1 ? &(buffers[i+1]) : &(buffers[0]));
}
currentBuffer = &(buffers[0]);
sem_wait(&(currentBuffer->semaphore));
}
#define N 1048576
int main()
{
init_buffers();
active = 1;
pthread_create(&read_from_buffer_thread, NULL, read_from_buffer_thread_function, NULL);
while(active)
{
write_to_buffer(data, N);
}
}
Amikor a szemafort/lockot/mutexet elengedi egy szál, mindössze annyi történik, hogy a többi szál, aki erre várt, futtatható állapotba kerül. Hogy közülük melyik lesz az aki a valóban futni fog, azt a kernel scheduler mondja meg egy (a user által nem teljesen megjósolható eredményű) algoritmus alapján. Tehát a fejlesztő aki arra számít, hogy FIFO módon, az a szál foglalja el a szemafort, amelyik a legrégebben vár rá, úgy pofára esik mint a székely bácsi, aki felajzva beront a NŐK feliratú ajtón, aztán odabent nő egy szál se, csak a WC.
A korrekt megoldás a jelelegi struktúrában, ha a szemafort mutexekre cseréljük és egy feltételes várakozást állítunk be az offset null illetve nem null értkére. Ez híven tükrözi a valóságot, miszerint az producer szál nem egyszserűen bufferre, hanem ÜRES bufferre, a consumer szál pedig TELE bufferre várakozik. Érdekes még, hogy kis furmánnyal a jelenlegi állapot úgy is javítható, hogy az olvasó szál addig nem engedi el az üres buffert, míg nem kap cserébe egy telit.
A wiki oldalán különben elég jó általános leírás olvasható a klasszikus szinkronizálási problémákról. Ha pedig egy kicsit mélyebb elemzést szeretnél klasszikus, kevésbé klasszikus, nem annyira klasszikus és távolról sem klasszikus szinkronizálási problémákról akkor olvasd inkábbAllen B. Downey: The Little Book of Semaphores című ingyenesen letölthető könyvét.