2 minute read

I/O 속도 개선 테스트

A. 일반적인 요청

import time, requests

def crawler(url):
    response = requests.get(url)
    return response.text

def save(count, url):
    result = 0
    for i in range(count):
        result += len(crawler(url))

    return result

url = 'https://www.google.com/'

count = 100

start = time.time()
result = save(count, url)
end = time.time()
print(result)
print(end-start)

결과

1315542
22.966204166412354

이제 2개의 멀티스레드를 사용해보겠다.

스레드

import time, requests
from threading import Thread

result = list()

def crawler(url):
    response = requests.get(url)
    return response.text

def save(count, url, result):
    tmp = 0
    for i in range(count):
        tmp += len(crawler(url))

    result.append(tmp)

url = 'https://www.google.com/'

count = 100

start = time.time()

th1 = Thread(target=save, args=(count//2, url, result))
th2 = Thread(target=save, args=(count//2, url, result))

th1.start()
th2.start()

th1.join()
th2.join()

result = sum(result)

end = time.time()
print(result)
print(end-start)

결과

1316077
11.677412033081055

단순하게 스레드 두개로 나눠서 각각 절반씩 계산했다.

정확한 실험은 아니지만 시간이 단축되어 성능이 좋아졌다.

리스트로 값을 담아서 계산한 이유는 한개의 변수로 값을 관리하면

메모리 가시성 관련되서 일정 시점에 메인 메모리에 증가시킨 값이 보장되지 않는다.

그래서 실행 할때마다 값이 달라지기 때문에 리스트로 값을 관리했다.

하지만 이것만으론 부족하다. 이번엔 비동기로 요청해본다.

비동기

import time, requests, asyncio

url = 'https://www.google.com/'
count = 100


def request(url):
    return requests.get(url)


async def save(future_list):
    result = 0
    for future in future_list:
        response = await future
        result += len(response.text)
    return result


async def main():
    loop = asyncio.get_event_loop()
    future_list = list()
    for i in range(count):
        future_list.append(loop.run_in_executor(None, request, url))

    result = await save(future_list)

    print(result)


start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
end = time.time()

print(end - start)

결과

1315599
2.2710509300231934

비동기를 사용하자 처음에 비해 약 10 배 정도 시간이 단축되었다

여기서 멀티스레딩과 비동기를 함께 사용하면 어떻게 될지 궁금해졌다.

멀티스레딩 + 비동기 조합으로 단축시켜 보겠다.

멀티 스레딩 + 비동기

import requests, asyncio, threading, queue

count = 100
url = 'https://www.google.com/'

q = queue.Queue(10)


def request(url):
    response = requests.get(url)
    return response.text


async def save(future_list):
    result = 0
    for future in future_list:
        tmp = await future
        result += len(tmp)
    return result


async def main(count, url):
    loop = asyncio.get_event_loop()

    future_list = list()
    for i in range(count):
        future_list.append(loop.run_in_executor(None, request, url))

    result = await save(future_list)
    q.put(result)


def task(count, url):
    try:
        loop = asyncio.get_event_loop()
    except RuntimeError:
        loop = asyncio.new_event_loop()
    loop.run_until_complete(main(count, url))


import time

start = time.time()

th1 = threading.Thread(target=task, args=([count // 2, url]))
th2 = threading.Thread(target=task, args=([count // 2, url]))

th1.start()
th2.start()

th1.join()
th2.join()


def sum_q():
    result = 0
    while not q.empty():
        result += q.get()
        q.task_done()
    return result


print("종료 : ", time.time() - start)
print(sum_q())

결과

종료 :  1.5799908638000488
1316344

처음 약 22초 에서 1초 정도로 단축 시켰다

파이썬 비동기 라이브러리인 asyncio 의 이벤트루프를 받아올때 일반적인 사용의 경우에는 별 다른 이상이없었지만,

asyncio.get_event_loop()
현재의 이벤트 루프를 가져옵니다.

현재 OS 스레드에 현재 이벤트 루프가 설정되어 있지 않고, OS 스레드가 메인이고, set_event_loop()가 아직 호출되지 않았으면, asyncio는 새 이벤트 루프를 만들어 현재 이벤트 루프로 설정합니다.

이 함수는 (특히 사용자 정의 이벤트 루프 정책을 사용할 때) 다소 복잡한 동작을 하므로, 코루틴과 콜백에서 get_event_loop()보다 get_running_loop() 함수를 사용하는 것이 좋습니다.

저수준 함수를 사용하여 수동으로 이벤트 루프를 만들고 닫는 대신 asyncio.run() 함수를 사용하는 것도 고려하십시오.

멀티스레딩 사용시

    raise RuntimeError('There is no current event loop in thread %r.'
    raise RuntimeError('There is no current event loop in thread %r.'

해당 오류가 발생해서, RuntimeError 발생시 새로운 이벤트루프를 할당해줬다.

Categories:

Updated: