Il buffer circolare (ring buffer, circular buffer) è frequentemente utilizzato in sistemi con memoria limitata, driver di dispositivo, reti e sistemi multitasking. Il suo concetto è stato utilizzato fin dai primi stadi dello sviluppo dei sistemi operativi, quando era importante utilizzare la memoria in modo ottimale e non sprecare risorse nello spostamento degli elementi dopo l'estrazione.
Le difficoltà tipiche sono la corretta gestione delle condizioni "buffer pieno" e "buffer vuoto", l'assenza di protezione contro la sovrascrittura dei dati, la sincronizzazione complicata in multithreading. Possono verificarsi errori a causa della confusione dei confini e del controllo scorretto degli indici.
Un oggetto buffer circolare è implementato utilizzando un array, due indici (head e tail) e, se necessario, una variabile contatore o logica aggiuntiva per distinguere tra gli stati "pieno" e "vuoto". La lettura e la scrittura avvengono in base al modulo della dimensione del buffer.
#define BUF_SIZE 8 char buffer[BUF_SIZE]; int head = 0, tail = 0; // head – scrittura, tail – lettura // Scrittura if (((head + 1) % BUF_SIZE) != tail) { buffer[head] = data; head = (head + 1) % BUF_SIZE; } else { // Buffer pieno } // Lettura if (head != tail) { char d = buffer[tail]; tail = (tail + 1) % BUF_SIZE; }
Caratteristiche chiave:
Come distinguere un buffer completamente pieno da uno completamente vuoto?
Il buffer vuoto viene spesso determinato da head == tail. Per quello pieno, si può lasciare una cella non occupata oppure memorizzare esplicitamente il numero di elementi in un contatore.
È possibile utilizzare il buffer in un ambiente multithreading senza blocchi?
No, nel caso standard, durante la lettura e la scrittura simultanee possono verificarsi race condition. È necessario utilizzare operazioni atomiche o blocchi.
Cosa succede se non si controlla l'overflow di head o tail?
In caso di overflow si verificherà un accesso al di fuori dei confini dell'array, provocando un comportamento indefinito e possibile corruzione della memoria.
Un sviluppatore ha implementato un buffer circolare in cui head == tail era interpretato sia come "vuoto" che "pieno" contemporaneamente, perdendo così il segnale di overflow.
Pro:
Contro:
Invece, è stata aggiunta una variabile contatore di elementi o è stato riservato uno slot per garantire che head non raggiungesse mai completamente tail.
Pro:
Contro: