Um semáforo eh um objeto com um valor inteiro que contem 2 rotinas de manipulação:

  • sem_wait()
  • sem_post()
#include <semaphore.h>

sem_t s;
sem_init(&s, 0, 1);

Semáforo como lock

so utilizar um wait previamente a sessão critica e liberá-lo após a região critica. Iniciar o semáforo com 1.

Semáforo para ordenação

podemos usar um semáforo como uma primitiva de ordenação, ou seja, colocamos uma thread em espera e chamamos uma função associada a ela. Mandamos um sem_wait() e fazemos o o codigo somente terminar quando zerar o valor de sem_init(t, 0, 0) com X iterações nas threads filhas chamadas, ou seja, quando o recurso do filho é criado.

1 sem_t s;
2
3 void *child(void *arg) {
4 printf("child\n");
5 sem_post(&s); // signal here: child is done
6 return NULL;
7 }
8
9 int main(int argc, char *argv[]) {
10 sem_init(&s, 0, 0);
11 printf("parent: begin\n");
12 pthread_t c;
13 Pthread_create(&c, NULL, child, NULL);
14 sem_wait(&s); // wait here for child
15 printf("parent: end\n");
16 return 0;
17 }

Produtor/Consumidor (Bounded-Buffer Problem)

Esse problema é muito comum. Existem 2 threads, uma que produz dados, outra que consome. Pense nisso como um buffer compartilhado, onde um escreve e o outro le, mas no qual cada um deles limita essa quantidade, ou seja, produção e consumo são limitados, entende-se, então, que o buffer também o é.

Existem, portanto, algumas rotinas e variáveis necessárias.

  • buffer
  • full // quão cheio está o buffer
  • use // quantos elementos estão em uso
  • MAX // máximo do BUFFER
  • sem_t vazio
  • sem_t cheio
void *producer(void *arg) {
	int i;
	for (i = 0; i < loops; i++) {
		sem_wait(&empty); // Line P1
		put(i); // Line P2
		sem_post(&full); // Line P3
	}
}

void *consumer(void *arg) {
	int tmp = 0;
	while (tmp != -1) {
		sem_wait(&full); // Line C1
		tmp = get(); // Line C2
		sem_post(&empty); // Line C3
		printf("%d\n", tmp);
	}
}

int main(int argc, char *argv[]) {
	// ...
	sem_init(&empty, 0, MAX); // MAX are empty
	sem_init(&full, 0, 0); // 0 are full
	// ...
}

Neste caso o resultado já é alcançado, já que o consumidor entra em queue de espera no caso do produtor estar produzindo algo, e vice versa para o produtor. Entretanto, só funcionaria para apenas 1 produtor e 1 consumidor.

Adicionando Exclusão Mútua

void *producer(void *arg) {
	int i;
	for (i = 0; i < loops; i++) {
		sem_wait(&empty);
		sem_wait(&mutex); // (NEW LINE)
		put(i); // Line P2
		sem_post(&mutex); // (NEW LINE)
		sem_post(&full);
	}
}

void *consumer(void *arg) {
	int i;
	for (i = 0; i < loops; i++) {
		sem_wait(&full);
		sem_wait(&mutex); // (NEW LINE)
		int tmp = get(); 
		sem_post(&mutex); //  (NEW LINE)
		sem_post(&empty);
		printf("%d\n", tmp);
	}
}

Precisamos adicionar exclusão mútua na alteração dos limites do buffer, ou seja, sempre que full e use forem ser alterados ou lidos, precisam ser guardados por um mutex lock.

Reader-Writer Locks

Esse tipo de Lock se baseia na ideia de um Escritor constante, e um leitor constante. Ou seja, nenhum deles efetivamente faz outra função ou depende um do outro, apenas escrevem ou leem, independentemente.

Isso quer dizer que precisamos garantir, apenas, que um leitor está consegue ler a ultima escrita, ou seja, não pode haver escrita enquanto o leitor lê.

Precisamos então permitir apenas um escritor por vez e fazer o primeiro leitor pegar o lock de escrita, também. Ou seja, se tiver alguém lendo, que seja apenas um ou mais leitores, ninguém poderá escrever e entrará na queue de espera.

Serão necessários apenas:

  • struct rwlock_t
  • rwlock_init()
  • rwlock_acquire_writelock()
  • rwlock_release_writelock()
  • rwlock_acquire_readlock()
  • rwlock_release_readlock()

Thread Throttling

Podemos usar um semaforo que limita a quantidade de threads que fazem a mesma coisa, não apenas para memória compartilhada, mas muitas threads poderiam também, até atrasar o processamento.

Digamos que muitas threads usem 500mb de memória individualmente. Se tivermos 10 threads executando a mesma rotina, já teríamos 5GB de memória usada, o que torna 100 threads algo completamente inviável. Limitando com um semáforo, impedimos esse crescimento de memória.

Esse approach se chama Throttling.

Como implementar semaforos

typedef struct __Zem_t {
	int value;
	pthread_cont_t cond;
	pthread_mutex_t lock;
} Zem_t

// only one thread can call this
void Zem_init (Zem_t *s, int value) {
	s->valeu = value;
	Cond_init(&s->cond);
	Mutex_init(&s->lock);
}

void Zem_wait(Zem_t *s) {
	Mutex_lock(&s->lock);
	while (s->value <= 0) 
		Cond_wait(&s->cond, &s->lock);
	s->value--;
	Mutex_unlock(&s->lock);
}

void Zem_post(Zem_t *s) {
	Mutex_lock(&s->lock);
	s->value++;
	Cond_signal(&s->cond);
	Mutex_unlock(&s->lock);
}