Programación multifío
Sumario
Programación de fíos
A execución dun proceso comenza cun único fío pero se poden crear máis sobre a marcha. Os distintos fíos dun mesmo proceso comparten:
- O espazo de memoria asignado ó proceso.
- A información de acceso a ficheiros. Estos se utilizan non só para almacenar datos, se non tamén para controlar dispositivos de entrada e saída (E/S).
En cambio, cada fío ten os seus propios valores para:
- Os rexistros do procesador.
- O estado da súa pila (stack). Na pila gárdase información acerca das chamadas en curso de execución a métodos de diversos obxectos. Para cada chamada gardanse entre outras cousas os datos locales (en variables internas do método).
O intercambio de información entre fíos é sinxelo, dado que os distintos fíos dun mesmo proceso comparten a memoria asignada ó proceso polo sistema operativo. Pero os distintos fíos dun mesmo proceso deben coordinarse para ó acceso os contidos da memoria e ós ficheiros que se utilizan para controlar as operacións de E/S.
Utilidade dos fíos
Podense utilizar fíos en situacións moi diversas, coma por exemplo:
- Programas servidores que proporcionan servizos a outros procesos. Por exemplo un servidor web ou de bases de datos. Cada petición ó servidor pódese atender nun fío distinto. Desa xeito podense atender simultáneamente moitas peticións de servizo mellorando o tempo de resposta para elas. Máis adiante aprenderase a programar servidores multifío que prestan servizos a oturos procesos a través de protocolos estándares de rede, e que crean sobre a marcha novos fíos para atender novas peticións.
- Programas variados que amosan unha interfaz gráfica de usuario mentres realizan procesos en segundo plano. Por exemplo un procesador de texto ou unha folla de cálculo. Neste exemplo, un fío podería responder ás acciones do usuario sobre a interfaz mentres outro podería recalcular os valores que cambian en calquera celda como consecuencia do cambio no valor dunaha celda, e volver a mostrar os novos valores; e outro finalmente podería ocuparse de gardar o documento periódicamente.
- Xogos nos que intervieñen moitos elementos que interactúan entre eles, e que poden ser controlados polos usuarios. En lugar dun único fío que controle todos os elementos do xogo, podente ter distintos fíos para distintos elementos do xogo.
- Programas de control en tempo real. Estos controlan un sistema mediante dispositivos sensores e actuadores. Obteñen información do estado do sistema a través de dispositivos sensores, e actúan sobre él mediante dispositivos actuadores. Un exemplo sería un programa de control do acceso a un aparcadoiro no que debe detectar a chegada de vehículos mediante determinados sensores, e debe actuar sobre o sistema amosando información para os conductores nunha pantalla, facendo que se xeren os tickets, e levantando e baixando unha barrera en momentos determinados. Os múltiples dispositivos que interveñen neste proceso podense controlar mediante distintos fíos que se comunican e coordinan entre sí.
Os fíos son máis frecuentes do que cabe pensar; todos os programas con interfaz gráfico son multifio porque os eventos e as rutinas de pintado das fiestras corren nun fío distinto ó principal. Por exemplo, en Java, as bibliotecas gráficas AWT ou Swing usan fíos.
Conceptos sobre fíos
Un fío, tamén chamado subproceso ou proceso lixeiro, é un fluxo de control secuencial independente dentro dun proceso e está asociado cunha secuencia de instruccións, un conxunto de rexistros e unha pila.
Cando se executa un programa, o sistema operativo crea un proceso e tamén crea o seu primeiro fío (o fío primario), quen pode a súa vez crear fíos adicionais.
Porén, poderiamos chegar a dicir que un proceso non se ejecuta, xa que só é o espazo de direccións onde reside o código que é executado mediante un ou máis fíos.
Caben as siguintes observacións:
- Un fío non pode existir independentemente dun proceso
- Un fío non pode executarse por si solo.
- Dentro de cada proceso pode haber varios fíos executándose.
Un único fío é similar a un programa secuencial; por si mesmo non oferta nada novo; o novedose acontece coa a habilidade de executar varios fíos dentro dun proceso de xeito que cada un destos fíos pode executar actividades diferentes ó mesmo tempo.
Recursos compartidos polos fíos
Un fío leva asociados os siguientes elementos:
- Un identificador único
- Un contador de programa propio
- Un conxunto de rexistros
- Unha pila (variables locales)
Un fío pode compartir con outros fíos do mesmo proceso os seguientes recursos:
- Código
- Datos (coma variables globales)
- Outros recursos do sistema operativo, coma os ficheiros abertos ou os sinais
Coma os fíos compartan recursos (por exemplo, puidendo acceder as mesmas variables) é preciso utilizar esquemas de bloqueo e sincronización. Isto pode dificultar o desenvolvemento de programas e a súa depuración.
Na sincronización de fíos e onde reside a arte de programar con fíos; de errar podemos crear aplicacións ineficientes ou con resultados errónes. Para facelo ben é preciso coñecer os mecanismos de sincronización, comunicación e compartición de recursos entre fíos.
Ventaxas do uso de fíos
Os fíos aportan as seguintes ventaxas sobre os procesos coma consecuencia de compartir o espazo de memoria:
- Consúmense menos recursos no lanzamiento e na execución dun fío que no caso dun proceso
- Tárdase moito menos tempo en crear e terminar un fío que un proceso
- A conmutación entre fíos do mesmo proceso ou cambio de contexto é bastante máis rápida que entre procesos
Por iso os fíos se lles denomina procesos lixeiros. Con todo, cando é aconsellabe usar fíos? Cando:
- A aplicación manexa entradas de varios dispositivos de comunicación
- A aplicación debe poder realizar diferentes tarefas á vez
- Interesa diferenciar tarefas cunha prioridade variada (por exemplo, prioridade alta para manexar tarefas de tempo crítico e baixa para outras tarefas)
- A aplicación vaise a executar nun entorno multiprocesador.
Supoñamos o escenario no que debemos crear uhna aplicación que se executará nun servidor para atender peticións de clientes (por exemplo un servidor web ou de base de datos):
- Cando se executa o programa éste abre o seu porto e queda á escoita esperando recibir peticións. Se cando recibe unha petición dun cliente ponse a procesala para obter una respuesta e devolverla, calquer petición que reciba mentras tanto non poderá ser atendida posto que está ocupado.
- A solución será construir a aplicación con múltiples fíos de execución. Ó executar a aplicación ponse en marcha o fío principal que queda a escoita. Cando recibe unha petición, creará un novo fío que se encarga de procesala e xenerar a consulta, mentras o fío principal segue á escoita recibindo peticións e creando fíos.
Deste xeito un xestor de bases de datos pode atender consultas de varios clientes; ou un servidor web pode atender a miles de clientes.
Qué acontece se o número de peticións simultáneas é tan elevado que a creación dun fío para cada unha delas pode comprometer os recursos do sistema? Neste caso resolverase mellor a xestión cun pool de fíos.
En conclusión os fíos son idóneos para programar aplicacións de entornos interactivos e en rede, así como simuladores e animacións.
Estados dun fío
O ciclo de vida dun fío comprende os diferentes estados nos que pode estar un fío desde que se crea ou nace ata que finaliza ou morre.
Os diferentes estados nos que se pode atopar un fío son:
- Novo (new): creouse un novo fío pero aínda non está disponible para a súa execución
- Executable (runnable): o fío está preparado para executarse. Pode estar Executándose, seempre e cando se lle asignou tempo de procesamento, ou ben que non esté executándose nun instante determinado en beneficio doutro fío, e por tanto estará Preparado.
- Non Executable ou Detido (no runnable): o fío podería estar executándose, pero hai alguhna actividade interna ó propio fío que llo impide, coma por exemplo unha espera producida por unha operación de Entrada/Saída (E/S). Se un fillo está en estado "Non Executable", non ten oportunidade de que se lle asigne tempo de procesamento.
- Morto ou Finalizado (terminated): o fío finalizou. A forma natural de que morra un fío é finalizando o seu método run().
Sincronización e comunicación de fios
Hai ocasións nas que os distintos fíos dun programa necesitan establecer algunha relación entre sí e compartir recursos ou información. Podense presentar as seguintes situacións:
- Dous ou máis fíos compiten por obter un mesmo recurso: por exemplo dous fíos que queren escribir nun mesmo ficheiro ou acceder á mesma variable para modificala
- Dous ou más fíos colaboran para obter un fin común e para iso precisan comunicarse a través dalgún recurso: por ejemplo un fío produce información que utilizará outro fío.
En calquiera destas situacións é preciso que os fíos se executen de xeito controlado e coordinado para evitar posibles interferencias que puideran desembocar en programas que se bloquean con facilidade y que intercambian datos de xeito equivocado.
Cómo conseguimos que os fíos se executen coordinadamente? Utilizando:
- Sincronización de fíos. É a capacidade de informar da situación dun fío a outro. O obxectivo é establecer a secuencialidade correcta do programa.
- Comunicación de fíos. É a capacidade de transmitir información desde un fío a outro. O obxectivo é o intercambio de información entre fíos para operar de forma coordinada.
O problema do interbloqueo (deadlock) e a inanición (starvation)
Consiste en que dous ou máis fíos se bloquean ou esperan indefinidamente. Cómo se chega a unha situación de interbloqueo?
- Porque cada fío espera a que lle chegue un aviso de outro fío que nunca lle chega
- Porque todos os fíos, de forma circular, agardan para acceder a un recurso
Outro problema, menos frecuente, é a inanición (starvation), que consiste en que un fío é sempre desestimado para a súa execución.
Acontece cando un fío non pode ter acceso regular os recursos compartidos e non pode avanzar, quedando bloqueado. Esto pode ocurrir porque:
- o fío nunca é seleccionado para o seu procesamento(por exemplo porque ten moi baixa prioridade e nunca chega o seu turno)
- outros fíos que compiten polo mesmo recurso llo impiden
- o recurso requirido nunca estará disponible (por exemplo un ficheiro que non é correctamente pechado e queda bloqueado)
Está claro que os programas que desenvolvamos deben estar exentos destes problemas polo que haberá que ser cuidadosos no seu deseño e controlar estas situacións.
Aplicacións multifio
Unha aplicación multifío debe reunir as seguintes propiedades:
- Seguridade
- A aplicación non chegará a un estado inconsistente por un mal uso dos recursos compartidos. Esto implicará sincronizar fíos asegurando a exclusión mutua.
- Viveza
- A aplicación non se bloqueará ou provocará que un fío non se poida executar. Esto implicará un comportamento non egoísta dos fíos e ausencia de interbloqueos e inanición.
A corrección da aplicación mídese en función das propiedades anteriores, puidendo ter:
- Corrección parcial. Cúmprese a propiedade de seguridade. Se o programa termina o resultado é o desexado.
- Corrección total. Cúmprense as propiedades de seguridade e viveza. O programa terminará nalgún momento e o resultado é o correcto.
Para desenvolver unha aplicación multifío será preciso ter en conta os siguientes aspectos:
- A situación dos fíos na aplicación:
- Fíos independentes. Non será necesario sincronizar e/ou comunicar os fíos.
- Fíos colaborando e/ou competindo. Será necesario sincronizar e/ou comunicar os fíos, evitando interbloqueos e esperas indefinidas.
- Xestionar as prioridades, de xeito que os fíos máis importantes se executen antes.
- Non todos os Sistemas Operativos implementan time-slicing, polo que hai que evitar que os threads sexan egoístas.
- A execución de fíos é non-determinística.
As aplicacións multifío son máis difíciles de desenvolver e complicadas de depurar que unha aplicación secuencial ou de un só fío; pero se utilizamos as librerías que aporta a linguaxe de programación, podemos obter algunhas vantaxes:
- Facilitar a programación. Require menos esforzo usar unha clase estándar que desenvolvela para realizar a mesma tarefa
- Maior rendemento. Os algoritmos utilizados foron desenvoltos por expertos en concurrencia e rendemiento
- Maior fiabilidade. Usar librerías ou bibliotecas estándar permítenos garantir un mínimo de calidade no noso software. Poden ser deseñadas para evitar:
- interbloqueos (deadlocks)
- cambios de contexto innecesarios
- condicións de carreira
- Menor mantemento. O código que xeneremos será máis lexible e fácil de actualizar
- Maior productividade. O uso dunha API estándar permite mellor coordinación entre desarrolladores e reduce o tempo de aprendizaxe.
Porén, cando vaiamos a desenvolver unha aplicación multifío debemos facer uso das utilidades que ofrece a propia linguaxe; iso facilitará a posta a punto do programa e a súa depuración.
Depuración e documentación
Dúas tarefas moi importantes cando desenvolvemos software son a depuración e a documentación:
- Mediante a depuración trataremos de corrixir fallos e erros de funcionamento do programa
- Mediante a documentación interna aportaremos lexibilidade os nosos programas
A depuración de aplicacións multifío é unha tarefa difícil debido a que:
- A execución dos fíos ten un comportamento non determinístico.
- Hai que controlar varios fluxos de execución.
- Aparecen novos erros potenciales debidos á compartición de recursos entre varios fíos:
- porque non se cumple a exclusión mutua
- porque se produce interbloqueo
Para documentar unha aplicación multifío non debemos escatimar en comentarios, debido a súa maior complexidade. Podemos utilizar xeneradores automáticos de documentación (como Javadoc), documentar os casos de proba unitarios (coma con Junit) ou a xestión de ciclo de probas (usando por exemplo TestLink), ademáis de elaborar manuales técnicos e de usuario.
Resumo
- A execución dun proceso comeza con un único fío
- mais este pode crear novos fíos sobre a marcha
- os distintos fíos dun mesmo proceso comparten memoria e código de programa
- O acceso ós obxectos compartidos por distintos fíos dun mesmo proceso deben controlarse mediante mecanismos de sincronización, para evitar erros que poderían darse se máis dun fío modifica á vez o estado deses obxectos
- O interbloqueo ou deadlock prodúcese cando dous ou máis fíos manteñen bloqueados un conxunto de objetos de bloqueo, e a súa execución está suspendida á espera de conseguir bloquear cada un un novo obxecto de bloqueo, que non poden bloquear porque xa están bloqueados por outros fíos
- Podese evitar o interbloqueo establecendo unha orde para os obxectos de bloqueo, e todos os fíos bloqueanse segundo este orde.
- Se unha vez conseguido o bloqueo sobre un obxecto de bloqueo obj nun bloque de código synchronized(obj), un fío comproba que non se dan as condicións necesarias para continuar coa súa execución, pode executar wait() sobre o obxecto de bloqueo con obj.wait(). Con iso a súa execución queda suspendida e en espera non activa, mentres que outro obxecto non execute
- obj.notifyAll() --> reanuda todos os procesos á espera no obxecto de bloqueo
- obj.notify() --> reanuda só un dos procesos á espera
- Podense incluir nunha clase os mecanismos de sincronización axeitados para permitir o uso dos seus obxectos por distintos fíos de xeito concurrente
- Desa maneira, non é preciso implementar neses fíos ningún mecanismo de sincronización para controlar o acceso concurrente ós obxectos da clase
- Dise entón que a clase é thread-safe ou segura para o seu uso concurrente por parte de varios fíos
Práctica
Glosario
- Condición de carrera
- Situación na que o correcto resultado dun programa depende do orden en que se intercale a execución das instruccións dos seus diferentes fíos.
- Espera non activa
- Situación na que se atopa un fío cuxa eeecución se atopa suspendida, de xeito que non utiliza tempo de procesador para comprobar se se dan as condicións para reanudar a súa execución. Cuando iso suceda, outro fío enviará unha notificación que permitirá a reanudación da súa execución.
- Fío
- Unidade de execución dun programa asociada a un proceso. Distintos fíos dun mesmo proceso comparten código de programa e datos.
- Interbloqueo
- Situación en que dous ou máis fios están bloqueados mutuamente, todos eles esperando para conseguir o bloqueo sobre obxectos de bloqueo bloqueados por outros fíos, de xeito que ningún de eles poderá continuar nunca.
- Método sincronizado
- Os métodos sincronizados de Java executanse en exclusión mutua entre distintos fíos.
- Obxecto de bloqueo
- Obxecto sobre o que hai que obter un bloqueo para poder executar un bloque de código. O bloqueo se libera cando termina a ejecución do bloque de código.
- Sección crítica
- Fragmento dun programa que non pode executar de maneira simultánea (concurrentemente) máis dun fío do programa; é decir, que distintos fíos se deben executar en exclusión mutua.
- Thread-safe
- Díse dunha clase cuxos métodos implementan os mecanismos de sincronización precisos para o uso concurrente dos seus obxectos por parte de disintos fíos, de xeito que non é necesario ningún mecanissmo de sincronización externo á propia clase.