Módulo 7: Orchestrating The Full Reservo System
Un blackboard, compartido por los tres patrones a la vez
Descripción
Los tres Track de PLAN ya corren juntos, con run_tracks_parallel (lección 04) manejando el
pipeline, el fan-out plano y el fan-out-con-handoff (lección 05) sin distinguir entre ellos. Esta
lección agrega la última pieza: un Blackboard (Módulo 6) compartido por toda la corrida, con una
restricción de diseño que no existía cuando el Módulo 6 lo construyó sobre una corrida
completamente secuencial: ninguna escritura puede ocurrir mientras los tres tracks corren en
paralelo — porque dos hilos escribiendo al mismo objeto mutable, al mismo tiempo, es exactamente
el tipo de condición de carrera que ninguna lección de esta guía introduce a la ligera.
La solución es simple y ya la anticipaste, sin saberlo, en la lección 04: run_tracks_parallel
devuelve sus resultados después de que el with ThreadPoolExecutor(...) as pool: termina —es
decir, después de que los tres hilos ya cerraron—. Todas las escrituras al Blackboard de esta
lección ocurren, sin excepción, fuera de esa sección paralela: antes de que empiece (el
supervisor, que ya sabe quién es el socio) o después de que termina (booking_agent, escribiendo
los hechos reales de la reserva).
Conexión con el módulo
Esta lección reusa, sin cambios, Blackboard y WriteLogEntry del Módulo 6, y la corrida completa
de tres tracks de la lección 05. Lo único nuevo es cuándo se llama a bb.write — antes y después
de la sección paralela, nunca durante—. La lección 07 toma exactamente esta misma corrida y le
agrega la respuesta final compuesta para Ana, cerrando el módulo.
Analogía: la pizarra, consultada antes y después del turno más ocupado
Vuelve a la pizarra de la sala de guardia del Módulo 6. Durante el momento más ocupado del turno —tres médicos atendiendo a la vez, cada uno su propio caso— nadie corre hacia la pizarra a mitad de una consulta para anotar algo a las apuradas; eso sería un caos, con anotaciones a medio escribir cruzándose entre sí. Lo que sí ocurre es: antes de que empiece ese tramo ocupado, alguien anota lo único que ya se sabe con certeza (qué paciente llegó); y después, cuando cada médico terminó su parte y tiene un resultado real y completo en la mano, recién ahí lo anota en la pizarra, con calma, uno a la vez.
Ejemplo trabajado: el Blackboard antes y después de los tres tracks
import ast
import concurrent.futures
import itertools
from dataclasses import dataclass, field
import reservo_tools as rt
def dispatch_parallel(tool_use_blocks, tools):
with concurrent.futures.ThreadPoolExecutor(max_workers=len(tool_use_blocks)) as pool:
futures = [pool.submit(tools[b["name"]], **b["input"]) for b in tool_use_blocks]
results = [f.result() for f in futures]
return [
{"type": "tool_result", "tool_use_id": b["id"], "content": str(r)}
for b, r in zip(tool_use_blocks, results)
]
def run_agent_parallel(question, model_script, tools, max_iterations=10):
messages = [{"role": "user", "content": question}]
for step in range(max_iterations):
turn = model_script[step]
messages.append({"role": "assistant", "content": turn["content"]})
if turn["stop_reason"] != "tool_use":
return turn, messages
tool_result_blocks = dispatch_parallel(turn["content"], tools)
messages.append({"role": "user", "content": tool_result_blocks})
raise RuntimeError(f"max_iterations alcanzado ({max_iterations})")
POLICY_DOCS = {
"no-show-policy": (
"Si un miembro no se presenta a una reserva confirmada y no cancela "
"con al menos 2 horas de anticipación, Reservo cobra el 50% del "
"precio cotizado como cargo por no-presentación."
),
"cancellation-policy": (
"Las reservas se pueden cancelar sin cargo hasta 2 horas antes del "
"horario reservado. Cancelaciones dentro de esas 2 horas aplican "
"el cargo de no-presentación."
),
}
def search_docs(query):
q = query.lower()
if "cancela" in q:
return f"[cancellation-policy] {POLICY_DOCS['cancellation-policy']}"
if "no" in q and ("present" in q or "show" in q):
return f"[no-show-policy] {POLICY_DOCS['no-show-policy']}"
return "No se encontró una política relevante para esa pregunta."
SPECIALISTS = {
"booking_agent": {
"tools": {
"list_rooms": rt.list_rooms, "get_quote": rt.get_quote,
"book_room": rt.book_room, "cancel_booking": rt.cancel_booking,
},
},
"policy_agent": {"tools": {"search_docs": search_docs}},
"pricing_agent": {"tools": {"get_quote": rt.get_quote}},
}
def run_specialist(name, task, model_script):
tools = SPECIALISTS[name]["tools"]
return run_agent_parallel(task, model_script, tools)
@dataclass
class PipelineStage:
kind: str
name: str
label: str
def last_tool_result(history):
for m in reversed(history):
if isinstance(m["content"], list):
for b in m["content"]:
if b["type"] == "tool_result":
try:
return ast.literal_eval(b["content"])
except (ValueError, SyntaxError):
return b["content"]
return None
def build_stage_task(stage, payload):
if stage.kind == "quote":
return f"Cotiza {payload['room']} {payload['tier']} {payload['hours']}h."
if stage.kind == "validate_policy":
return (f"¿Cuál es la política de cancelación para una reserva de "
f"{payload['room']} de {payload['hours']}h, antes de confirmarla?")
if stage.kind == "confirm":
if payload.get("cleared_to_book"):
return (f"Reserva {payload['room']} {payload['tier']} {payload['hours']}h "
f"para {payload['member']} -- la política de cancelación ya se validó.")
return (f"Reserva {payload['room']} {payload['tier']} {payload['hours']}h "
f"para {payload['member']}.")
raise ValueError(f"no sé armar la tarea de la etapa {stage.kind!r}")
def extract_payload(stage, history, payload):
new_payload = dict(payload)
result = last_tool_result(history)
if stage.kind == "quote":
new_payload["price_cents"] = result["price_cents"]
elif stage.kind == "validate_policy":
new_payload["cancellation_policy"] = result
new_payload["cleared_to_book"] = True
elif stage.kind == "confirm":
new_payload["booking_id"] = result["booking_id"]
new_payload["confirmed"] = result["confirmed"]
return new_payload
def run_pipeline(stages, model_scripts, initial_payload):
payload = dict(initial_payload)
trace = []
for i, stage in enumerate(stages):
task = build_stage_task(stage, payload)
final, history = run_specialist(stage.name, task, model_scripts[i])
payload = extract_payload(stage, history, payload)
trace.append({
"stage": i + 1, "kind": stage.kind, "agent": stage.name,
"label": stage.label, "task": task, "history": history,
"output": final["content"][0]["text"],
})
return payload, trace
def run_tracks_parallel(jobs):
results = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=len(jobs)) as pool:
future_to_key = {pool.submit(fn): key for key, fn in jobs.items()}
for future in concurrent.futures.as_completed(future_to_key):
key = future_to_key[future]
results[key] = future.result()
return results
HANDOFF_TOOL_NAME = "handoff_to_specialist"
@dataclass
class HandoffPackage:
sender: str
receiver: str
reason: str
task: str
context: dict = field(default_factory=dict)
def run_agent_with_handoff(question, model_script, tools, self_name, max_iterations=10):
messages = [{"role": "user", "content": question}]
for step in range(max_iterations):
turn = model_script[step]
messages.append({"role": "assistant", "content": turn["content"]})
if turn["stop_reason"] != "tool_use":
return turn, messages, None
block = turn["content"][0]
if block["name"] == HANDOFF_TOOL_NAME:
inp = block["input"]
package = HandoffPackage(
sender=self_name, receiver=inp["receiver"], reason=inp["reason"],
task=inp["task"], context=inp.get("context", {}),
)
return None, messages, package
tool_result_blocks = dispatch_parallel(turn["content"], tools)
messages.append({"role": "user", "content": tool_result_blocks})
raise RuntimeError(f"max_iterations alcanzado ({max_iterations})")
def run_specialist_with_handoff(name, task, model_script):
tools = SPECIALISTS[name]["tools"]
return run_agent_with_handoff(task, model_script, tools, self_name=name)
def run_with_handoff(name, task, model_scripts):
final, history, package = run_specialist_with_handoff(name, task, model_scripts[name])
trace = [{"agent": name, "history": history, "package": package}]
if package is None:
return final, trace
receiver_final, receiver_history, receiver_package = run_specialist_with_handoff(
package.receiver, package.task, model_scripts[package.receiver],
)
trace.append({"agent": package.receiver, "history": receiver_history, "package": receiver_package})
return receiver_final, trace
# ---- M6: Blackboard (sin cambios) ----
WRITE_SEQ = itertools.count(1)
@dataclass
class WriteLogEntry:
seq: int
writer: str
field: str
value: object
@dataclass
class Blackboard:
member: str | None = None
room: str | None = None
tier: str | None = None
hours: int | None = None
price_cents: int | None = None
booking_id: int | None = None
log: list = field(default_factory=list)
def write(self, writer, **fields):
for key, value in fields.items():
setattr(self, key, value)
self.log.append(
WriteLogEntry(seq=next(WRITE_SEQ), writer=writer, field=key, value=value)
)
PIPELINE_STAGES = [
PipelineStage(kind="quote", name="booking_agent", label="cotizar"),
PipelineStage(kind="validate_policy", name="policy_agent",
label="validar la política de cancelación"),
PipelineStage(kind="confirm", name="booking_agent", label="confirmar la reserva"),
]
INITIAL_PAYLOAD = {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}
model_script_quote = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Focus pro 3h cuesta 6000 centavos."}]},
]
model_script_policy = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "política de cancelación"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Puedes cancelar sin cargo hasta 2 horas antes del horario "
"reservado. No hay ningún impedimento para confirmar."
)}]},
]
model_script_confirm = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Reservé Focus pro 3h para Ana (confirmación #1)."}]},
]
def job_book_focus():
return run_pipeline(
PIPELINE_STAGES,
[model_script_quote, model_script_policy, model_script_confirm],
INITIAL_PAYLOAD,
)
model_script_pricing = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 3}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 3}},
]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Studio pro 3h: 9600 centavos. Boardroom pro 3h: 19200 centavos. "
"Studio es la opción más barata de las dos."
)}]},
]
def job_compare_rooms():
return run_specialist("pricing_agent", "Compara Studio y Boardroom pro 3h.", model_script_pricing)
model_script_boardroom_booking = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 2}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": HANDOFF_TOOL_NAME,
"input": {
"receiver": "policy_agent",
"reason": "la pregunta de no-presentación es política, fuera de mi expertise",
"task": "¿qué pasa si un miembro no se presenta a una reserva confirmada?",
"context": {"room": "Boardroom", "tier": "pro", "hours": 2, "price_cents": 12800},
}}]},
]
model_script_boardroom_policy = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "qué pasa si no me presento a mi reserva"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Si no te presentas a una reserva confirmada y no cancelas con al "
"menos 2 horas de anticipación, Reservo cobra el 50% del precio "
"cotizado como cargo por no-presentación."
)}]},
]
model_scripts_boardroom = {
"booking_agent": model_script_boardroom_booking,
"policy_agent": model_script_boardroom_policy,
}
def job_boardroom_no_show():
return run_with_handoff(
"booking_agent", "Cotiza Boardroom pro 2h. ¿Qué pasa si no llego?", model_scripts_boardroom,
)
bb = Blackboard()
print("=== antes de abrir los tracks: el supervisor escribe lo único que ya sabe ===")
bb.write("supervisor", member="Ana")
print(f"bb.member = {bb.member!r}")
print()
print("--- tres tracks a la vez: pipeline, fan-out plano, y un fan-out que ADENTRO hace handoff ---")
jobs = {
"book_focus": job_book_focus,
"compare_rooms": job_compare_rooms,
"boardroom_no_show": job_boardroom_no_show,
}
results = run_tracks_parallel(jobs)
print("claves según llegaron (informativo):", list(results.keys()))
print()
print("=== salida SIEMPRE en el mismo orden (sorted por key del track) ===")
for key in sorted(results):
print(f"--- {key} ---")
if key == "book_focus":
payload, trace = results[key]
print(" payload final:", payload)
elif key == "compare_rooms":
final, history = results[key]
print(" respuesta:", final["content"][0]["text"])
elif key == "boardroom_no_show":
final, trace = results[key]
print(f" {trace[0]['agent']} cedió el turno a {trace[0]['package'].receiver} "
f"(razón: {trace[0]['package'].reason!r})")
print(" respuesta final:", final["content"][0]["text"])
print()
print("=== después de cerrar el pool (sin escrituras concurrentes): booking_agent "
"escribe los hechos REALES de la reserva ===")
payload_focus, _ = results["book_focus"]
bb.write("booking_agent", room=payload_focus["room"], tier=payload_focus["tier"],
hours=payload_focus["hours"], price_cents=payload_focus["price_cents"])
bb.write("booking_agent", booking_id=payload_focus["booking_id"])
print(bb)
print()
print("--- el log completo: ¿refleja el orden real de los hilos? ---")
for entry in bb.log:
print(f" #{entry.seq} {entry.writer:<14} escribió {entry.field}={entry.value!r}")
print()
print("compare_rooms y boardroom_no_show no escribieron nada -- son cotizaciones de "
"comparación, no la reserva de registro (misma regla de M6, lección 07).")
Qué esperar (corrida real; la línea de "claves según llegaron" puede variar entre ejecuciones — el resto, no):
=== antes de abrir los tracks: el supervisor escribe lo único que ya sabe ===
bb.member = 'Ana'
--- tres tracks a la vez: pipeline, fan-out plano, y un fan-out que ADENTRO hace handoff ---
claves según llegaron (informativo): ['compare_rooms', 'boardroom_no_show', 'book_focus']
=== salida SIEMPRE en el mismo orden (sorted por key del track) ===
--- boardroom_no_show ---
booking_agent cedió el turno a policy_agent (razón: 'la pregunta de no-presentación es política, fuera de mi expertise')
respuesta final: Si no te presentas a una reserva confirmada y no cancelas con al menos 2 horas de anticipación, Reservo cobra el 50% del precio cotizado como cargo por no-presentación.
--- book_focus ---
payload final: {'room': 'Focus', 'tier': 'pro', 'hours': 3, 'member': 'Ana', 'price_cents': 6000, 'cancellation_policy': '[cancellation-policy] Las reservas se pueden cancelar sin cargo hasta 2 horas antes del horario reservado. Cancelaciones dentro de esas 2 horas aplican el cargo de no-presentación.', 'cleared_to_book': True, 'booking_id': 1, 'confirmed': True}
--- compare_rooms ---
respuesta: Studio pro 3h: 9600 centavos. Boardroom pro 3h: 19200 centavos. Studio es la opción más barata de las dos.
=== después de cerrar el pool (sin escrituras concurrentes): booking_agent escribe los hechos REALES de la reserva ===
Blackboard(member='Ana', room='Focus', tier='pro', hours=3, price_cents=6000, booking_id=1, log=[WriteLogEntry(seq=1, writer='supervisor', field='member', value='Ana'), WriteLogEntry(seq=2, writer='booking_agent', field='room', value='Focus'), WriteLogEntry(seq=3, writer='booking_agent', field='tier', value='pro'), WriteLogEntry(seq=4, writer='booking_agent', field='hours', value=3), WriteLogEntry(seq=5, writer='booking_agent', field='price_cents', value=6000), WriteLogEntry(seq=6, writer='booking_agent', field='booking_id', value=1)])
--- el log completo: ¿refleja el orden real de los hilos? ---
#1 supervisor escribió member='Ana'
#2 booking_agent escribió room='Focus'
#3 booking_agent escribió tier='pro'
#4 booking_agent escribió hours=3
#5 booking_agent escribió price_cents=6000
#6 booking_agent escribió booking_id=1
compare_rooms y boardroom_no_show no escribieron nada -- son cotizaciones de comparación, no la reserva de registro (misma regla de M6, lección 07).
Ejecuta este ejemplo varias veces seguidas y compara: la línea de "claves según llegaron" cambia de
corrida en corrida —confirmando que los tres hilos sí compiten de verdad—, pero el log del
Blackboard, de la primera a la última entrada, sale idéntico cada vez: #1 siempre
supervisor, #2 a #6 siempre booking_agent, en ese orden exacto. El log no está registrando
"cuándo terminó cada hilo" — está registrando el orden en que el hilo principal, ya con los tres
resultados en la mano, decidió llamar a bb.write. Y ese orden es, por diseño, siempre el mismo.
Por qué ninguna escritura ocurre "durante" la sección paralela
Vale la pena ser preciso sobre por qué esto es seguro, no solo afortunado. run_tracks_parallel
devuelve su diccionario results recién después de que el bloque with concurrent.futures.ThreadPoolExecutor(...) as pool: termina — y ese with no termina hasta que
los tres future.result() ya se resolvieron. En ningún punto del código de esta lección hay una
llamada a bb.write dentro de job_book_focus, job_compare_rooms o job_boardroom_no_show — las
tres funciones devuelven datos (un payload, un final, un trace), nunca tocan bb
directamente. Todas las escrituras están, literalmente, fuera de la indentación de cualquier hilo:
una antes de run_tracks_parallel(jobs), dos después. Esta es una decisión de diseño explícita de
esta lección, no una casualidad — un sistema real que sí necesitara escribir al Blackboard desde
dentro de un track paralelo tendría que resolver la sincronización entre hilos con cuidado (un
Lock, por ejemplo), un problema que esta guía no construye porque el diseño de "escribir solo
fuera de la sección paralela" lo evita de raíz.
Errores comunes
-
Intentar llamar a
bb.writedentro de uno de losjob_*para "ahorrar una línea" al final. Rompe la garantía de esta lección: si dos jobs escribieran al mismoBlackboarddesde hilos distintos, al mismo tiempo, el orden de las entradas del log dejaría de ser predecible —y, dependiendo de qué tan compleja fuera la escritura, podría incluso corromper el estado intermedio del objeto—. La disciplina de "escribe solo fuera delwith" no es un detalle de estilo, es lo que hace segura toda esta lección. -
Pensar que el log "no sirve" porque no refleja el verdadero orden de finalización de los hilos. Sirve para exactamente lo que el Módulo 6 ya estableció: reconstruir QUIÉN escribió QUÉ y en qué SECUENCIA de decisiones del sistema — no para cronometrar qué tan rápido corrió cada hilo. Esas son dos preguntas distintas, y el log solo responde la primera.
-
Olvidar que
compare_roomsyboardroom_no_showno escriben nada. Igual que en el Módulo 6, lección 07, esta asimetría no es un accidente — solobooking_agent, en el trackbook_focus, produce un hecho de registro (una reserva real) que vale la pena que el resto del sistema pueda leer después. -
Ejecutar esta lección en un proceso que ya tenía reservas. Si
booking_idno sale1, el proceso ya corrió otro ejemplo de la guía antes — cada lección asume un Reservo desechable, recién iniciado.
Ejercicios
Ejercicio 1: Confirma tu propia ejecución, varias veces (Fácil)
Ejecuta el ejemplo trabajado tres veces seguidas, en el mismo proceso. Confirma que el log del
Blackboard —de #1 a #6— sale idéntico en las tres corridas, aunque la línea de "claves según
llegaron" varíe.
Ver solución
No hay una única "solución de código" para este ejercicio — es una verificación: si el log sale
idéntico en las tres corridas (aunque el orden de llegada de los hilos varíe), confirmaste con tus
propios ojos la garantía central de esta lección: las escrituras al Blackboard son deterministas
porque ocurren fuera de la sección paralela, no porque los hilos terminen siempre en el mismo orden.
Ejercicio 2: ¿Qué leería pricing_agent si intentara consultar bb.member DURANTE el fan-out? (Medio)
job_compare_rooms no lee el Blackboard en el ejemplo trabajado. Modifícalo para que, antes de
armar su tarea, lea bb.member (ya escrito por el supervisor ANTES de que empezara la sección
paralela) y lo use para personalizar el saludo de su respuesta. Ejecuta la corrida completa y
confirma que esa lectura, aunque ocurre DENTRO de un hilo, es segura.
Ver solución
def job_compare_rooms_personalized(bb):
member_read = bb.member # lectura DENTRO de un hilo, pero de un campo que
# ya se escribió ANTES de abrir la sección paralela
final, history = run_specialist("pricing_agent", "Compara Studio y Boardroom pro 3h.", model_script_pricing)
return final, history, member_read
bb2 = Blackboard()
bb2.write("supervisor", member="Ana")
jobs_ex2 = {
"book_focus": job_book_focus,
"compare_rooms": lambda: job_compare_rooms_personalized(bb2),
"boardroom_no_show": job_boardroom_no_show,
}
results_ex2 = run_tracks_parallel(jobs_ex2)
_, _, member_leido = results_ex2["compare_rooms"]
print("member leído dentro del hilo de compare_rooms:", member_leido)
Salida esperada:
member leído dentro del hilo de compare_rooms: Ana
Explicación: esta lectura es segura porque bb2.member ya tenía su valor final ('Ana') desde
ANTES de que run_tracks_parallel abriera el ThreadPoolExecutor — ningún hilo modifica
bb2.member durante la sección paralela, así que leerlo desde dentro de un hilo no compite con
ninguna escritura concurrente. La regla de esta lección no prohíbe TODAS las operaciones sobre el
Blackboard dentro de un hilo — prohíbe las ESCRITURAS durante la sección paralela; las lecturas de
campos que ya se estabilizaron antes de esa sección son seguras, exactamente como esta demuestra.
Ejercicio 3: Simula qué pasaría con una escritura DENTRO de la sección paralela (Difícil)
Sin necesidad de provocar una condición de carrera real y difícil de reproducir, razona: si
job_book_focus y job_boardroom_no_show ambos intentaran escribir al mismo Blackboard
—cada uno desde su propio hilo, al mismo tiempo, cada uno con su propio booking_id—, ¿qué campo
del Blackboard terminaría reflejando el resultado final, y por qué eso sería un bug, aunque
Python no lance ninguna excepción?
Ver solución
No hace falta ejecutar nada para razonar esto con el código de la lección 02 del Módulo 6 en mente:
Blackboard.write hace setattr(self, key, value) — una asignación directa, sin ningún chequeo de
"¿alguien más ya escribió esto en este mismo instante?". Si dos hilos llamaran a
bb.write("booking_agent", booking_id=1) y bb.write("booking_agent", booking_id=99)
"simultáneamente" (en la práctica, el GIL de Python serializa las operaciones individuales de
setattr, así que una de las dos siempre se ejecuta estrictamente después de la otra, pero CUÁL de
las dos es la última no está definido por el código — depende del scheduler del sistema operativo).
El resultado sería que bb.booking_id terminaría con el valor de lo que sea que se haya escrito
al final, sin ningún error, sin ninguna advertencia — un typo silencioso de un tipo distinto al que
ya viste en el Módulo 6, lección 02 (Ejercicio 3), pero igual de peligroso: el Blackboard reportaría
una reserva como la reserva "de registro" cuando, en una corrida distinta, podría haber sido la
otra. Esto es exactamente lo que la disciplina de esta lección —escribir solo fuera de la sección
paralela— evita de raíz, sin necesitar ningún Lock: si cada booking_id se escribe en su propio
momento, secuencial, después de que su hilo ya terminó, nunca hay dos escrituras compitiendo por el
mismo campo al mismo tiempo.
Resumen y siguiente paso
- El
Blackboardde esta lección solo se escribe fuera de la sección paralela: una vez antes (el supervisor,member), dos veces después (booking_agent, los hechos reales de la reserva). - Confirmado ejecutando varias veces: el orden de llegada de los hilos varía entre corridas, pero el
logdelBlackboard—de#1a#6— sale idéntico siempre, porque refleja el orden de las escrituras del hilo principal, no el orden de finalización de los hilos. - Las LECTURAS de campos ya estabilizados (como
bb.member, escrito antes de abrir el pool) son seguras incluso dentro de un hilo — lo que la disciplina de esta lección prohíbe son las ESCRITURAS durante la sección paralela. - Sin esta disciplina, dos escrituras concurrentes al mismo campo producirían un resultado sin ningún error visible, pero potencialmente incorrecto — un typo silencioso de una naturaleza distinta a los que ya viste en el Módulo 6.
Siguiente lección: 07 — La corrida completa, ejecutada de punta a punta. Juntamos las seis lecciones de este módulo en un solo script: la petición completa de Ana, resuelta con los tres patrones y el blackboard compartido, con la respuesta final compuesta para el socio.
Recursos adicionales
- Python — El Global Interpreter Lock (GIL) — El mecanismo que hace que operaciones individuales como
setattrno se corrompan a mitad de camino bajo hilos concurrentes, aunque el ORDEN entre operaciones distintas siga sin estar definido. - Python —
dataclasses— El módulo detrás deBlackboardyWriteLogEntry, reusado sin cambios desde el Módulo 6. - Anthropic — Multi-agent research system — Un sistema real donde el estado compartido se actualiza en puntos de sincronización explícitos, no en cualquier momento arbitrario de la ejecución paralela.
- Python —
threading.Lock— El mecanismo que SÍ haría falta si un diseño real necesitara escribir al estado compartido desde dentro de hilos concurrentes — fuera del alcance de esta lección, que lo evita por diseño.