본문 바로가기

Etc/Study

thread & event loop & task

반응형

1event loop 내 n task

하나의 스레드에는 하나의 event loop 만 실행가능

 

cannot schedule new futures after interpreter shutdown

RuntimeError: Cannot close a running event loop

 

task.cancel()

await task > 무한 대기

 

없으면 

RuntimeError: Cannot close a running event loop

 

그리고 실행하면 

cannot schedule new futures after interpreter shutdown

 

 

 

 

질문 요약:

  • loop1 = create_safe_loop() 실행 후
  • loop2 = create_safe_loop() 실행하면
  • 두 번째 create_safe_loop() 실행 시 이미 존재하는 이벤트 루프(loop1)가 반환되지 않는가?
  • 아직 스레드가 나뉘지 않았으므로, 같은 스레드에서 asyncio.get_running_loop()가 loop1을 반환하는 것 아닌가?

 

  • 같은 스레드에서 실행된다면, asyncio.get_running_loop()는 loop1을 반환함.
  • 즉, loop2 = create_safe_loop()를 실행할 때 같은 스레드에서 실행되므로, 이미 실행된 loop1이 반환될 가능성이 있음.

💡 왜?

  • asyncio.get_running_loop()는 현재 실행 중인 이벤트 루프를 반환.
  • 같은 스레드에서 loop1이 asyncio.set_event_loop(loop1)으로 설정되었으면,
    이후 asyncio.get_running_loop()를 호출하면 loop1이 반환됨.

즉, loop1 = create_safe_loop() 실행 후 loop2 = create_safe_loop()를 실행하면 loop2는 loop1과 동일한 이벤트 루프가 될 가능성이 있음.


📌 2. 해결 방법: 스레드를 먼저 생성하고 loop를 분리해야 함

🔹 loop1과 loop2가 동일한 이벤트 루프가 되지 않도록 하려면,
각각 다른 스레드에서 create_safe_loop()를 실행해야 함.
🔹 즉, loop1을 생성한 후 t1.start()로 실행한 다음 loop2를 생성해야 함.

 

 

 

 

질문 요약:

  • create_safe_loop()와 run_loop()에서 둘 다 asyncio.set_event_loop(loop)를 호출하고 있음.
  • 이것이 중복 등록이 되는가?

📌 1. asyncio.set_event_loop(loop)의 역할과 범위

🔹 asyncio.set_event_loop(loop)는 현재 실행 중인 스레드에서 사용할 기본 이벤트 루프를 설정하는 함수.
🔹 즉, asyncio.get_running_loop()를 호출하면 이 함수로 설정한 이벤트 루프가 반환됨.
🔹 하지만, 각 스레드는 독립적인 실행 환경을 가지므로, 스레드마다 set_event_loop(loop)를 호출해야 함.


📌 2. create_safe_loop()에서 set_event_loop(loop)가 필요한가?

def create_safe_loop():
    try:
        loop = asyncio.get_running_loop()  # ✅ 현재 실행 중인 루프 확인
        if loop.is_closed():
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)  # ✅ 현재 스레드에서 기본 루프 설정
    except RuntimeError:
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)  # ✅ 현재 스레드에서 기본 루프 설정
    return loop

현재 실행 중인 루프(get_running_loop())가 없거나 닫혀 있으면 새로운 루프를 생성하고, 이를 현재 스레드의 기본 루프로 설정.
하지만, 이 함수는 main() 함수가 실행되는 메인 스레드에서만 실행됨.
즉, create_safe_loop()는 이벤트 루프를 생성하는 역할을 하며, 이후 다른 스레드에서 실행되는 루프와는 관계가 없음.

📌 3. run_loop()에서 set_event_loop(loop)가 필요한가?

def run_loop(loop):
    asyncio.set_event_loop(loop)  # ✅ 현재 실행 중인 스레드에서 이벤트 루프 설정
    loop.run_forever()

여기서 asyncio.set_event_loop(loop)는 반드시 필요함!
이벤트 루프는 create_safe_loop()에서 만든 것이지만, 그것은 main()이 실행되는 스레드에서 설정된 것.
새로운 스레드에서 run_loop()를 실행할 때, 해당 스레드에서 loop를 기본 루프로 설정하지 않으면 asyncio.get_running_loop()를 호출할 수 없음.

 

 

4. create_safe_loop()에서 set_event_loop(loop)를 제거할 수 있는가?

만약 create_safe_loop()에서 asyncio.set_event_loop(loop)를 제거하면, main()에서 asyncio.get_running_loop()를 호출할 수 없게 됨.
즉, main()에서 실행되는 async 함수들이 실행될 기본 루프가 설정되지 않음.
따라서, create_safe_loop()에서 set_event_loop(loop)는 그대로 유지하는 것이 좋음.


5. run_loop()에서 set_event_loop(loop)를 제거할 수 있는가?

아니요, run_loop()에서 set_event_loop(loop)를 제거하면 해당 스레드에서 loop를 기본 루프로 설정할 수 없음.
즉, asyncio.get_running_loop()를 호출하면 RuntimeError: There is no current event loop 오류가 발생함.
따라서, run_loop()에서 set_event_loop(loop)를 반드시 유지해야 함.

 

6. 결론

함수set_event_loop(loop) 필요 여부이유

create_safe_loop() ✅ 필요 메인 스레드에서 실행될 루프를 설정해야 함
run_loop() ✅ 필요 새 스레드에서 실행될 루프를 설정해야 함

🚀 최종 정리

create_safe_loop()에서는 asyncio.set_event_loop(loop)를 호출해야 메인 스레드에서 asyncio.get_running_loop()가 가능함.
run_loop()에서는 asyncio.set_event_loop(loop)를 호출해야 새 스레드에서 asyncio.get_running_loop()가 가능함.
즉, 두 곳에서 asyncio.set_event_loop(loop)를 호출하는 것은 중복이 아니라 필수적인 과정. 🚀

 

 

 

Cannot close a running event loop 오류가 떠도 괜찮은가?

결론: 일반적으로 괜찮지 않습니다.
✔ 이 오류는 이벤트 루프가 실행 중인 상태에서 loop.close()를 호출할 때 발생합니다.
정상적인 종료 과정에서 이 오류가 발생했다면, 루프를 제대로 정리하지 않았다는 신호일 가능성이 큽니다.
✔ 다만, 서비스 종료 직전이라면 무시해도 동작에는 영향을 미치지 않을 수 있습니다.


📌 1. 오류 원인: loop.close()를 호출했지만 루프가 아직 실행 중

🔹 이 오류가 발생하는 대표적인 상황

  1. 아직 실행 중인 Task가 남아 있음 → loop.stop()을 호출하기 전에 Task가 끝나지 않음.
  2. 다른 스레드에서 실행 중인 루프를 닫으려고 함 → asyncio.get_running_loop()가 반환하는 루프가 현재 스레드에서 실행 중이면 닫을 수 없음.
  3. asyncio.run() 내부에서 loop.close()를 직접 호출 → asyncio.run()이 자동으로 루프를 닫으므로, loop.close()를 별도로 호출하면 오류가 발생할 수 있음.

2. 해결 방법: loop.close() 호출 전에 loop.stop()을 먼저 실행

🔹 루프를 닫기 전에 반드시 실행 중인 Task를 모두 정리해야 함. 🔹 Task를 취소한 후, 루프를 stop()하고 종료해야 함.

🔹 올바른 루프 종료 코드

import asyncio

async def task():
    while True:
        print("Running task...")
        await asyncio.sleep(1)

async def main():
    loop = asyncio.get_running_loop()
    t = asyncio.create_task(task())

    await asyncio.sleep(5)  # ✅ 5초 후 종료
    t.cancel()

    try:
        await t  # ✅ Task가 정상 종료될 때까지 대기
    except asyncio.CancelledError:
        print("Task was cancelled.")

    loop.stop()  # ✅ 먼저 루프 중지
    loop.close()  # ✅ 그 후 루프 종료

asyncio.run(main())  # ✅ 실행

이제 Cannot close a running event loop 오류 없이 안전하게 종료됨.
loop.stop()을 먼저 실행하고, 실행 중인 Task가 종료된 후 loop.close()를 호출해야 함.


3. asyncio.run()을 사용할 때 loop.close()를 직접 호출하면 안 됨

🔹 asyncio.run()은 내부적으로 루프를 자동으로 닫음. 🔹 즉, asyncio.run(main())을 실행한 후 loop.close()를 직접 호출하면 오류가 발생할 수 있음.

async def main():
    print("Asyncio is running!")
    await asyncio.sleep(1)

asyncio.run(main())  # ✅ `loop.close()`를 직접 호출하지 않아야 함.

 

따라서 asyncio.run() 내부에서 실행된 루프를 직접 닫을 필요가 없음.


4. 멀티 스레드 환경에서 Cannot close a running event loop가 발생하는 경우

🔹 다른 스레드에서 실행 중인 루프를 loop.close()로 닫으려고 하면 오류 발생. 🔹 해결 방법: loop.call_soon_threadsafe()를 사용하여 안전하게 종료.

import asyncio
import threading

def run_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()

loop = asyncio.new_event_loop()
t = threading.Thread(target=run_loop, args=(loop,), daemon=True)
t.start()

# ✅ 안전한 종료 방법
def shutdown():
    print("Stopping loop...")
    loop.stop()
    loop.call_soon_threadsafe(loop.close)

shutdown()

 

따라서 asyncio.run() 내부에서 실행된 루프를 직접 닫을 필요가 없음.


🚀 결론

이 오류가 발생하면 일반적으로 문제가 있는 것이므로 수정하는 것이 좋음.
루프를 닫기 전에 반드시 loop.stop()을 먼저 호출해야 함.
asyncio.run() 내부에서 loop.close()를 직접 호출하지 않아야 함.
멀티 스레드 환경에서는 loop.call_soon_threadsafe(loop.close)를 사용하여 안전하게 종료해야 함. 🚀

 

 

 

RuntimeError: Cannot close a running event loop가 call_soon_threadsafe(loop.close) 이후에도 발생하는 이유 및 해결 방법

이벤트 루프가 loop.run_forever()로 실행 중인 상태에서 loop.close()를 호출하면 RuntimeError가 발생할 수 있음.
이 오류는 이벤트 루프를 완전히 중지하지 않고 loop.close()를 호출했을 때 발생함.
따라서 loop.close()를 호출하기 전에 반드시 loop.stop()을 실행하여 루프가 완전히 중지되었는지 확인해야 함.

 

📌 1. loop.close() 전에 loop.stop()이 실행되지 않으면 발생하는 문제

🔹 loop.call_soon_threadsafe(loop.close)를 호출하더라도, 이벤트 루프가 아직 실행 중이면 RuntimeError가 발생할 수 있음.
🔹 loop.stop()은 이벤트 루프의 실행을 중지하지만, 즉시 종료되지 않고 다음 반복(iteration)에서 중지됨.
🔹 따라서 loop.stop() 호출 직후 바로 loop.close()를 호출하면 여전히 이벤트 루프가 실행 중일 수 있음.

 


2. 해결 방법: loop.stop()을 먼저 실행한 후 loop.close()를 안전하게 실행

🔹 올바른 순서:

  1. loop.call_soon_threadsafe(loop.stop)을 실행하여 이벤트 루프 중지 예약
  2. time.sleep(0.1)으로 잠시 대기하여 이벤트 루프가 중지될 시간을 줌
  3. loop.call_soon_threadsafe(loop.close) 실행
import asyncio
import threading
import time

def run_loop(loop):
    """스레드에서 실행할 이벤트 루프"""
    asyncio.set_event_loop(loop)
    loop.run_forever()

loop = asyncio.new_event_loop()
t = threading.Thread(target=run_loop, args=(loop,), daemon=True)
t.start()

# ✅ 안전한 종료 방법
def shutdown():
    print("Stopping loop...")
    loop.call_soon_threadsafe(loop.stop)  # ✅ 먼저 루프 중지 예약

    time.sleep(0.1)  # ✅ 루프가 멈출 시간을 확보
    loop.call_soon_threadsafe(loop.close)  # ✅ 루프 종료

shutdown()

이제 RuntimeError: Cannot close a running event loop 오류 없이 정상적으로 종료 가능.
루프가 중지될 시간을 확보하기 위해 time.sleep(0.1)을 추가하여 loop.close() 호출 시 루프가 완전히 종료되었는지 확인.


🚀 결론

loop.call_soon_threadsafe(loop.close)를 호출해도 이벤트 루프가 완전히 중지되지 않으면 RuntimeError가 발생할 수 있음.
loop.stop()을 먼저 호출한 후, 루프가 멈출 시간을 주고(time.sleep(0.1) 또는 asyncio.sleep(0)) loop.close()를 실행해야 함.
이제 RuntimeError: Cannot close a running event loop 없이 안전하게 이벤트 루프를 종료할 수 있음. 🚀

반응형

'Etc > Study' 카테고리의 다른 글

자료구조 [Queue, Stack, Heap]  (0) 2025.03.27
RAG (Retrieval-Augmented Generation)  (0) 2025.03.27
Thread & Task  (0) 2025.03.27
Semaphore  (0) 2025.03.27