C'est tout à fait normal. Tu te focalises sur le nombre d'éléments à produire et à consommer mais tu prends pas en compte le cycle de vie de tes threads.
Imaginons un cas simple: 2 consumers et 1 producer: tu peux arriver facilement à un deadlock provoqué par le semaphore sur le traitement des derniers éléments à consommer :
/* DERNIER ROUND */
Producer 0 waits semaphore
Producer 0 waits mutex
Producer 0 produces item n° 8192
Producer 0 released mutex
Consumer 2 waits semaphore
Consumer 1 waits mutex
Consumer 1 consumes item n° 8192
Consumer 1 released mutex
/* AH... PLUS DE PRODUCER POUR LIBÉRER CONSUMER 2?
*/
Dans le passage en gras, il ne reste qu'un élément à consommer, mais les deux threads étaient prêts à consommer un élément. L'ordonnanceur a privilégié le thread du consumer 1, il consomme tranquillement le dernier item pendant que son pote "Consumer 2" reste bloqué sur le semaphore qui ne sera jamais libéré.
Il te manque une gestion de fin de cycle pour que tes threads ne restent pas là à attendre en espérant avoir quelque chose à consommer alors que le bar est fermé
!
Je te conseille de mettre plus de logs quand tu travailles sur ce genre d'exercices de programmation concurrente. Ca te permet de comprendre qui est bloqué, où et pourquoi. Limite le nombre de threads pour éviter les casse-têtes!
A+