thread

Os primeiros exercícios são relacionados ao comportamento de Threads, também conhecidas como processos leves. O epos será o sistema operacional utilizado nesta disciplina onde os trabalhos requisitados são propostas de soluções para problema inseridos no S.O. A seguir é mostrado os diagramas de classe e de estado da thread inicial.

diagrama de classe

diagrama de estados

O famigerado delete

Neste problema é perguntado o que aconteceria se o desenvolvedor "deletar" uma thread e depois dar um join() na mesma. E, claro, propor uma solução para o mesmo.

O snipet a seguir ilustra o nosso teste. Com este teste, embora tenhamos executado uma ação destrutiva, todos as outras threads executaram normalmente com exceção da thread que deu o join(). Infelizmente, neste caso, quem fez esta operação foi a thread main. Esta, ficou eternamente esperando a finalização do filósofo 0, que deu o calote, pendurando, assim, a aplicação.

...
delete phil[0];
for(int i = 0; i < 5; i++) {
   int ret = phil[i]->join();
   ...	

Em uma abordagem mais formal, a main inicia a aplicação, cria todas as threads, deleta o phil[z] e pede um join(). O processador executa o método join, sendo que a main ainda detém o processador. Dentro do join é verificado o status da thread, no caso phil[0] que é um lixo pois o objeto foi destruido. Portanto como não é FINISHING, o processador "trigga" o yield() fazendo a main liberar o processador. Neste momento, a main perde o processador e as outras threads vão executar, depois que todas executam, começa o retorno dos joins que, quando chega na vez do processo "deletado" a aplicação pendura.

Este problema é decorrente do fato do método join ser busy waiting, assim uma solução seria fazer o metodo join ser idle waiting, o que solucionaria o problema. O problema do busy waiting é o nosso segundo exercício, o que deixa o nosso primeiro apenas na proposta sem poder demonstrá-lo antes do segundo. Para não deixar sem uma resposta avaliável, a solução será alcançada ao modificar o destrutor da Thread.

dorminhôco

A espera ocupada é caracterizada quando uma thread que está esperando uma condição continua a concorrer pelo processador, onde, volta e meia, a thread que está esperando pergunta se a condição foi atingida. Este fato, embora se tratando de threads, implica em diminuição de performance. Uma solução para a busy waiting é o chamado idle waiting, onde a thread que está esperando - ela dorme - aguarda por uma sinalização quando a condição for satisfeita.

Quando falamos de idle waiting, a partir do momento em que a thread está em estado de espera, ela só volta a concorrer pelo processador apenas quando ela é sinalizada. Assim, a nossa proposta de solução é a criação de uma fila de espera, fila esta como um atributo de objeto. E o métodos de espera e sinalização. Para isto, a thread precisa de mais um estado: WAITING. O snipets abaixo mostram as modificação feitas no código.

A solução proposta é adicionar um novo estado: "WAITING" juntamente com uma fila para esta condição: "_waiting". Isto permite a modificação do método join():

...
if(_state != FINISHING)<--- pergunta apena uma vez, e só acorda quando sinalizado
   Thread::wait(&_waiting); <--- método e classe que pede um ponteiro como parâmetro.
Neste caso, passamos a referência de um atributo de objeto, a nossa fila _waiting
...

Assim, a thread que está esperando passa a não mais requerir o processador, o que exige uma notificação para sair deste estado. Esta notificação é feita por uma thread quando esta termina ou quando ela é deletada( solução do primeiro problema ). Para isto é necessário modificar ~Thread() e exit(int status).

void Thread::exit(int status)
{
   ...

   if(Traits::active_scheduler)
      CPU::int_disable();

   Thread::notifyAll(&(_running->_waiting)); <--- método de classe que recebe como parâmetro 
a referência da fila _waiting da thread que está em estado running.
if(_ready.empty() && !_suspended.empty()) idle(); // implicitly re-enables interrupts ... }
~Thread() {
   Thread::notifyAll(&_waiting)); <---
   _ready.remove(this);
   _suspended.remove(this);
   free(_stack);
}

A questão é que, quando uma thread acaba o processamento, ela sinaliza todas as threads que estavam esperando por ela. Esta abordagem também é considerada no problema do delete.

A seguir é mostrado a implementação dos métodos wait() e notify() e notifyAll.

void Thread::wait(Queue * _waitingQueue) {
   db(TRC) << "Thrad::wait()\n";

   if(Traits::active_scheduler)
      CPU::int_disable();

   if(!_ready.empty()) {
       Thread * old = _running;
       old->_state = WAITING;
       //--
       _waitingQueue->insert(&old->_link);
       
       _running = _ready.remove()->object();
       _running->_state = RUNNING;

       db(INF) << "old={" << old << ","
                       << *old->_context << "}\n";
       db(INF) << "new={" << _running << ","
                       << *_running->_context << "}\n";

       CPU::switch_context(&old->_context, _running->_context);
   }
   
   if(Traits::active_scheduler)
       CPU::int_enable();
}

void Thread::notify(Queue * _waitingQueue){
   db(TRC) << "Thread::notify()\n";

   if(Traits::active_scheduler)
      CPU::int_disable();

   if(!_waitingQueue->empty()){
      Thread * notified = _waitingQueue->remove()->object();
      notified->_state = READY;
      _ready.insert(¬ified->_link);
   }

   if(Traits::active_scheduler)
      CPU::int_enable();
}

void Thread::notifyAll(Queue * _waitingQueue){
   db(TRC) << "Thread::notify()\n";

   if(Traits::active_scheduler)
      CPU::int_disable();

   while(!_waitingQueue->empty()){
      Thread * notified = _waitingQueue->remove()->object();
      notified->_state = READY;
      _ready.insert(¬ified->_link);
   }

   if(Traits::active_scheduler)
      CPU::int_enable();
}

Para a ralização do experimento foi realizado o seguinte teste:
Mantendo o join como busy waiting, e adicionando um log de debug no laço de espera,

OStream cout; <---
while(_state != FINISHING){
   yield(); <--- busy waiting
   cout << "busy\n"; <---
}
obtemos o seguinte resultado:
The Philosopher's Dinner:
Philosophers are alife and hungry!
The dinner is served!
/\|/\waitiing phil[0]
phil[0] -> thinking
phil[1] -> thinking
phil[2] -> thinking
phil[3] -> thinking
phil[4] -> thinking
busy <---
phil[0] ->  eating 
phil[1] ->  eating 
phil[2] ->  eating 
phil[3] ->  eating 
phil[4] ->  eating 
busy <---
phil[0] -> thinking
phil[1] -> thinking
phil[2] -> thinking
phil[3] -> thinking
phil[4] -> thinking
busy <---
phil[0] ->  eating 
phil[1] ->  eating 
phil[2] ->  eating 
phil[3] ->  eating 
phil[4] ->  eating 
busy <---
...
Ao imlementar a nossa proposta, adicionando, também, o log de debug:
OStream cout; <---
while(_state != FINISHING){
   wait(); <--- idle waiting
   cout << "busy\n"; <---
}
obtemos a seguinte resposta:
The Philosopher's Dinner:
Philosophers are alife and hungry!
The dinner is served!
/\|/\waitiing phil[0]
phil[0] -> thinking
phil[1] -> thinking
phil[2] -> thinking
phil[3] -> thinking
phil[4] -> thinking
phil[0] ->  eating 
phil[1] ->  eating 
phil[2] ->  eating 
phil[3] ->  eating 
phil[4] ->  eating 
phil[0] -> thinking
phil[1] -> thinking
phil[2] -> thinking
phil[3] -> thinking
phil[4] -> thinking
phil[0] ->  eating 
phil[1] ->  eating 
phil[2] ->  eating 
phil[3] ->  eating 
phil[4] ->  eating 
...
phil[3] ->  eating 
phil[4] ->  eating 
busy <---
Philosopher 0 ate 10 times 
waitiing phil[1]
Philosopher 1 ate 10 times 
waitiing phil[2]
Philosopher 2 ate 10 times 
waitiing phil[3]
Philosopher 3 ate 10 times 
waitiing phil[4]
Philosopher 4 ate 10 times 
The end!

Este resultado, além de demonstrar que o idle waiting está funcionando, nos permite deduzir que não precisamos do laço para testes. Apenas um condiional é necessário.

Atualizando os diagramas:

diagrama de classe

diagrama de estados

sinaleira

<--- modificar!!! Neste problema, primeiro é proposto para retirarmos a função que decrementa do loop.

em semaphore.h:

antes:
void p() { // proberen 
   while(dec(_value) < 0)
      sleep();
}

depois:
void p() { 
   int value = dec(_value); <---
   while(value < 0)
      sleep();
}

No teste realizado não foi possível notar nenhuma diferença no resultado.

O segundo exercíco é modificar o código para que a espera seja do tipo idle waiting e implementar a sinalização wakeup. A primeira solução considerada foi chamar os métodos da thread que trata de esperas: wait() e notify(). O problema é que nem wait() nem notify() são métodos de classe, não podendo ser chamados deste jeito. Uma possibilidade seria tornar estes métodos em estático, o que seria complicado, já que existe código dependente destes métodos sendo não-estáticos.
Outra solução, a qual adotamos, foi criar métodos de classe para solucionar este problema, acompanhado por uma fila como atributo de classe.

void v() { // verhoegen
   if(inc(_value) < 1)
      wakeup();
}
em synchronizer.h:
...
protected:
   // Atomic operations
   bool tsl(volatile bool & lock) { return CPU::tsl(lock); }
   int inc(volatile int & number) { return CPU::finc(number); }
   int dec(volatile int & number) { return CPU::fdec(number); }

   // Thread operations
   void sleep() {
      if(!busy_waiting) 
         Thread::sleep(); <---
   }
   
   void wakeup() {
      if(!busy_waiting) 
         Thread::wakeup(); <---
   }
   
   void wakeup_all() {
      if(!busy_waiting) 
       Thread::wakeup_all(); <---
   }
...
Assim, foi acrescentado ao código os métodos estáticos: sleep, wakeup, wakeup_all; o estado BLOCKED; e a fila estática _blocked; A seguir é mostrado o código referente às mudanças.
thread.h:
...
enum  {
 RUNNING,
 READY,
 WAITING,
 SUSPENDED,
 BLOCKED,
 FINISHING
};
...
static void sleep();
static void wakeup();
static void wakeup_all();
...
static Queue _blocked;
...

thread.cc:
...
void Thread::sleep() {

   if(Traits::active_scheduler)
      CPU::int_disable();

   if(!_ready.empty()) {
       Thread * old = _running;
       old->_state = BLOCKED;
       
       _blocked.insert(&old->_link);
       
       _running = _ready.remove()->object();
       _running->_state = RUNNING;

       CPU::switch_context(&old->_context, _running->_context);
   }
   
   if(Traits::active_scheduler)
       CPU::int_enable();
}

void Thread::wakeup(){
   if(Traits::active_scheduler)
      CPU::int_disable();

   if(!_blocked.empty()){
      Thread * signaled = _blocked.remove()->object();
      signaled->_state = READY;
      _ready.insert(&signaled->_link);
   }

   if(Traits::active_scheduler)
      CPU::int_enable();
}

void Thread::wakeup_all(){
   if(Traits::active_scheduler)
      CPU::int_disable();

   while(!_blocked.empty()){
      Thread * signaled = _blocked.remove()->object();
      signaled->_state = READY;
      _ready.insert(&signaled->_link);
   }

   if(Traits::active_scheduler)
      CPU::int_enable();
}
...    
Nenhuma das modificações acima pode produzir algum efeito sobre a percepção do resultado da aplicação. Mesmo mudando wakeup por wakeup_all.

alarm

Este problema trata do alarm. Ou temporizador relativo. Neste problema é solicitado a modificação, que antes era busy waiting, para idle waiting. E utilizar como tratador do alarm o semáforo. A nossa modificação consiste em declarar um semáforo, iniciado em "0". Utilizar a função proberen do semáforo para que este fique em uma fila de espera até que o tratador o sinalize com a função verhoegen, o que caracteriza o idle waiting. O semáforo é inicialzado em "0" para que, na primeira passagem, a thread é barrada.

em alarm.h:

class Alarm
{
private:
   ...
   Semaphore * semaphore(0); <---
   ...
   
em alarm.cc:

antes:
void Alarm::delay(const Microseconds & time)
{
   Tick t = _elapsed + time / period();
   while(_elapsed < t);
}
depois:

void Alarm::delay(const Microseconds & time)
{
   Tick t = _elapsed + time / period();
   if(_elapsed < t) <---
  	   semaphore.p(); <---  Na primeira passagem, a thread é suspensa.
}
void Alarm::timer_handler(void)
{
   static Tick next;
   static Handler handler;

   _elapsed++;
    
   ...

   if(next)
      next--;
   if(!next) {
      if(handler)
         handler();
      if(_requests.empty())
         handler = 0;
      else {
         Alarm * alarm = _requests.remove()->object();
         next = alarm->_ticks;
         handler = alarm->_handler;
         if(alarm->_times != -1)
            alarm->_times--;
         if(alarm->_times) 
            _requests.insert(&alarm->_link, alarm->_ticks);
      }
      semaphore.v(); <---  sinaliza a thread que estava esperando.
   }
}

VÉÉÉÉI É TCP/IPÊÊÊÊÊ VÉI