En informática , un lenguaje de programación consta de una sintaxis más un modelo de ejecución . El modelo de ejecución especifica el comportamiento de los elementos del lenguaje. Al aplicar el modelo de ejecución, se puede derivar el comportamiento de un programa que fue escrito en términos de ese lenguaje de programación. Por ejemplo, cuando un programador "lee" un código, en su mente, recorre lo que hace cada línea de código. En efecto, simula el comportamiento dentro de su mente. Lo que hace el programador es aplicar el modelo de ejecución al código, lo que da como resultado el comportamiento del código.
Todos y cada uno de los lenguajes de programación tienen un modelo de ejecución, que determina la manera en que se programan las unidades de trabajo (que se indican mediante la sintaxis del programa) para su ejecución . Entre los ejemplos detallados de la especificación de los modelos de ejecución de algunos lenguajes populares se incluyen los de Python , [1] el modelo de ejecución del lenguaje de programación Unified Parallel C (UPC), [2] un análisis de varias clases de modelos de ejecución, como los de lenguajes imperativos frente a los funcionales , [3] y un artículo que analiza los modelos de ejecución para lenguajes integrados en tiempo real . [4]
La semántica operacional es un método para especificar el modelo de ejecución de un lenguaje. El comportamiento observado de un programa en ejecución debe coincidir con el comportamiento derivado de la semántica operacional (que define el modelo de ejecución del lenguaje).
Un modelo de ejecución cubre aspectos como qué es una unidad de trabajo indivisible y cuáles son las restricciones sobre el orden en que pueden realizarse esas unidades de trabajo. Por ejemplo, la operación de suma es una unidad de trabajo indivisible en muchos lenguajes y, en lenguajes secuenciales, dichas unidades de trabajo están limitadas a realizarse una tras otra.
Para ilustrar esto, considere el lenguaje de programación C , como se describe en el libro de Kernighan y Richie. [5] C tiene un concepto llamado declaración. La especificación del lenguaje define una declaración como un fragmento de sintaxis que termina con un ";". La especificación del lenguaje luego dice que "la ejecución del programa procede una declaración después de la otra, en secuencia". Esas palabras: "la ejecución del programa procede una declaración después de la otra, en secuencia" son una parte del modelo de ejecución de C. Esas palabras nos dicen que las declaraciones son unidades de trabajo indivisibles y que proceden en el mismo orden que su aparición sintáctica en el código (excepto cuando una declaración de control como IF o FOR modifica el orden). Al afirmar que "la ejecución del programa procede una declaración después de la otra, en secuencia", el modelo de programación ha establecido restricciones sobre el orden de realización de las unidades de trabajo.
El lenguaje C tiene un nivel adicional en su modelo de ejecución, que es el orden de precedencia. El orden de precedencia establece las reglas para el orden de las operaciones dentro de una sola declaración. El orden de precedencia puede verse como la declaración de las restricciones para la ejecución de las unidades de trabajo que están dentro de una sola declaración. Por lo tanto, ";", "IF" y "WHILE" cubren las restricciones sobre el orden de las declaraciones, mientras que el orden de precedencia cubre las restricciones sobre el trabajo dentro de una declaración. Por lo tanto, estas partes de la especificación del lenguaje C también son parte del modelo de ejecución del lenguaje C.
Los modelos de ejecución también pueden existir independientemente de los lenguajes de programación, como por ejemplo la biblioteca POSIX Threads y el modelo de programación Map-Reduce de Hadoop . La implementación de un modelo de ejecución puede realizarse mediante un compilador o un intérprete y, a menudo, incluye un sistema de tiempo de ejecución .
Una implementación de un modelo de ejecución controla el orden en el que se lleva a cabo el trabajo durante la ejecución. Este orden puede elegirse con antelación, en algunas situaciones, o puede determinarse dinámicamente a medida que avanza la ejecución. La mayoría de los modelos de ejecución permiten distintos grados de ambas cosas. Por ejemplo, el lenguaje C fija el orden del trabajo dentro de una declaración y fija el orden de todas las declaraciones, excepto aquellas que involucran una declaración IF o una forma de declaración de bucle. Por lo tanto, la mayor parte del orden de ejecución puede elegirse estáticamente, antes de que comience la ejecución, pero una pequeña parte debe elegirse dinámicamente, a medida que avanza la ejecución.
Las opciones estáticas se implementan con mayor frecuencia dentro de un compilador , en cuyo caso el orden de trabajo se representa por el orden en el que se colocan las instrucciones en el binario ejecutable. Las opciones dinámicas se implementarían entonces dentro del sistema de ejecución del lenguaje . El sistema de ejecución puede ser una biblioteca, a la que se llama mediante instrucciones insertadas por el compilador , o el sistema de ejecución puede estar integrado directamente en el ejecutable , por ejemplo, insertando instrucciones de bifurcación, que toman decisiones dinámicas sobre qué trabajo realizar a continuación.
Sin embargo, también se puede construir un intérprete para cualquier idioma, en cuyo caso todas las decisiones sobre el orden de ejecución son dinámicas. Un intérprete puede considerarse en parte traductor y en parte implementación del modelo de ejecución.
Los lenguajes ensambladores también tienen modelos de ejecución, al igual que cualquier otro lenguaje. Un modelo de ejecución de este tipo se implementa mediante una microarquitectura de CPU. Por ejemplo, tanto una secuencia de ejecución en orden de 5 etapas como una CPU grande fuera de orden implementan el mismo modelo de ejecución de lenguaje ensamblador. El modelo de ejecución es la definición del comportamiento, por lo que todas las implementaciones, ya sean en orden o fuera de orden o interpretadas o JIT, etc., deben dar exactamente el mismo resultado, y ese resultado está definido por el modelo de ejecución.
En la era moderna, la programación paralela es un tema cada vez más importante. Los modelos de ejecución paralela tienden a ser complejos porque involucran múltiples líneas de tiempo. Los modelos de ejecución paralela necesariamente incluyen el comportamiento de construcciones de sincronización. Una construcción de sincronización tiene el efecto de establecer un orden entre las actividades en una línea de tiempo en relación con las actividades en otra línea de tiempo.
Por ejemplo, una construcción de sincronización común es el bloqueo. Considere una línea de tiempo. La línea de tiempo tiene un punto en el que ejecuta la construcción de sincronización "obtener la propiedad del bloqueo". En los subprocesos Posix, esto sería pthread_mutex_lock(&myMutex). En Java, esto sería lock.lock(). En ambos casos, la línea de tiempo se llama subproceso. Los modelos de ejecución de C y Java son secuenciales y establecen que la línea de tiempo tiene actividades que vienen antes de la llamada para "obtener la propiedad del bloqueo", y actividades que vienen después de la llamada. Del mismo modo, existe una operación de "ceder la propiedad del bloqueo". En C, esto sería pthread_mutex_unlock(&myMutex). En Java, esto sería lock.unlock(). Nuevamente, los modelos de ejecución de C y Java definen que un grupo de declaraciones se ejecuta antes de que se ceda la propiedad del bloqueo, y otro grupo de declaraciones se ejecuta después de que se ceda la propiedad del bloqueo.
Ahora, considere el caso de dos líneas de tiempo, también conocidas como dos subprocesos. Un subproceso, llamado subproceso A, ejecuta algunas instrucciones, llamadas instrucciones A-pre-gain-lock. Luego, el subproceso A ejecuta "gain owned of the lock" (obtiene la propiedad del bloqueo), luego el subproceso A ejecuta instrucciones A-post-gain-lock (posteriores a la obtención del bloqueo), que vienen después de que A obtiene la propiedad del bloqueo. Finalmente, el subproceso A realiza "give up owned of the lock" (ceder la propiedad del bloqueo). Luego, el subproceso A realiza instrucciones A-post-giveup-lock (ceder el bloqueo).
Un segundo hilo, llamado hilo B, ejecuta algunas instrucciones, llamadas instrucciones B previas al bloqueo. Luego, el hilo B ejecuta "obtener la propiedad del bloqueo" y, a continuación, el hilo B ejecuta instrucciones B posteriores al bloqueo, que se producen después de que B obtiene la propiedad del bloqueo.
Ahora, podemos hablar del modelo de ejecución paralela de la construcción de sincronización de "obtención de la propiedad del bloqueo" y "cesión de la propiedad del bloqueo". El modelo de ejecución es el siguiente:
"En el caso de que la propiedad del bloqueo pase del hilo A al hilo B, las declaraciones A-post-gain-lock vienen antes que las declaraciones B-post-gain-lock".
La complicación surge del hecho de que el modelo de ejecución no tiene ningún medio para que la ejecución de "ceder la propiedad del bloqueo" tenga alguna influencia sobre qué ejecución de "obtener la propiedad del bloqueo" en alguna otra línea de tiempo (hilo) sigue. Muy a menudo, solo ciertas transferencias dan resultados válidos. Por lo tanto, el programador debe pensar en todas las combinaciones posibles de un hilo que cede un bloqueo y otro hilo que lo obtiene a continuación, y asegurarse de que su código solo permita combinaciones válidas.
El único efecto es que las instrucciones A-post-gain-lock vienen antes que las instrucciones B-post-gain-lock. No ocurre ningún otro efecto y no se puede confiar en ningún otro orden relativo. En concreto, A-post-give-up-lock y B-post-gain-lock no tienen un orden relativo definido, lo que sorprende a mucha gente. Pero el hilo A puede haber sido intercambiado después de ceder la propiedad, por lo que las instrucciones A-post-give-up-lock pueden ocurrir mucho después de que muchas instrucciones B-post-gain-lock hayan finalizado. Esa es una de las posibilidades que se deben tener en cuenta al diseñar bloqueos e ilustra por qué la programación multihilo es difícil.
Los lenguajes paralelos modernos tienen modelos de ejecución mucho más fáciles de usar. El modelo de subprocesos fue uno de los modelos de ejecución paralela originales, lo que puede explicar por qué ha persistido a pesar de ser difícil de usar.