Python에서 Asyncio로 멀티코어 성능 활용
이것은 Python Concurrency 열에 있는 내 기사 중 하나 이며 유용하다고 생각되면 여기 에서 나머지 기사를 읽을 수 있습니다 .
소개
이 기사에서는 다중 코어 CPU에서 Python asyncio 코드를 실행하여 동시 작업의 전체 성능을 잠금 해제하는 방법을 보여줍니다.
우리의 문제는 무엇입니까?
asyncio는 하나의 코어만 사용합니다.
이전 기사에서 Python asyncio를 사용하는 메커니즘을 자세히 다루었습니다. 이 지식을 통해 asyncio가 다중 스레드 작업 전환 중에 GIL 경합 프로세스를 우회하도록 작업 실행을 수동으로 전환하여 고속으로 IO 바인딩 작업을 실행할 수 있음을 알 수 있습니다.
이론적으로 IO 바운드 작업의 실행 시간은 시작부터 IO 작업 응답까지의 시간에 따라 달라지며 CPU 성능과는 무관합니다. 따라서 수만 개의 IO 작업을 동시에 시작하고 신속하게 완료할 수 있습니다.
그러나 최근에 나는 수만 개의 웹 페이지를 동시에 크롤링해야 하는 프로그램을 작성하고 있었고, 내 asyncio 프로그램이 웹 페이지의 반복 크롤링을 사용하는 프로그램보다 훨씬 더 효율적이지만 여전히 오래 기다려야 한다는 것을 알게 되었습니다. 내 컴퓨터의 전체 성능을 사용해야 합니까? 그래서 작업 관리자를 열고 다음을 확인했습니다.
처음부터 내 코드가 하나의 CPU 코어에서만 실행되고 있었고 다른 여러 코어는 유휴 상태였습니다. 네트워크 데이터를 가져오기 위해 IO 작업을 시작하는 것 외에도 태스크는 데이터가 반환된 후 압축을 풀고 형식을 지정해야 합니다. 작업의 이 부분은 많은 CPU 성능을 소비하지 않지만 더 많은 작업 후에 이러한 CPU 바인딩 작업은 전체 성능에 심각한 영향을 미칩니다.
내 asyncio 동시 작업을 여러 코어에서 병렬로 실행하고 싶었습니다. 내 컴퓨터의 성능을 짜낼까요?
asyncio의 기본 원칙
이 퍼즐을 풀기 위해 기본 asyncio 구현인 이벤트 루프부터 시작해야 합니다.
그림에서 보듯이 프로그램에 대한 asyncio의 성능 향상은 IO 집약적인 작업에서 시작됩니다. IO 집약적인 작업에는 HTTP 요청, 파일 읽기 및 쓰기, 데이터베이스 액세스 등이 포함됩니다. 이러한 작업의 가장 중요한 기능은 CPU가 차단되지 않고 외부 데이터가 반환되기를 기다리는 동안 컴퓨팅에 많은 시간을 소비한다는 것입니다. 특정 결과를 계산하기 위해 CPU를 항상 점유해야 하는 다른 종류의 동기 작업과는 매우 다릅니다.
asyncio 작업 배치를 생성할 때 코드는 먼저 이러한 작업을 대기열에 넣습니다. 이때 큐에서 한 번에 하나의 작업을 가져와 실행하는 이벤트 루프라는 스레드가 있습니다. 작업이 await 문에 도달하고 기다리면(일반적으로 요청 반환을 위해) 이벤트 루프는 큐에서 다른 작업을 가져와서 실행합니다. 이전 대기 작업이 콜백을 통해 데이터를 가져올 때까지 이벤트 루프는 이전 대기 작업으로 돌아가 나머지 코드 실행을 완료합니다.
이벤트 루프 스레드는 하나의 코어에서만 실행되므로 "나머지 코드"가 CPU 시간을 차지하면 이벤트 루프가 차단됩니다. 이 범주의 작업 수가 많으면 각각의 작은 차단 세그먼트가 합쳐져 프로그램 전체가 느려집니다.
내 솔루션은 무엇입니까?
이것으로부터 우리는 Python 코드가 하나의 코어에서만 이벤트 루프를 실행하고 IO 데이터 처리로 인해 프로그램 속도가 느려지기 때문에 asyncio 프로그램이 느려진다는 것을 알고 있습니다. 각 CPU 코어에서 이벤트 루프를 시작하여 실행하는 방법이 있습니까?
우리 모두 알다시피 Python 3.7부터 모든 asyncio 코드는 메서드를 사용하여 실행하는 것이 좋습니다. 메서드는 asyncio.run
다음 코드 대신 코드를 실행하기 위해 이벤트 루프를 호출하는 고수준 추상화입니다.
try:
loop = asyncio.get_event_loop()
loop.run_until_complete(task())
finally:
loop.close()
이전 기사에서는 실제 예제를 사용하여 asyncio의 loop.run_in_executor
방법을 사용하여 프로세스 풀에서 코드 실행을 병렬화하는 동시에 기본 프로세스에서 각 자식 프로세스의 결과를 가져오는 방법을 설명했습니다. 이전 기사를 읽지 않았다면 여기에서 확인할 수 있습니다.
따라서 우리의 솔루션이 등장합니다. 메서드를 통해 멀티 코어 실행을 사용하여 여러 하위 프로세스에 많은 동시 작업을 배포한 loop.run_in_executor
다음 asyncio.run
각 하위 프로세스를 호출하여 해당 이벤트 루프를 시작하고 동시 코드를 실행합니다. 다음 다이어그램은 전체 흐름을 보여줍니다.
녹색 부분은 우리가 시작한 하위 프로세스를 나타냅니다. 노란색 부분은 우리가 시작한 동시 작업을 나타냅니다.
시작 전 준비
작업 구현 시뮬레이션
문제를 해결하기 전에 시작하기 전에 준비해야 합니다. 이 예제에서는 웹 콘텐츠를 크롤링하는 실제 코드를 작성할 수 없습니다. 대상 웹 사이트에 매우 성가신 일이 될 수 있기 때문입니다. 따라서 코드를 사용하여 실제 작업을 시뮬레이트합니다.
코드에서 알 수 있듯이 먼저 asyncio.sleep
임의의 시간에 IO 작업 반환을 시뮬레이션하는 데 사용하고 반복 합계를 사용하여 데이터가 반환된 후 CPU 처리를 시뮬레이션합니다.
전통적인 코드의 효과
다음으로, 우리는 기본 메서드에서 10,000개의 동시 작업을 시작하는 전통적인 접근 방식을 취하고 이 동시 작업 배치에 소비되는 시간을 관찰합니다.
그림에서 볼 수 있듯이 코어가 하나만 있는 asyncio 작업을 실행하면 시간이 더 오래 걸립니다.
코드 구현
다음으로 순서도에 따라 멀티코어 asyncio 코드를 구현하고 성능이 향상되는지 확인해 봅시다.
코드의 전체 구조 설계
먼저, 설계자로서 전체 스크립트 구조, 필요한 메서드, 각 메서드가 수행해야 하는 작업을 먼저 정의해야 합니다.
각 방법의 특정 구현
그런 다음 각 방법을 단계별로 구현해 보겠습니다.
이 query_concurrently
메서드는 지정된 작업 배치를 동시에 시작하고 asyncio.gather
메서드를 통해 결과를 가져옵니다.
메서드 run_batch_tasks
는 자식 프로세스에서 직접 시작되므로 비동기 메서드가 아닙니다.
마지막으로 우리의 방법이 있습니다 main
. loop.run_in_executor
이 메소드는 메소드를 호출하여 run_batch_tasks
프로세스 풀에서 메소드를 실행하고 하위 프로세스 실행 결과를 목록으로 병합합니다.
다중 프로세스 스크립트를 작성하고 있으므로 if __name__ == “__main__”
기본 프로세스에서 기본 메서드를 시작하려면 다음을 사용해야 합니다.
코드를 실행하고 결과를 확인
다음으로 스크립트를 시작하고 작업 관리자에서 각 코어의 로드를 확인합니다.
보시다시피 모든 CPU 코어가 활용됩니다.
마지막으로 코드 실행 시간을 관찰하고 멀티스레드 asyncio 코드가 실제로 코드 실행 속도를 몇 배로 높이는지 확인합니다! 임무 완수!
결론
이 기사에서는 asyncio가 IO 집약적인 작업을 동시에 실행할 수 있지만 동시 작업의 대량 배치를 실행할 때 여전히 예상보다 오래 걸리는 이유를 설명했습니다.
asyncio 코드의 기존 구현 체계에서 이벤트 루프는 하나의 코어에서만 작업을 실행할 수 있고 다른 코어는 유휴 상태이기 때문입니다.
그래서 동시 작업을 병렬로 실행하기 위해 여러 코어에서 각 이벤트 루프를 개별적으로 호출할 수 있는 솔루션을 구현했습니다. 마지막으로 코드의 성능이 크게 향상되었습니다.
내 능력의 한계로 인해 이 문서의 솔루션에는 필연적으로 결함이 있습니다. 귀하의 의견과 토론을 환영합니다. 적극적으로 답변해드리겠습니다.