0votos

La tela de araña en Clojure

por josejuan hace 6 años

Se supone que inmutable (¡¿?!) y concurrente, corre en paralelo sobre tantos hilos como arañas. Mi primer código en Clojure (que por cierto me parece feo, feo)... :P

Un problema fácil de resolver imperativamente... ¿y funcionalmente? (inmutabilidad y transparencia referencial). Aun así (el enfoque funcional), creo que puede ser un buen kata para practicar punteros y/o TAD's.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
; 1. Se supone que inmutable (véase http://clojure.org/refs) pero no debo de 
;    entender como funciona, porque a mi me parece el típico mutable. 
 
; 2. Está paralelizado, cada araña corre en un hilo independiente. 
 
; 3. Feo, feo, feo, ... no me gusta nada la sintáxis, la forma de generar vectores, las refs, ... 
 
 
 
; size, grid y arañas 
(def n 6) 
(def g (vec (map ref (repeat (* n n) 0)))) 
(def s (vec (map ref (map #(do [%1 %1]) (range 0 n))))) 
 
; incrementa (shared) un nodo 
(defn i[n] (dosync (alter (g n) + 1))) 
(defn ii[[x y]] (i (+ x (* y n)))) ; ¡que cosa más fea, por dios! 
 
; mueve una araña (shared) en un toroide 
(defn am[p] (vec (map #(mod (+ n %1) n) (map + p (rand-nth [[0 1] [1 0] [0 -1] [-1 0]]))))) 
(defn aam[z] (dosync (alter (s z) am))) 
 
; spider loop 
(defn sp[z p] (dotimes [r p] (aam z) (ii @(s z)))) 
 
; ejecuta en paralelo el movimiento de las arañas, cada araña da 'p' pasos independientemente de las otras 
(defn spiders[p] (dorun (apply pcalls (map (fn [z] (fn [] (sp z p))) (range 0 n))))) 
 
 
 
 
 
 
 
 
;; NOTAS ;;;;;;;;;;;;;;;;;; 
 
; (todo en base 0) 
 
; para efectuar 'p' pasos (eg. 60) 
=> (spiders 60) 
nil 
 
; para obtener la suma de 'g' 
=> (apply + (map deref g)) 
360 
 
; yo tengo 6 cores, con 6 arañas no pasa del 600% de consumo CPU (¿bloqueos?). :P 
=> (spiders 6000000) 
nil 
 
=> (apply + (map deref g)) 
36000360 
35 comentarios
0votos

Escrito por vemv hace 6 años

Que manía con los nombres de pocas letras ;p sólo les encuentro gracia cuando denominan cosas genéricas, no específicas de dominio. Echándole un buen vistazo, cuando lo entienda te cuento qué tal.
0votos

Escrito por josejuan hace 6 años

Cuestión de gustos, a mí me resulta MUCHO mas fácil leer y revisar un código con nombres cortos, de hecho, suelo ir renombrando (mucho más fácil de largo a corto). Para mí la manía es la de los que ponen nombres Supercalifragilisticoespialidosos ;P

No se si me dejo algún nombre:

n, nº de arañas (nxn el nº de nodos).
g, es el grafo, los nodos están enumerados como "x + y * n"
s, las spiders, las coordenadas vienen como "[x y]"
i, incrementa el valor en el nodo n-ésimo
ii, incrementa el valor en el nodos con posición "[x y]"
am, calcula un movimiento de araña aleatorio dada una posición
aam, mueve la araña n-ésima
sp, mueve una araña un nº de veces dejando las semillas (incrementando nodos)
spiders, mueve en paralelo las arañas un nº de veces
0votos

Escrito por jneira hace 6 años

A mi me gustas el estilo intermedio con abreviaturas de 2 o 3 chars y no mas largas de 8 si puede ser :-P
0votos

Escrito por josejuan hace 6 años

Mi media es 2,222 chars por nombre ;)
0votos

Escrito por vemv hace 6 años

Vale ya lo entiendo, pero antes de comentar... tiene g alguna propiedad especial que permita considerarlo un toroide (que para mí significa "rosquilla") más que como una cuadrícula?
0votos

Escrito por josejuan hace 6 años

Uhm... sí, un toroide es como una rosquilla... ejem, bueno, suele llamarse toroide... en fin, el nombre es lo de menos.

La propiedad no la tiene 'g', la tiene 'am', en concreto la parte que mapea cada coordenada a MOD(n). Por eso no me gusta la sintáxis de Lisp, no permite expresar las cosas como las expresamos los humanos normalmente, igual se ve mejor así (yo sí lo veo menos confuso):

(dx, dy) = rand-nth [(0, 1), (1, 0), (0, -1), (-1, 0)]
(x', y') = ((x dx) MOD n, (y dy) MOD n)

así, si estás en la columna 0 y vas a la izquierda, apareces en la última columna o si estás en la fila 0 y subes, apareces en la última fila. Es decir, tiene estructura topológica toroidal, aunque habitualmente, basta decir que es un toroide. Es algo así como en Matrix, cuando están en la estación de metro virtual practicando y Neo sale por una boca del metro y aparece por la anterior. Aunque no sea evidente, esa estación de metro tiene estructura topológica toroidal, sólo que hay que doblar la habitación para verlo.

Podemos representar un toroide con una cuadrícula, porque podemos transformar una en otra, una forma fácil de verlo es la siguiente: toma un folio y haz un tubo con él... bien, ahora supón que ese tubo es elástico y une la entrada y la salida para formar ¡un toroide!.
0votos

Escrito por josejuan hace 6 años

Mña, me dejé las llaves, perdón:

(dx, dy) = rand-nth [(0, 1), (1, 0), (0, -1), (-1, 0)]
(x', y') = ((x + dx) MOD n, (y + dy) MOD n)
0votos

Escrito por vemv hace 6 años

Mi pregunta no se refería al uso de la palabra toroide (no, no prefiero 'rosquilla' ;p) si no a qué se refiere. Gracias por la explicación!
1votos

Escrito por vemv hace 6 años

Para empezar, mi humilde "refactor" (muy entre comillas) que usé para hacer más manejable el programa para mí. https://gist.github.com/3699728 (puedes ojear un par de trucos ahí tales como mapv, range)

Nombres aparte, últimamente no veo beneficio en crear funciones "auxiliares" como las que has expuesto (e.g. aam). Introducen más indirección que abstracción (http://zedshaw.com/essays/indirectionisnot_abstraction.html); obligan al lector a aprender un pequeño e innecesario lenguaje. Esta idea se está promoviendo tímidamente en el entorno Clojure, porque lo cierto es que tristemente, mucha gente hace esto.

Como bien indicas hay que ponerle un signo de interrogación a "inmutable": crear un vector de refs no puede reflejar una captura consistente del mundo. Un mejor punto de partida sería crear una sola ref, abarcando una colección inmutable de valores (e.g. casillas, arañas). Por otra parte, una granularidad tan grande implica un mayor grado de contención. Esta dicotomía no suele ser fácil de resolver. Ayer precisamente el autor de "Clojure programming" (O'Reilly) publicó https://github.com/cgrand/megaref.

dosync es igual que atomically en Haskell (según veo): al llamar por separado 2 veces a dosync, creas un estado potencialmente inconsistente (las arañas se han movido, pero las frecuencias aún no han incrementado).
0votos

Escrito por josejuan hace 6 años

"funciones "auxiliares""

ayudan a depurar y reutilizar código, es una práctica habitual en programación funcional

"obligan al lector"

no se, para gustos, podrían ponerse inline, pero lisp (clojure) no ayuda mucho a que luego eso quede más claro

"Un mejor punto de partida sería crear una sola ref"

¡no, no! una sola ref probablemente bloquee cualquier proceso ¡y todos los procesos estarán bloqueados! es decir, lograrás un rendimiento mucho peor que no paralelizar (mira la solución de jneira).

"dosync"

para mí "dosync" es lo mismo que el "lock" de C# y otras estructuras concurrentes, no veo que Clojure haya introducido nada especial, sigue siendo el programador el que tiene que hacer el trabajo duro (consistencia y share), ver la ref que puse para más datos.
1votos

Escrito por vemv hace 6 años

>> es una práctica habitual en programación funcional

y una mala, aparte de practicada en ruby o java igualmente. mi profesor se quejaba de cuando yo lo hacía en qbasic ;p tardé años en encontrarle la razón.

>> lisp no ayuda

entre let y destructuring, no veo por qué no...

Por último, en cuanto a consistencia y sharing, arriesgo muy poco si afirmo que sencillamente estás equivocado. A raíz de tu recomendación leí "Beatiful concurrency" de Simon P. Jones, que resume muy bien el uso de la STM y cómo se compara con la técnica del locking. Realmente dirías que ambas equivalen?

En tu solución, leer el estado del mundo (consistentemente) requeriría bloquear.

Para una visión conceptual más completa recomiendo 'Are we there yet?' de Rich Hickey. Un punto a destacar: en la presencia de inmutabilidad, puedes compartirlo todo (nada de defensive copying / copy-on-write).
0votos

Escrito por josejuan hace 6 años

y una mala

dudo que puedas demostrarlo, son el tipo de cosas que si tú dices 10 a favor otro pueden decir 10 en contra; yo lo hago como me da.

estás equivocado

dije "probablemente", no tengo ni idea de como Clojure gestiona la memoria, si nos ceñimos a STM, habrá que ver como afecta un rollback si la operación la realizas sobre 1 elemento o sobre N. No vale con decir STM, hay que decir dónde realizas la transacción.

¿Equivalen?, ¡claro!, solo que STM requiere de mayor esfuerzo para implementarla (transacciones), con un "dosync" realizas la operación tantas veces como sea preciso para que funciones, con "lock" esperas a que la situación sea tal que sólo lo tienes que ejecutar una vez.

Si te refieres a si funcionan igual, internamente no, pero igualmente tienen propiedades en común que son las que he comentado (localidad, que hace que sea mejor bloquear/transaccionar con objetos pequeños que grandes).

En tu solución, leer el estado del mundo (consistentemente) requeriría bloquear

efectivamente, de ahí que si no usas un "ref" para cada nodo, igualmente te va a afectar en cada hilo, ahora creo entrever el comentario de la doc de Clojure (la que referencié) "hay que tratar las 'refs' como si fueran inmutables, sino Clojure no te va a ayudar en nada", de ahí que mi forma de ver el problema siga siendo correcta, es decir, debes ser local.
0votos

Escrito por jneira hace 6 años

  • Esto... en cuanto a ambito de la referencia nuestro codigo tiene la misma "granularidad" (la celda del grid) ¿¿???
  • lock = dosync??? Las referencias en clojure (la STM de ref, atom, agent) es una capa mas abstracta que se construye sobre toda la parafernalia propensa a errores de la concurrencia de bajo nivel. Si te refieres a que dosync usa locks por debajo y supone bloqueos estas en lo cierto.
0votos

Escrito por josejuan hace 6 años

tiene la misma "granularidad"

ni idea a bajo nivel, pero no tendría ningún sentido convertir un vector de números a un vector de referencias de números si no hiciera falta ¿no?

lock = dosync?

Lo que he dicho antes; 'lock = dosync' en cuanto a que aseguran la forma en que se accede a un recurso. La ventaja que veo a STM es que puede abstraer la paralelización en base al lenguaje (eg. en Haskell) ¡pero en Clojure te lo tienes que currar tú!
0votos

Escrito por jneira hace 6 años

¿Que diferencia hay entre haskell y clojure para que en esta ultima tengas que currartelo tu?
0votos

Escrito por josejuan hace 6 años

Si usas haskell "puro", todo es inmutable (de verdad) y por tanto únicamente tienes que usar algo como "par". Ya está; el balanceo de threads, las estrategias (uso eficiente del pool de threads), etc... las puede sacar haskell automáticamente.

La cuestión, si volvemos a mi planteamiento del desafío original, es que aún no hemos conseguido una versión inmutable (de verdad) que efectúe el trabajo sobre las arañas.

En una versión muy simplificada (e incompleta/incorrecta) con dos arañas y usando un doble zipper, sería algo como:

data Zipper2 = Zipper2 ( Zipper1 Zipper1 )


Entonces, hacer un paso sería algo como:

doStep (Zipper2 a b) = par a' (pseq b' (Zipper2 a' b'))
  where a' = doSpider a
        b' = doSpider b


(En la práctica habría un paso adicional que pasaría la araña b al zipper1 y la a al zipper2 cuando intersectan, etc...)

Además, al ser inmutable, el resultado de "doStep" es usable directamente sin incoherencia ninguna (y no como en Clojure, que 'g' va cambiando con el tiempo).

Y quizás lo más importante, es que no debes hacer tu algoritmo en función de la paralelización (como en Clojure teniendo que usar 'ref' cuando lo natural es usar el dato directamente).
0votos

Escrito por jneira hace 6 años

De acuerdo contigo...pero ya no estamos hablando de usar STM en haskell sino de usar los famosos zippers con varios huecos.
Por lo que se usar STM en haskell y en clojure es muy similar en cuanto a ventajas e inconvenientes.
(COmo comente en otro sitio en clojure tambien hay zippers..per de un solo hueco claro)
0votos

Escrito por josejuan hace 6 años

¡Y a mí que con STM!, a mi lo que me importa es un lenguaje en el que pueda hacer las cosas sencillas, seguras y eficientes. :P

No se si haskell usa STM o no internamente cuando hago "par", se que en algunos escenarios es "sencillo" de usar y ya y que la solución que he hecho en Clojure la he paralelizado haciendo el tipo de cosas que hago siempre en otros lenguajes imperativos.
0votos

Escrito por jneira hace 6 años

  • lo de mapv me lo vuelvo a apuntar (ya me comentaste algo en la primera solucion)
  • usar un ambito muy grande o muy pequeño para delimitar la transaccion tiene cada uno su problematica y creo que cgrand intentaba conciliar ambos (tengo que mirar mejor su megaref y subrefs)
  • si las dos operaciones, modificacion sobre el grid y sobre las arañas no se paralelizan en dos hilos sino que son secuenciales.. ¿Por que meterlo en una transaccion? Igual me estoy perdiendo algo muy obvio...
  • de hecho me parece que lo unico que tiene "peligro" de quedarse inconsistente son las celdas del grid (dos incrementos simultaneos que se quedan en uno)
0votos

Escrito por josejuan hace 6 años

¿Por que meterlo en una transaccion?

lo hice inconscientemente, pero está claro que la probabilidad de colisión será menor en dos elementos que en uno que es suma de ambos, es obvio, si lo haces dentro de la misma transacción, y los tiempos de ejecución de los dos pasos secuenciales son t1 y t2, está claro que la probabilidad de colisionar en t1 por un lado y t2 por otro es menor que la de colisionar t1t2 por un lado y t1t2 por otro.

De todos modos, como el coste de gestionar una transacción puede influir (sea éste t0) entonces tenemos que

t1t0 / t2t0 podría superar a t1t2t0

Puedes hacer un bench, a ver que te sale, pero las probabilidades de colisión son importantes (y el coste de la transacción, claro).
0votos

Escrito por jneira hace 6 años

Mmmm yo estaba comparando la transaccion de dosync con usar atoms. Atom es la referencia mas sencilla ya que es poco mas que un lock sobre la lectura+escritura del valor que hay dentro del atom. Si solo vas a modificar un elemento (como en tu codigo) vale con usar atom, si vas a modificar dos o mas y tienen que cambiar de forma consistente hay que usar refs.
Otra cosa es que decidas modificar cada elemento por separado (usando atoms) o los dos a la vez (usando refs). Creo que vemv tenia razon y lo precavido si quieres lecturas consistentes seria meter ambas en una dosync.
0votos

Escrito por jneira hace 6 años

Me explico fatal, quiero decir que atom es mejor si vas a realizar una operacion (aunque dentro hagas n secuencialmente) y ref+dosync si vas a realizar las dos (o mas) operaciones de forma transaccional.
Vamos que en tu codigo se podria sustituir sin modificar su comportamiento ref por atom y dosync+alter por swap!.
Y es que en esencia tu solucion y la mia son muy similares ;-)
0votos

Escrito por jneira hace 6 años

En realidad es aglo mas que un lock sobre el read+write:
Internally, swap! reads the current value, applies the function to it, and attempts to compare-and-set it in. Since another thread may have changed the value in the intervening time, it may have to retry, and does so in a spin loop. 
0votos

Escrito por josejuan hace 6 años

Has ignorado mis comentarios ¿verdad? >(

"it may have to retry, and does so in a spin loop"

por eso he dicho que STM puede llegar a ser peor que interbloqueos si no está bien balanceada la carga y/o la transacción es grande (probabilidad de colisión alta).
0votos

Escrito por josejuan hace 6 años

Ahora que caigo, la información de cada araña es independiente de las demás, por tanto 'aam' no necesita una transacción "dosync" y podemos quitarla ¡pero porque nosotros sabemos que no habrá colisión!.

He aquí un ejemplo (creo que claro) de que es el programador quien debe hacer esa inferencia.
0votos

Escrito por jneira hace 6 años

(La ultima vez que mando un comentario sin guardarlo en el portapapeles)

Me han gustado especialmente los comentarios, aunque el codigo no esta mal (no me esperaba menos)

1.- La solucion es en mi opinion esencialmente mutable ya que las refs son una forma de modificar/consultar variables globales compartidas. Como dice en el primer parrafo del link que citabas:
transactional references (Refs) ensure safe shared use of *mutable* storage locations via a software transactional memory (STM) system

Clojure te ofrece cuatro opciones para modificar variables globales (estado global) y las cuatro son seguras concurrentemente aunque cada una tiene su aplicacion mas adecuada. Las refs se suelen usar cuando se va a modificar dos o mas elementos dentro de una transaccion (de las de sql o ACI) de forma sincrona. Para modificar un elemento de forma aislada y sincrona se recomiendan los atoms.

2.- Lo de paralelizar con futures (o con pmap que lo usa o pcalls que llama a este ultimo) no es "magico". Como dice su (doc pmap):
Only useful for computationally intensive functions where the time of f dominates the coordination overhead.

En mi caso compare usar map o pmap y el primero era el doble de rapido (aunque el orden de complejidad no variaba) Tendria que estudiar el por que y no se si has hecho la prueba de no usar pcall a ver como varia el rendimiento en tu caso.

3.- Si abominas la notacion polaca en la matematica complicado que te guste lisp. Sin embargo no veo que la forma de crear vectores en clojure sea especialmente fea: [x y z] o (vec (list x y z)) o (vector x y z). No se si hay alternativas mas "bonitas" (¿fromList [x,y,z] de haskell?)
Tampoco me parece feo lo de (ref x) o (atom x)..hay alguna alternativa mas "bonita" (breve, expresiva) para crear una referencia con las propiedades que tienen ambas (¿IORef de haskell?)?
0votos

Escrito por josejuan hace 6 años

Si abominas la notacion polaca

no la abomino, sólo que no me gusta, ¿pero tú has visto la forma de escribir una simple ecuación matemática? XD XD

sea especialmente fea

no he dicho que en haskell me guste, pero al menos es puro (no empecemos, ya sabes a lo que me refiero), el uso de IORefs para mi no es admisible, será un mal necesario para quien tenga que usarlos. Y sí, "fromList [1..n]" es mejor que "(vec (range 1 n))" (para mí, por supuesto).

¿Formas más elegantes?, pues hombre:

1. la de C++ es la canónica, que no está mal.
2. la que más me gusta es C# (var v = new Tipo[n]).
3. como concisas ¿javascript? (var v = []).
0votos

Escrito por jneira hace 6 años

Me referia a la sintaxis y he puesto mal el ejemplo de IORef, me referia a que la sintaxis necesaria para crear una referencia valida dentro de una STM no creo que sea mas breve y estetica en otro lenguaje.
En haskell con do notation:
shared <- atomically $ newTVar 0

lo que seria
(def shared (ref 0))

En otros lenguajes oop con la libreria adecuada podria ser algo asi como
var shared=new Ref(0)

y yo no le veo que sea especialmente mas bonito (a mi el new me sobra) pero bueno es cuestion de gustos


En cuanto a los vectores estamos comparando formas diferentes de crearlos.
En clojure un vector tiene notacion especial y directa como en javascript:
(def v [])
(def v [1 2 3])

Para mi es mucho mejor que lo equivalente en este caso (uso seudo-descenciente de c)
var v=new int[]
var v=new int[] {1,2,3}


En cuanto a crear un vector a partir de una coleccion existente o partir de n valores
(def v (vec '(1 2 3)))
(def v (vec coll))

Me parece mejor (al menos mas breve) que la forma de java(script) (no se si c# hay otra forma)
var v=coll.toArray()


Lo que es mejor en haskell son los rangos que es lo que hace que el fromList [1..] sea mas breve pero entre fromList y vec pues ya me diras que diferencia hay (puramente sintactica no entremos en el polimorfismo en funcion del retorno de una funcion que la liamos).. demasiado corta la de clojure? :-P
0votos

Escrito por josejuan hace 6 años

Creo que [] no es un vector en Clojure, es una lista. Pero igual hay azúcar por ahí, a lo que voy, es: mira tus propios ejemplos.

Ahora mete esas sintáxis dentro de otras (como pasa en el código, eg. la solución actual) y verás que se embrolla todo con los paréntesis y "polaca".

Pero bueno, que repito que es cuestión de gustos y simplemente yo no me hago.
0votos

Escrito por jneira hace 6 años

Nooooo [] en haskell son listas en clojure son vectores:
user> (type [])
clojure.lang.PersistentVector
0votos

Escrito por josejuan hace 6 años

Ok, es "map" quien la convierte en secuencia y por eso hace falta el "vec". Bien, algo es algo.
1votos

Escrito por josejuan hace 6 años

Bueno, aunque no creo que me pase a Clojure, me quedo con las siguientes conclusiones, si alguna creéis que es incorrecta, por favor:

1. hay que tratar las operaciones "ref" como si fueran inmutables para que STM se pueda llevar a cabo de forma eficiente (yo interpreto el "como si fuera inmutable" el contenido del "ref" y la forma en que se procesa ¡commit/rollback!).

2. me da en la nariz, que si la transacción es costosa en tiempo, el rendimiento puede llegar a ser desastroso, porque puede darse el caso de transacciones que deban ejecutarse varias veces, por lo que un tratamiento por bloqueos sería mucho más adecuado (sabes que como mucho esperarás X y además, sin consumir CPU).

3. por [1] y [2], está claro (según lo que he visto) que el elemento transaccionado debe ser lo más pequeño posible, si inmutamos una estructura grande en una transacción, es de esperar que el tratamiento interno del lenguaje haga commit/rollback eficientemente (el osasaki del otro día jneira), pero el mayor problema es que la probabilidad de colisión es mucho mayor.

4. ni idea porque no he mirado, pero supongo que por [3] viene lo que comentas jneira de los "modos de paralelizar".

5. por [1], si nos fijamos en los nºs aleatorios que se usan, éstos están dentro de la transacción y dudo que sean inmutables, eso en principio, hace que en caso de tenerse que reiniciar la transacción, se use un nº aleatorio diferente (¿de ahí otra vez lo de tratarlo como si fuera inmutable?).

6. por [3] el tuneo del nº de hilos me parece que será más complicado de ajustar (más crítico), de ahí lo de que con pocos cores STM no va bien, supongo.

En fin, son ideas vagas que se quedan ahí, en la cartuchera.

Por último, ¿alguien postea la ecuación de segundo grado en Clojure? XD XD

# en la mayoría de lenguajes:
var x = (- b + sqrt(b * b - 4 * a * c)) / (2 * a)
0votos

Escrito por jneira hace 6 años

(def x (/ (+ (- b) (sqrt (- (* b b) (* 4 a c))))
          (* 2 a)))


La ventaja es precisamente que no se diferencia entre operadores y funciones. No hay definicion diferenciada, reglas de precedencia, notacion para convertir un operador en funcion y viceversa...todo mas sencillo vaya. Muerto el perro muerta la rabia :-P

Vamos a hacer la operacion inversa con esta
(def y (/ (+ (* a b c) (* (- x y z) 3) 5) 2 3))
0votos

Escrito por josejuan hace 6 años

Es verdad ¡mucho mejor¡ XD XD XD

No se, pero yo esa fórmula la veo picasiana XD XD XD

La que has puesto, si no cojo papel y lápiz va a ser que no sale, supongo que con práctica... ;P
0votos

Escrito por josejuan hace 6 años

Ahora que caigo también (al final me haré daño...), donde pongo:

yo tengo 6 cores, con 6 arañas no pasa del 600% de consumo CPU (¿bloqueos?). :P 


¡con 6 cores no se puede pasar del 600%! XD XD XD

es decir, concuerda con STM que no se interrumpen los procesamientos (¡ojo! ya hemos visto que esto no quiere decir que sea mejor, puede que se esté "quemando gasolina" inútilmente si debe repetirse la transacción).

Comenta la solución

Tienes que identificarte para poder publicar tu comentario.