• No se han encontrado resultados

Excepciones. Sistemas Embebidos II

N/A
N/A
Protected

Academic year: 2021

Share "Excepciones. Sistemas Embebidos II"

Copied!
166
0
0

Texto completo

(1)

Excepciones

(2)

Excepciones

Una excepción puede definirse como la ocurrencia de un error en tiempo de ejecución

Al hecho de mostrar la condición de excepción, al que invocó la operación que causó dicha excepción, se le denomina generar (señalar o lanzar) la

excepción, mientras que, a la respuesta del invocante

(directo o indirecto) se le denomina gestión, manejo

o captura de dicha excepción

La gestión de excepciones se puede considerar un mecanismo de recuperación de errores hacia

adelante aunque, puede utilizarse también para

(3)

Excepciones

En la práctica, la mayor parte de un sistema, está dedicada a tratar con situaciones anormales,

excepcionales o no deseadas;

la parte más pequeña corresponde a la propia aplicación:

más de 2/3 del código! está dedicada a la detección y

manejo de errores (Cristian, 1982)

Es muy probable que esta porción, contenga errores ya que, además de su complejidad,

 Puesto que rara vez se ejecutan, es el general la parte del

código menos ensayada, documentada y entendida

La mayoría de las fallas de diseño existente en un sistema parecen estar ubicadas en el código que maneja situaciones excepcionales

(4)

Excepciones

El ejemplo más citado en la literatura es

el accidente del cohete

Ariane 5

debido a

una excepción de software

no manejada

.

(5)
(6)

25 años después de Cristian (Cabral y

Marques) …gracias a Java

En Exception Handling: A Field Study in Java and .NET Menos del 10% del código se dedica al tratamiento de errores y, el porcentaje más común está alrededor de un 5%

Los desarrolladores rechazan usar este mecanismo,

escribir código de calidad para manejo de errores es una tarea compleja, engorrosa y propensa a errores. Se

(7)

Potencial incremento en la

complejidad en Java

(8)

Potencial incremento en la

complejidad en Java

Cuidado con su uso! Debería volverse a las raíces y recordar que las excepciones son excepcionales, usarlas para eventos raros (que no ocurren

frecuentemente), no para el control del flujo normal. Lamentablemente no está de acuerdo con el uso

actual de las excepciones en lenguajes tales como Java.

En el siguiente ejemplo el 1º método es 750 veces más lento que el 2º (bajo Windows XP y Java 1.4) si se llama con una referencia que no sea del tipo

(9)

Potencial incremento en la

complejidad en Java

public static boolean testForInteger1(Object x) {

try {

Integer i = (Integer) x;

return true;}

catch (Exception e) {

return false;}}

public static boolean testForInteger2(Object x)

{return x instanceof Integer;}

(10)
(11)
(12)

En C++

En C++ se necesita prestar atención a las excepciones

sólo en aquéllos lugares donde se violan invariantes de clases, en todo el resto del código no hay que

preocuparse

 Las invariantes de clase

 describen condiciones que son válidas para todos los objetos de dicha clase

 garantizan que un objeto está en un estado válido

Las implementaciones C++ modernas reducen la sobrecarga del uso de excepciones a un porcentaje pequeño (un 3%, otros afirman entre un 10-15% y algunos cerca de 30%)

(13)
(14)

Excepciones

Aunque el trabajo de

Goodenough

(

1975

)

puso los cimientos de la

terminología

relacionada con las excepciones

, aún hoy día,

no existe un acuerdo

en la

definición del

concepto de excepción, como así tampoco en

cuanto a su utilización

En realidad, la palabra excepción sugiere

algo que ocurre muy raramente

”. Sin

embargo, esto se condice sólo parcialmente,

con el uso que comúnmente se hace de ellas

en la práctica diaria de la programación

(15)

Excepciones

Las excepciones constituyen una forma adicional de pasar información al invocante de un método

Por tanto, resuelven el problema de los lenguajes de programación que permiten un único valor de retorno Pueden utilizarse para distintos propósitos. Goodenough (1975) señala 3:

Señalar una avería

Clasificar un resultado (Ej.: overflow en suma, EOF),

información adicional del resultado para que el invocante pueda interpretarlo adecuadamente

Control o monitoreo (Ej. “se han procesado n registros”), el

invocante desea que se le notifique que se alcanzó alguna condición

Por tanto, no necesariamente indican “eventos

excepcionales”. Ej.: en Java InterruptedException se utiliza para sincronizar threads

(16)

¿Qué es excepcional?

No poder cumplir una precondición en una función

Un constructor no puede construir un objeto (no puede establecer las invariantes de la clase)

Un error fuera de rango v[v.size()] = 7

Incapacidad de adquirir un recurso (ej. Se cae una red)

En contraste, la terminación de un lazo ordinario no es excepcional, a menos que sea infinito la terminación es normal y esperada

(17)

¿Qué es excepcional?

No lance excepciones como forma alternativa de retornar un valor de una función o, donde bastaría usar estructuras de control locales

(18)

Excepciones en C++

Para un efectivo manejo de errores usando

excepciones, los mecanismos del lenguaje deben usarse basándose en una estrategia.

Por tanto presentaremos los conceptos de garantía de seguridad frente a excepciones, central para la

recuperación de errores en tiempo de ejecución y, la técnica RAII (Resource Adquisition is Initialization) para la gestión de recursos usando constructores y

destructores.

(19)
(20)

Excepciones

La noción de una excepción se provee para ayudar a suministrar información desde el punto donde se

detecta el error hasta el punto donde pueda manejarse.

 Idea general para manejar errores en forma no local

Una función que no puede manejar un problema lanza (throws) una excepción, esperando que su invocante

(directo o indirecto) pueda manejarlo

La función, en la cadena de invocaciones, que desee manejar un determinado problema, lo indica atrapando (catching) la correspondiente excepción

(21)
(22)

Excepciones

Un componente invocante indica el tipo de fallas que está dispuesto a manejar especificando esas excepciones en las cláusulas catch de un bloque try

Un componente invocado que no puede completar su tarea asignada, informa este hecho lanzando una

excepción usando una expresión throw

Invocado Invocante

(23)

Excepciones

La función taskmaster() está preparada para manejar una excepción del tipo Some_error pero, pueden

generarse otros tipos de excepciones.

Por ejemplo, do_task() puede llamar a otras

funciones para hacer varias sub tareas, algunas de las cuales pueden generar excepciones debido a que no pueden ejecutar sus sub tareas asignadas.

Una excepción diferente de Some_error indica que

taskmaster() falló en hacer su tarea asignada y, esta falla debe manejarse por alguna función que invoque

(24)

Excepciones

Una función invocada no puede sólo retornar un

indicativo que ocurrió un error.

Si el programa continúa trabajando (y, no sólo imprime un mensaje de error y termina), el retorno de la función invocada debe

 dejar el programa en un buen estado  Y, no perder recursos

En C++ el mecanismo de manejo de excepciones está integrado con aquéllos del constructor/destructor y

(25)

Mecanismo de gestión de

excepciones

Alternativa a los mecanismos tradicionales cuando son insuficientes, poco elegantes y propensos a errores

Es completo, puede usarse para manejar todos los errores detectados por código ordinario

Permite al programador separar explícitamente el código de manejo de errores del código “normal”, haciendo el programa más legible

Permite un estilo de más regular de manejo de errores, lo que simplifica la cooperación entre fragmentos de códigos escritos separadamente

(26)

Excepciones

Una excepción es un objeto lanzado (thrown) para representar la ocurrencia de un error

Puede ser de cualquier tipo que pueda copiarse pero, se recomienda usar sólo tipos definidos por el usuario,

específicamente para este propósito.

De esa forma, minimizamos las posibilidades que 2 librerías no relacionadas usen el mismo valor (por

ejemplo 17) para representar distintos errores, lo cual lleva el código de recuperación de errores al caos.

(27)

Excepciones

Si eso se vuelve tedioso, la librería estándar tiene una pequeña jerarquía de clases de excepción

Una excepción puede contener información sobre el error que representa.

Su tipo representa la clase de error y cualquier dato que contenga representa la ocurrencia particular de ese

error. En la librería estándar contiene un string que puede usarse para transmitir información de la

(28)

Manejo de errores tradicional

Consideremos las alternativas al manejo de

excepciones para una función que detecta un problema

que no puede manejar localmente, de forma tal que el error debe reportarse al invocante de dicha función. Cada enfoque tradicional tiene problemas y, ninguno es general

Tradicionalmente en una aplicación coexisten una combinación no sistemática de ellas

 Caos en programas compuestos de partes desarrolladas por

(29)

Terminar el programa

Enfoque muy drástico.

Para la mayoría de los errores se puede y debe hacerse algo mejor

 Por ejemplo, en la mayoría de las situaciones deberíamos por lo

menos escribir un mensaje decente o registrar el error antes de terminar.

En particular, una librería que no conoce el propósito o estrategia general del programa en la cual estará

embebida, no puede simplemente exit() o abort().

Una librería que termina incondicionalmente no puede

(30)

Retornar un valor de error

Esto no siempre es factible puesto que a menudo no hay valores de error aceptables

Para esta función, cada resultado entero es un valor

posible, no hay ningún valor entero para representar una entrada fallida.

Como mínimo deberíamos modificar la función para que retorne un par de valores.

Aún cuando este enfoque sea factible, generalmente es

inconveniente debido a que, en cada invocación debe

chequearse para ver si se produjo un error.

(31)

Retornar un valor de error

Además, a menudo los invocantes ignoran la

posibilidad de errores o simplemente olvidan testear uno de los valores retornados.

 Por ejemplo, printf retorna un valor negativo si ocurrió un

error de salida o codificación, aunque los programadores esencialmente nunca verifican esto.

Finalmente, algunas funciones no tienen valores de retorno, un ejemplo obvio son los constructores

(32)

Retornar un valor legal y dejar el

programa en estado erróneo

Tiene el problema que la función invocante puede no notar que el programa quedó en un estado erróneo. Por ejemplo, muchas funciones de la librería estándar de C (las funciones matemáticas no lo requieren)

(33)

Retornar un valor legal y dejar el

programa en estado erróneo

Aquí el valor de d no tiene sentido y errno se setea

(en 0 antes de invocar) para indicar que -1.0 no es un valor aceptable para esta función (NaN dependiendo de la implementación, errno distinto de 0).

Sin embargo, los programas fallan de configurar y testear errno para evitar errores consecuentes

causados por valores retornados en invocaciones fallidas.

Además, el uso de variables no locales para registrar condiciones de error, no funciona bien en presencia de

(34)

Invocar una función manejadora

de error

Es alguno de los anteriores pero disfrazado puesto que, el problema se vuelve inmediatamente: ¿Qué hace esta

función?

A menos que dicha función pueda resolver

completamente el problema (en este caso ¿porqué lo consideraríamos un error?), debe a su vez,

 terminar el programa,

 retornar con alguna indicación que ocurrió un error,  establecer un estado de error o,

(35)

Excepciones

El manejo de errores seguirá siendo una tarea difícil

El manejo de excepciones, aunque mas formal que las técnicas a las que reemplaza, es aún relativamente

desestructurada, en comparación con otras

características del lenguaje que sólo involucran el flujo de control local.

El mecanismo de manejo de excepciones de C++ provee al programador de una forma de manejar errores donde ellos puedan ser manejados más naturalmente

Las excepciones hacen visible la complejidad del

manejo de errores. Sin embargo no son la causa de aquella complejidad

(36)

¿Cuándo no usarlas?

Un componente de un sistema embebido con restricciones

temporales críticas, donde se debe garantizar que cumpla con sus

deadlines. En ausencia de herramientas que puedan estimar con precisión el tiempo máximo para que una excepción se propague de un throw a un catch, deben usarse métodos de manejo de errores alternativos

Un programa viejo y grande en el que la gestión de memoria se

realiza ad hoc usando new y delete, en lugar de usar algún esquema sistemático tales como manejadores de recursos (ej. Usen vector, string).

En sistemas con poca capacidad de memoria y donde el soporte para el manejo de excepciones consuma unos 2K de memoria Ídem que antes, recurrir a las técnicas tradicionales.

(37)

Artículo interesante

En “Abnormal Events Handling for Dependable

Embedded Systems” (2006), Luis E. Leyva-del-foyo y otros

Se analizan las dificultades de manejar eventos anormales e, introducen un marco de trabajo que integran conceptos de:

 Diseño por contrato,

 niveles de seguridad frente a excepciones (imitado en C) y,  las distintas fases de tolerancia a fallas.

Diseñan un mecanismo que utiliza códigos de errores, excepciones y aserciones ejecutables en código de

(38)

Cláusula throw

La cláusula throw lanza una excepción. En la cláusula puede ir un int, un float, un string o un objeto de cualquier tipo que pueda copiarse. Se parece a un return (return 30, …throw 30), ambos informan el

resultado al invocar una función. Return tiene un tipo fijo, throw puede lanzar cualquier tipo

(39)
(40)
(41)

Cláusula throw

En general no se recomienda usar tipos primitivos en la claúsula throw, tal como int.

Es preferible definir tipos con el único fin de manejar excepciones, lo cual minimiza la confusión acerca de su propósito.

El objeto excepción atrapado, es en principio, una

(42)

Cláusula throw

Esta variable temporal puede copiarse varias veces antes de ser atrapada: la excepción se pasa de la función invocada a las invocantes, así hasta que se encuentra un manejador apropiado.

Por tanto nunca poner demasiada cantidad de datos en las excepciones

Los datos asociados al objeto excepción (si existen) se usan típicamente para producir mensajes de error

o para ayudar en la recuperación.

El tipo de excepción se usa para seleccionar el manejador (catch de un bloque try)

(43)

Rica información en las

excepciones

(44)

Propagación de excepciones

(45)

Propagación de excepciones

El proceso de pasar una excepción a través de la cadena de invocaciones, desde el punto donde se

detectó hasta un manejador se llama “stack unwinding”. Si ninguna función en la cadena de invocaciones tiene un manejador adecuado, la excepción es manejada por el runtime, abortando el programa. En C++ se invoca a

(46)
(47)
(48)
(49)

Propagación de excepciones

El tiempo de vida de los objetos locales de la función donde se generó la excepción, termina

(50)

Propagación de excepciones

Los destructores de los objetos construidos en el bloque try que falló, se invocan en orden inverso al de la construcción

En el ejemplo se destruyen los strings, luego del lanzamiento en h() en este orden: “not”, “or”, “excess”, “in”

aunque no “at all” ya que nunca se ejecutará ni tampoco “Byron” ya no fue afectado

(51)

Terminación anormal

std::terminate() (invoca a std::abort()) hace que se imprima un mensaje de error indicando el tipo de excepción que ocurrió

(52)

Terminación anormal

Las reglas específicas para invocar a terminate() son, entre otras:

Cuando no se halla un manejador apropiado para una excepción que se lanzó

Cuando una función noexcept trata de salir con un throw

Cuando se invoca un destructor durante el desapilado y éste trata de salir con un throw

Cuando el código invocado al propagar una excepción (por ejemplo, un constructor de copia) trata de salir con un throw

(53)

Terminación anormal

Un usuario puede invocar terminate() si no son factibles enfoques menos drásticos.

Por defecto terminate() llama abort() que es la opción correcta para la mayoría de los usuarios, especialmente durante el debug.

Sino es aceptable el usuario puede proveer una función manejadora de la terminación invocando

(54)
(55)

Terminación personalizada

Por suerte el programa puede ganar el control

reemplazando la llamada a abort() por la invocación a la función callback que le pasemos como

(56)

Terminación personalizada

Hay 4 reglas que se aplican a dicha función:

 No debe tomar argumentos

 No debe retornar, sólo puede terminar su ejecución con exit

o abort

 No está permitido que lance una excepción

Debe añadir el header <exception> para la función set_terminate()

Igualmente, no es una forma muy efectiva de manejar excepciones

(57)
(58)

Atrapando excepciones

El manejador H se invoca si:

1. H es del mismo tipo que E

2. H es una clase base pública de E

3. Si H y E son de tipo punteros y 1 o 2 se mantienen para

los tipos a los que se refieren

4. H es una referencia y 1 o 2 se mantiene para el tipo al cual

(59)

Cláusula throw

Aconsejado throw por valor (guarda en variable temporal) y atrape por referencia o referencia

(60)

Cláusula throw

(61)

Agrupamiento de excepciones

Se puede utilizar las jerarquías de herencia para crear familias de excepciones para reflejar las

relaciones entre las distintas clases de errores que representan

(62)
(63)

Cláusula throw

Atrapar por referencia en lugar de por copia (al igual que cuando se pasan argumentos por copia a una función) evita el slicing

La mayoría de los manejadores (catch) no modifican la excepción atrapada y, en general, se recomienda el uso de const, que además enfatiza que esto no ocurrirá en el bloque catch

(64)
(65)

Slicing

Al igual que cuando se pasan parámetros por valor, cuando se atrapa por valor puede producirse «slicing»

(66)

Orden en los catch

Debido a que una excepción derivada puede ser

atrapada por un manejador para más de un tipo de

excepción, es importante el orden en el cual son escritos los manejadores

Los manejadores son procesados en el orden en el que

(67)
(68)
(69)

Atrapando excepciones

Minimice el uso explícito de try/catch, es verboso y su uso no trivial es propenso a errores.

try/catch puede ser un signo de un manejo de recursos o de errores no sistemático y/o de bajo nivel

Atrape las excepciones sólo cuando vaya a hacer algo significativo con ellas (implican complejidad y gasto),

tampoco lo haga sólo para lanzar una diferente.

En caso contrario, deje que se propaguen hasta el nivel superior, allí informe sobre la falla y continue o exit

Permita que las acciones de limpieza (liberación de recursos adquiridos) sean manejadas por RAII

(70)
(71)

C++11: noexcept

Usado para indicar que una función no lanzará

excepciones, no hace falta usarlo en cada función

Si igualmente lo hace, se invoca a std::terminate (no se invocarán destructores para los objetos locales)

Muchas funciones de la librería estándar son noexcept y, lo son todas las heredadas de la librería estándar de C

Pueden declarar a una función condicionalmente

noexcept. En la librería estándar esto es muy común, e importante en operaciones que se aplican a contenedores

(72)

C++11: noexcept

Los destructores son noexcept por defecto (C+11) (no deberían lanzar excepciones)

Cuando se aplica a una función, se le permite al

compilador generar mejor código objeto (establece ciertas optimizaciones)

También es una forma de informar a los programadores acerca de la función declarada noexcept y, ver si es

necesario o no, tratar las excepciones que puedan (o no) generarse al invocar dicha función

double compute(double d) noexcept { return log(sqrt(d <= 0 ? 1 : d)); }

(73)
(74)

Catch all en C++

Técnica subóptima puesto que maneja todas las excepciones de la misma forma (debería estar en

último lugar en la lista de catch de un try), no provee información detallada de la excepción ocurrida

(75)

Catch all en C++

Puede proveerse como “último recurso” para que el programa termine de forma consistente en el caso de

excepciones no esperadas No permite tomar acciones

significativas de

recuperación más que relanzar la excepción que ocurrió (throw;)

(ineficiente, código difícil de mantener)

(76)

Catch all en C++

Debería usarse sólo en main o cuando se invoca

código de terceros que no se sabe cómo manejaron las excepciones

(77)

Rethrow la misma excepción

Un rethrow es indicado con un throw sin un operando

Útil para manejar problemas locales y pasar la excepción al invocante para acciones necesarias

posteriores donde sea más apropiado, en caso que el

manejador no pueda manejar completamente el error

Esto es válido sólo dentro de un catch o, desde dentro de un catch de una función invocada directa o

indirectamente (común en un catch all, previo tareas de limpieza)

(78)

Rethrow

A veces, la información necesaria para manejar mejor el error no está disponible en un solo lugar, de forma tal que, la acción de recuperación está mejor

distribuida sobre varios manejadores

Si se intenta un rethrow donde no hay una excepción para relanzar se invoca std::terminate().

(79)
(80)

¿Porqué C++ no tiene

finally

?

Porque tiene destructores que se encargan de liberar recursos

(81)

Jerarquía de excepciones estándar

en C++

(82)

Jerarquía de excepciones en C++

En la librería estándar hay una pequeña jerarquía de excepciones que puede usarse directamente, para

excepciones que requieran un manejo genérico o, como clases bases

Las 2 clases principales de excepciones estándar logic_error

 Destinado a errores previsibles

 Argumentos de funciones no válidos, invariantes violados, etc.  Una alternativa potencial a assert

run_time error

(83)
(84)

Clase Exception

Clase base de todas las excepciones estándar, definido en el header <exception>

what() retorna un mensaje dependiente de la

(85)

Usando las excepciones estándar

Están disponibles para usar en los programa

 Se pueden usar como están, generar subclases de ellas o

ignorarlas y crear excepciones propias

(86)
(87)
(88)
(89)

Invariantes

Para recuperarse de un error, un programa debe quedar en un “buen estado” (válido, consistente, legal)

Cada clase tiene una noción sobre qué significa “buen estado”, dado por sus invariantes (Hoare, 1972)

Una invariante es establecida en el constructor de una clase. Java, Python, C#, no pueden implementar

invariantes por construcción

Condición lógica para los datos miembro que, el constructor debe establecer y que,

las funciones miembro, que acceden a la representación del estado de un objeto hasta que se destruyen, deben

asumirlas (son las pre y postcondiciones)

(90)

Invariantes

En la siguiente clase Vector tiene una invariante simple que es:

v apunta a un arreglo de sz elementos enteros y, Todas las funciones miembros están escritas con la

suposición que la misma es verdadera o sea que, esta regla simple impuesta por el diseñador, debe mantenerse siempre que se invoca una función miembro y,

debe asegurarse al retornar de las mismas que dicha

(91)
(92)

Invariantes

Por ejemplo, en size() no se cambia ningún dato miembro, o sea se mantienen las invariantes que garantizan que sz realmente almacena el número de elementos

El operador suscripto es más comprometido aunque, es simple porque está escrito basado en las

invariantes básicas (no hay que chequear v!=0), aunque ¿cómo se basa en ellas?

(93)

Invariantes

Si el constructor se definió de la siguiente forma, si new genera una excepción y no se construye ningún objeto, es imposible crear un Vector que mantenga los elementos requeridos

Para no perder recursos, el destructor debería liberar la memoria adquirida por el constructor.

La razón por la cual es tan simple, es porque confía en la invariante de que v está apuntando a memoria asignada

(94)

Invariantes

Considere la siguiente implementación ingenua del operador de asignación.

¿Puede generar excepciones?

Si eso ocurre, ¿Siguen manteniéndose las invariantes?

En realidad, es un desastre esperando suceder!

Veamos un ejemplo donde pueda comprobarse esta afirmación

(95)
(96)

Invariantes

Si espera, al ejecutarlo, que aparezca el mensaje

Oops: memory exhausted! debido a que no tiene 320 Mb de memoria dinámica de sobra, se sentirá

decepcionado.

Sino tiene unos 160 Mb libres en el heap, la

construcción de v2 fallará de manera controlada y producirá dicho mensaje de error pero,

Sin embargo, si tiene esa cantidad de heap libre pero no los 320 Mb, eso no sucederá

Cuando la operación de asignación trate de otorgar memoria para la copia de los elementos (v=new int[sz]), se generará una excepción bad_alloc.

(97)
(98)

Invariantes

Cuando se genera la excepción, al salir del bloque try donde se intenta definir vec de capacidad 320 Mb, se invoca al destructor que intenta desasignar el espacio de vec.v pero,

el operador = ya había hecho un delete de dicho espacio!

Algunos gestores de memoria desaprueban estos

intentos de hacer un delete 2 veces en el mismo área del heap, algunos entran en un lazo infinito

(99)

Invariantes

El error está en que el operador = falló de mantener la invariante: v apunta a un array de sz enteros,

hecho esto era cuestión de tiempo para que ocurra un desastre

Arreglar este problema es fácil: Debe asegurarse que la invariante se mantenga antes de lanzar una

excepción. O, aún más simple, no deseche una buena representación antes de tener una alternativa.

(100)

Invariantes

Ahora si new falla al reservar memoria (para p) y lanza una excepción, el vector a ser asignado (lado izquierdo del =) sigue sin modificarse y,

en particular, en nuestro ejemplo si esto ocurre, al lanzarse la excepción termina mostrando el mensaje

Oops: memory exhausted!.

El código dentro de una función miembro puede romper las invariantes de clase mientras que sean restauradas antes que retorne (postcondición)

Ahora Vector es un ejemplo de manejo de recursos

(los elementos del array) de forma simple y segura usando la técnica RAII

(101)
(102)

Manejo de recursos

Los recursos no son solamente “memoria” y, al igual que con ella, requieren una operación de liberación

luego de adquirirlos y usarlos

Ejemplos:

 Conexión a bases de datos remotas

 Apertura y cierre de archivos/sockets/windows  Trabajo seguro con mutex

 Adquisición y liberación de memoria dinámica desde el heap  Conexiones de red, threads

(103)
(104)

RAII

Técnica general que se basa en las propiedades de

constructores y destructores y su interacción con el mecanismo de manejo de excepciones

Cuando una función adquiere un recurso (Ejemplo: abrir un archivo, asignar memoria dinámica, adquirir un mutex, etc),

en general, es esencial para la ejecución futura del

sistema que, el recurso sea liberado apropiadamente; a menudo, esto debe concretarse antes de retornar a su invocante

(105)

RAII

La simetría de constructor/destructor refleja la simetría inherente en los pares de funciones para

adquirir/liberar recursos tales como fopen/fclose, lock/unlock, new/delete, etc.

(106)

RAII

Esto parece plausible hasta que, se realiza algo que falla luego de invocar a fopen() y, antes de invocar a fclose(). Una excepción puede provocar que

use_file() termine sin invocar a fclose()

(107)

RAII

El código donde se usa el archivo se encierra en un

bloque try que atrapa cada excepción, cierra el archivo y

relanza la excepción. Esta solución es muy verbosa,

tediosa y potencialmente costosa. Peor aún, este código se vuelve potencialmente mas complejo cuando hay que adquirir y liberar varios recursos. La forma general del problema es:

Afortunadamente hay una

(108)

RAII

Típicamente es importante liberar recursos en orden inverso a la adquisición. Liberar correctamente los recursos es particularmente importante para

programas que se ejecutan durante largo tiempo

Esto claramente se asemeja al comportamiento de los objetos locales creados por constructores y

destruidos por destructores cuando salen fuera de su ámbito.

Por lo tanto podemos manejar la adquisición y

liberación de tales recursos usando objetos locales de clases con constructores y destructores. Por ejemplo podemos crear una clase File_ptr (clase

(109)
(110)

RAII

Podemos construir un objeto File_ptr dando un FILE* o, los argumentos requeridos por fopen(). En

cualquier caso, un objeto File_ptr será destruido cuando sale de su ámbito y su destructor cerrará el archivo.

File_ptr lanza una excepción sino puede abrir un archivo porque, de lo contrario cada operación que use el archivo tendrá que chequear por nullptr.

Nuestra función use_file ahora se reduce a esta mínima:

(111)

RAII

Si el constructor no puede construir un objeto válido debe lanzar una excepción. Dejar un objeto en

(112)

RAII

El destructor será invocado independientemente, si la función termina normalmente o, si sale debido a que se señaló una excepción.

El mecanismo de manejo de excepciones nos

permite remover el código de manejo de errores del algoritmo principal.

El código resultante es más simple y menos propenso a errores que su tradicional contrapartida.

(113)

RAII

La adquisición de un recurso (aquí FILE*) está ligada a la

construcción (inicialización) de un objeto mientras que, la

liberación lo está a la destrucción del mismo

El compilador asegura que el destructor será invocado cada vez que una variable local (pila) deja su ámbito (aún debido a una excepción)

 Los recursos críticos serán delimitados a objetos locales

RAII es crítico para escribir código seguro frente a excepciones

Para todos los recursos

 Memoria (hecho por std::string, std::vector, std::map….)

 Locks (std::unique_lock), archivos (std::fstream), sockets, threads

(std::thread)…, punteros inteligentes (gestión de memoria dinámica)

(114)

RAII

A menudo, se sugiere que escribir una clase handle

(RAII) es tedioso por lo que, proveer una sintaxis para la acción catch(…) sería una mejor solución. El problema con este enfoque se necesita recordar “atrapar y corregir” el problema, dondequiera que se adquiera un recurso en forma no disciplinada

(normalmente decenas o centenas de lugares en un programa grande), mientras que, la clase handle de RAII necesita escribirse una sola vez

(115)
(116)

Ejemplo

Del libro “Embedded programming with modern C++ Cookbook - Practical recipes to help you build robust and secure embedded applications on Linux” de Igor Viarheichyk (2020)

(117)
(118)

Ejemplo

Podemos ver un claro orden (ningún thread worker es interrumpido por otro y cada uno se ejecuta del principio al fin)

lock_guard es un wraper encima de un mutex que usa la técnica RAII que automáticamente hace un lock del mutex en el constructor y hace

automáticamente un unlock en el destructor del

mismo, cuando el objeto envuelto sale de su alcance Aquí se usa para proteger la sección crítica del código (código entre paréntesis en la función worker, estos paréntesis definen el ámbito de dicha sección crítica)

(119)
(120)

RAII

Un objeto no se considera construido hasta que su

constructor se completa. Entonces y, sólo entonces, el “desapilado” invocará al destructor de dicho objeto. Un objeto construido de sub objetos se construye en la medida que se construyen sus sub objetos. Un array se construye en la medida que se construyen sus elementos (sólo los elementos construidos completamente son

destruidos en el desapilado)

Un constructor intenta asegurarse que sus objetos se construyan completa y correctamente. Cuando esto no

puede alcanzarse, un constructor bien escrito, restaura en la medida de lo posible el estado del sistema a aquél de antes de la creación; no deja sus objetos “a medio

(121)

RAII

Considere una clase en la cual necesita adquirir 2 recursos: un archivo y un mutex, esta adquisición puede fallar y lanzar una excepción.

El constructor nunca debe completarse habiendo

adquirido 1 de los 2 recursos solamente (o sin poder adquirir ninguno).

Esto debería lograrse sin imponer una sobrecarga de complejidad para el programador.

En el siguiente ejemplo, en una clase X se adquieren 2 recursos: objetos de las clases File_ptr y

std::unique_lock. La adquisición de ambos recursos,

está representada por la inicialización de ambos objetos locales que representan los recursos

(122)

RAII

Aquí si ocurre una excepción después que p sea construido pero, antes que lck sea construido,

entonces el destructor de p (pero no el de lck ya que no se llegó a construir) será invocado.

El autor del constructor no necesita escribir código explícito para manejo de excepciones

(123)
(124)
(125)

RAII

El recurso más común es la memoria: string, vector y otros contenedores de la librería estándar usan RAII

para manejar implícitamente la adquisición y liberación.

Comparado con el manejo ad-hoc de memoria

usando new/delete, ahorra un montón de trabajo y evita un montón de errores.

En lugar de objetos locales si necesita punteros a objetos, use los declarados en la librería estándar

tales como unique_ptr y shared_ptr para evitar fugas de memoria

(126)
(127)
(128)
(129)
(130)
(131)

RAII

Provee una estrategia más elegante a las usadas en

Java, etc.

“Destruction Is Resource Release (DIRR)”, tal vez sea el nombre más apropiado

En caso de trabajar con un hardware excesivamente limitado o en STR duros (excepciones no

suficientemente predecibles desde el punto de vista de tiempo) y, no pueda usar excepciones: simule RAII,

chequee que los objetos sean válidos luego de su

construcción y que se puedan liberar todos los recursos en los destructores.

RAII es la mejor y más sistemática forma de manejar recursos, aún sin excepciones

(132)

Ejemplo

Si se desea escribir

Si g no fue construido correctamente, func termina con una excepción, sino se puede lanzar una excepción,

puede simular el estilo RAII, añadiendo a Gadget una función miembro valid(). El problema ahora es que el invocante tiene que recordar para testear el valor de retorno

(133)
(134)

Ver orden de liberación

https://www.modernescpp.com/index.php/garbage-collectio-no-thanks

(135)
(136)
(137)

Wrapper para FreeRTOS

Michael Becker implementó una serie de clases wrapers para encapsular funcionalidades de FreeRTOS:

https://github.com/michaelbecker/freertos-addons,

permitiendo escribir código para una aplicación en C++ a la vez que use FreeRTOS. Presenta además 48 demos de proyectos mostrando cómo usar su librería

En

https://github.com/michaelbecker/freertos-addons/blob/master/c%2B%2B/Source/include/mute

x.hpp puede ver la clase LockGuard que se comporta

(138)
(139)

Reglas generales

Un objeto es completamente construido cuando termina su constructor

Si ello no ocurre, un constructor compatible con RAII,

deja al sistema con tan pocos cambios como sea posible Si un objeto está compuesto de sub-objetos es

construido hasta que todas sus partes sean construidas Si se sale de un ámbito (bloque, función…), entonces se invocan los destructores de todos los objetos locales a dicho ámbito, construidos exitosamente

Una excepción causa que el flujo del programa salga de todos los bloques, entre el throw y el correspondiente catch

(140)
(141)

Garantías de seguridad

Ninguna pieza de código individual requiere el mismo grado de tolerancia a fallas

 Un buen manejo de errores es multinivel

La seguridad frente a excepciones implica un examen cuidadoso de operaciones individuales

El estándar provee un conjunto (razonable) de niveles de garantías de seguridad frente a las excepciones

La librería estándar puede ser utilizable en esencialmente cualquier programa

 Por tanto, proporciona un buen ejemplo

Los conceptos, técnicas y enfoques de diseño son más importantes que los detalles

(142)

Garantías de seguridad de las

excepciones

Sin garantías

 Si una función lanza una excepción, puede suceder cualquier

cosa

 Los datos pueden corromperse o no ser válidos luego de una excepción  Use RAII, aún cuando no utilice excepciones

(143)

Garantías de seguridad de las

excepciones

Garantía básica (no pérdida)

 Si se lanza una excepción, las invariantes de un objeto son

aún válidas (no se corrompen estructuras de datos ni objetos, no hay “punteros colgados”) y, no se pierden recursos (por ej. toda la memoria asignada es retornada apropiadamente), las operaciones pueden quedar a medias

(aunque los objetos quedan en estado consistente)

 Se debería soportar, por lo menos, esta garantía para todas

(144)

Garantía básica

Los recursos usados deben ser destructibles

(posiblemente cuando se “desenrolla” la pila, o sea se supone que el destructor no lanzará excepciones) y

usables aún luego de una excepción

No hay garantías que el contenido de los datos sean los mismos que antes de invocar la función donde se generó la excepción (estado válido aunque no previsible)

 Si el componente tiene muchos estados válidos, luego de una

excepción no tenemos idea en que estado estará el componente, sólo que es válido.

 Las opciones de recuperación en este caso son limitadas: destruir

o, resetear el componente a un estado conocido antes de un uso posterior

(145)

Garantía básica

Puede ser la mejor elección cuando una garantía

firme (strong) es demasiado costosa en términos de

memoria o de performance

Frecuentemente este nivel es suficiente para el manejo de errores

Apropiadas cuando el invocante puede manejar operaciones fallidas que cambiaron el estado de objetos

Asegúrese que su destructor no lanzará excepciones bajo ninguna circunstancia

Tenga especial cuidado para mantener cualquier invariante que haya definido

(146)
(147)
(148)

Excepciones desde un

destructor

Un destructor puede invocarse:

Invocación normal: un objeto sale de su ámbito, delete, etc.

Invocación durante manejo de excepción: Durante el “desarmado de la pila” por el mecanismo de manejo de excepciones, se sale del ámbito de un objeto que posee un destructor

(149)

Excepciones desde un destructor

Las excepciones nunca deberían dejar el destructor. En caso de lanzarse una excepción en el destructor

cuando estaba siendo invocado al producirse otra

excepción y se estaba “desenrollando la pila” (no pueden propagarse dos excepciones simultáneamente), en este caso se invoca a std::terminate() y la aplicación muere!

Se pueden lanzar excepciones dentro del destructor pero,

nunca deberían escapar del mismo.

La librería estándar supone que, ni los destructores, ni las funciones de desasignación (ej.:delete), ni swap (por

(150)

Excepciones desde un

destructor

(151)

Excepciones desde un

destructor

(152)

Excepciones desde un

destructor

Los contenedores de la librería estándar no permiten construir contenedores de objetos cuyo destructor pueda lanzar excepciones

Si el destructor invoca a funciones que pueden lanzar excepciones, debe protegerse a sí mismo. Esto

permite especificar diferentes acciones dentro del destructor dependiendo si, un objeto es destruido normalmente o, como parte del desapilado

(153)

Garantía firme (

Commit or

rollback

)

Si se lanza una excepción, el estado de los objetos deben quedar como antes de que esto ocurra (rollback). Es

decir, si una operación falla debe garantizarse que no tendrá efectos secundarios

Una operación debe ser exitosa (commit) o rolled back (a veces demasiado costoso), nunca parcialmente completa

std::vector::push_back(a)

insert 1 solo elemento en una list uninitialized_copy()

(154)

Garantía firme

Las llamadas a las funciones con esta garantía son

atómicas en el sentido de que, si lo hacen con éxito lo

hacen completamente y, si fallan, es como si el programa nunca las hubiese llamado

Esta opción a veces penaliza la performance. Debería implementarse cuando es “natural” y libre para

hacerlo y, sólo en operaciones claves.

En la práctica, no se requiere tan a menudo como se esperaría puesto que, la garantía básica asegura que no se perderán recursos ni se violarán invariantes, sabiendo que el programa aún estará en un estado válido (aunque no el ideal)

(155)

Garantía firme (copy y swap)

Piense en una línea crítica que divide lo que está arriba (prepare: puede lanzar excepciones pero no modifica los datos originales), de lo que está debajo (commit: no debe lanzar excepciones y modifica los objetos originales, std::swap() puede ayudar en esta parte)

Puede ser muy difícil escribir código con esta garantía; sino se puede dividir en 2 fases, sea cuidadoso!

(156)

Garantía firme

Las operaciones que pueden lanzar excepciones se realizan

sobre una copia de los datos; si se realizan exitosamente, el objeto actual y su copia (ahora exitosamente

(157)

Garantía firme

temp es destruida al retornar (por destructor) y si ocurre una excepción, se destruye al desarmar la pila

(158)

Lectura complementaria

Generic: Change the Way You Write Exception-Safe Code — Forever

(https://www.drdobbs.com/article/print?articleId=18440

3758&siteSectionName=cpp)

Usan técnicas de programación genérica para ayudar a escribir código con garantía firme de seguridad frente a excepciones.

 Escribir código correcto en presencia de excepciones no es una

tarea fácil

 ScopeGuard clase genérica para un rollback, que puede

cancelarse si la acción intentada es exitosa (commited)

 Crea una clase ScopeGuard que si se necesita ejecutar

operaciones “todo o nada”, se añade una línea de código donde se instancia un objeto de esta clase después de cada una de estas operaciones

(159)

Lectura complementaria

ScopeGuard permite escribir código transaccional que permite deshacer el código que precede la creación de un objeto ScopeGuard si el siguiente código lanza una excepción

Luego de la versión C++11 muchos reimplementaron, a partir de este artículo, esta facilidad extremadamente importante para seguridad frente a excepciones.

La librería Boost tiene la suya:

https://www.boost.org/doc/libs/1_54_0/libs/scope_exit/ doc/html/index.html

(160)
(161)

Garantía nothrow (Failure

transparency)

Bajo ninguna circunstancia, la función no genera ni permite propagar ninguna excepción, ni usa funciones que lo

hagan.

Atrapan y manejan todas las excepciones lanzadas por las operaciones que usan. Siempre terminan exitosamente

Marcarlas como noexcept

Los destructores deberían tener este nivel de garantía, ídem con las funciones para desasignar y clean-up

(funciones clear/erase/reset en contenedores)

Esta garantía es provista por unas pocas operaciones

simples de la librería estándar tales como swap() y

(162)

Funciones C como fclose() no pueden lanzar excepciones (a menos que tomen argumentos funciones que sí lo hacen, tal como qsort() o

bsearch() que toman como argumentos punteros a funciones como argumentos)

Todas las funciones que trabajan sólo con tipos de datos primitivos son nothrow.

Ojo! Operaciones inocentes como <, =, ==, != o sort() pueden lanzar excepciones

La seguridad con respecto a excepciones se obtiene si algunas funciones (críticas) sostienen esta garantía

(163)

Garantía no throw

Es el nivel más alto de seguridad, aunque no siempre el mejor

 Las excepciones no son malas, son una herramienta para

tratar con situaciones problemáticas y, esta garantía la prohibe

(164)

Working Draft, Standard for Programming Language C++ N3337

(165)

Reglas generales

Decida qué nivel de tolerancia a fallas necesita

 No todos los fragmentos de código necesitan ser seguros

frente a las excepciones

Apunte a proveer una garantía firme y (siempre),

provea una garantía básica sino puede permitirse una garantía firme

 Mantenga un buen estado (generalmente el anterior) hasta

que haya construido uno nuevo, luego realice la actualización “atómicamente”

Defina “buen estado” (invariante) cuidadosamente

 Establezca las invariantes en el constructor (no en funciones

(166)

Reglas generales

Minimice los bloques try explícitos

Represente los recursos directamente

 Prefiera RAII en el código donde sea posible

 Evite manejo de memoria ad hoc con new/delete. Piense en

funciones que asignan recursos y retornan punteros “básicos” (fopen, malloc, strdup,etc.)

Mantenga el código altamente estructurado (“estilizado”)

 El “código aleatorio” oculta ciertamente problemas con las

Referencias

Documento similar

Antes que nada, conviene subrayar que, a diferencia de lo mantenido por algunos sectores doctrinales, el Tribunal no utiliza dicho precepto para justificar excepciones al principio

Y esa es una cuestión grave, porque la coeducación no forma parte de nuestra formación inicial (salvo honrosas excepciones) y es, por tanto, imprescindible, que la

Cuando Parra afirma que «la vida no tiene sentido», para Julio Ortega parece claro que lo dice desde «el absurdo hallado» y que este encuentro se produce precisamente contra

Se comienza con los minerales mas comunes en la mayoría de las mineralizaciones estudiadas, con algunas excepciones, es decir se describen los sulfuros de los metales de

En primer lugar porque el Reglamento, que parece no incluir entre las excepciones a los inmuebles, no puede prevalecer contra la Ley de Expropiación Forzosa que al referirse a

La más significativa de esas excepciones se encuentra en las calificadas, tanto por el Tribunal Constitucional como por el Tribunal Supremo, como &#34;situaciones o relaciones

Son, por lo tanto, honradas excepciones el estudio y la recopilación de textos en torno a la homosexualidad realizado por Richard Cle- minson (Cleminson, 1995) y sobre todo,

2 Introducción El origen de la dificultad para expresar formalmente la diferencia entre reglas por defecto y reglas excepcionales se encuentra en la forma de asignar estructura