카테고리 없음

스파르타 AI-8기 TIL(10/11) -> 동시성, 병렬 처리, 메모리 관리

kimjunki-8 2024. 10. 11. 18:30

어제와 바로 이어가겠습니다.

 

4. Pipe 클래스
Pipe 클래스는, 두 프로세스간 서로 데이터를 공유할 수 있는 공간을 마련한다고 생각하시면 됩니다.
send(obj): 파이프를 통해 데이터를 보냅니다.
recv(): 파이프를 통해 데이터를 받습니다.
close(): 파이프를 닫습니다

ㄴ밑에 코드를 보면, upper_info와 lower_info가 Pipe()클래스로 인해 서로 연결이 되었고, lower_info를 info 함수에 인자로 넣으면, 아무리 서로 떨어져있어도, 서로 Pipe로 연결되어 있기에, 정보를 전달할 수 있습니다. 즉, pipe.send로 안에 있는 정보를 전달 했다는 소리입니다. 

그렇기에 데이터를 전달받았습니다 = 3 으로 출력이 되는것을 볼 수 있습니다.
from multiprocessing import Process, Pipe

def info(pipe, a):
    #.send로 정보 전달
    pipe.send(f"데이터를 전달 받았습니다 = {a}")
    #.close로 파이프 닫기
    pipe.close

if __name__ == '__main__':
    #Pipe로 upper와 lower 연결
    upper_info, lower_info = Pipe()
    프로세스 = Process(target = info, args = (lower_info, 3))
    프로세스.start()
    #recv로 데이터 받기
    print(upper_info.recv()) 
    프로세스.join

5. Lock 클래스
Lock 클래스는  여러 프로세스가 동시에 공유 자원에 접근할 때, 한 번에 하나의 프로세스만 자원에 접근할 수 있도록 잠금(lock)을 설정합니다.
추가 메서드:
acquire(): 잠금을 설정하여 자원 접근을 제어합니다.
release(): 잠금을 해제하여 다른 프로세스가 자원에 접근할 수 있게 합니다.
from multiprocessing import Process, Lock
import time

def Lock_fun(락, item):
    time.sleep(1)
    print(f'{item} 사용')
    with 락:
        print(f'{item} 사용중...')
        time.sleep(1)
    print(f'{item} 사용 완료')

if __name__ == '__main__':
    lock = Lock()
    processing = Process(target = Lock_fun, args = (lock, '포션'))
    processing.start()
    processing.join()
여기서 궁금증하나! 저 with은 도대체 어떨 때 쓰는걸까? 그리고 acquire()이랑 release()는 왜 안보이는 걸까?
모든 궁금증은 with as 구문으로 알 수 있습니다!

먼저, with as 구문을 쓸 수 있냐 없냐는 해당 객체에 __enter__()와 __exit__() 메서드를 가지고 있냐 없냐에 따라 달립니다. 있으면 with as 구문을 쓸 수 있고, 없으면 쓸 수 없습니다.
그런데 __enter__()와 __exit__()이 있는지 없는지 어떻게 알 수 있냐는 바로 이미 배운 매타프로그래밍에서 hasattr()로 알 수 있습니다!

예를 들어 이미 만들어 놓은 코드에서, 
# __enter__ 메서드가 있는지 확인
print(hasattr(락, "__enter__"))  # True가 출력되면 __enter__가 있음

# __exit__ 메서드가 있는지 확인
print(hasattr(락, "__exit__"))   # True가 출력되면 __exit__가 있음

이렇게 알 수 있는 것이죠!

두번째로 acquire()이랑 release()에 대해 말하겠습니다. acquire은 잠금을 설정하고, release는 해제하는 역할을 합니다..........
사실 with as구문이 저 두 역할을 다 합니다! 즉, with as구문에 저 두개가 다 들어있다고 생각하시면 됩니다!
with as 구문은 시작할 때 자동으로 파일을 열어주고, 닫을 때 자동을 닫게 해주는 성능 좋은 녀석이므로, acquire이랑 release를 둘 다 시행해주는 구문이라 생각하시면 편합니다.

6. Value 클래스
 프로세스 간에 값을 공유할 수 있게 합니다. 주로 숫자나 문자열과 같은 간단한 데이터 타입을 공유할 때 사용합니다. 그리고 value에서 쓰이는 속성은 하나 밖에 없습니다
value: 공유하는 값에 접근할 때 사용합니다

밑에 코드를 보면, Value('i', 11)로 되어 있습니다. 
자, 저 i는 int의 i를 뜻합니다. 즉, 다른 값으로도 바꿀 수 있다는 뜻입니다('f' = float, 'b' = bool). 그리고 11은 초기값을 말합니다.
위에 벨류.value를 통해 값을 공유한다는 의미를 가지고 있으며, range에 30부터 시작한다고 설정을 해놓아도, 공유값의 시작값을 11로 설정했기 때문에, 10부터 시작한다는 의미를 가지고 있습니다. 하지만 주의 사항은 30으로 설정을 했기 때문에, 30번 작동을 한다는 의미 입니다. 즉, 0부터는 -1, -2, -3.....이렇게 30번을 채우기 전에 끝나지 않는다는 의미입니다.
from multiprocessing import Process, Value
import time

def 감소(벨류):
    for _ in range(30):
        time.sleep(1)
        벨류.value -= 1
        print(f'발사시간: {벨류.value}')
        if 벨류.value == 0:
            time.sleep(1)
            print('미사일 발사!')

if __name__ == '__main__':
    공유값 = Value('i', 11)
    processing = Process(target = 감소, args = (공유값,))
    processing.start()
    processing.join()

7. Array 클래스
Array는 리스트와 같은 형태로 여러 값을 공유할 수 있습니다. 여러 프로세스가 동일한 배열에 접근할 수 있습니다.
형태는 Value 클래스와 매우 유사하지만, 공유하는 값은 리스트의 형태로 전달합니다.

형태는 value와 매우 유사하게, 'i' int를 전달, 그 값을 그대로 받는 것까지 똑같습니다. 하지만 전달하는 값의 형태가 리스트인 것만 다릅니다.
from multiprocessing import Process, Array
import time
import random

def 감소(어레이):
    for l in range(len(어레이)):
        time.sleep(1)
        어레이[l] += 1
        print(f'발사시간: {어레이[l]}')
        if 어레이[l] == 10:
            time.sleep(1)
            print('미사일 발사!')

if __name__ == '__main__':
    공유값 = Array('i', [0,1,2,3,4,5,6,7,8,9])
    processing = Process(target = 감소, args = (공유값,))
    processing.start()
    processing.join()

비동기 I/O (asyncio)
비동기는 스레드와, 프로세싱과 똑같이 프로그램이 입출력 작업을 수행하는 동안 다른 작업을 동시에 처리할 수 있게 해주는 프로그래밍 패턴입니다. 하지만 매우 복잡하기 때문에, 간단하게만 설명하겠습니다.
1. 비동기의 의미
비동기의 개념으로는
비동기 함수: async def 키워드를 사용하여 정의된 함수로, 비동기 작업을 수행할 수 있습니다.
코루틴: 비동기 함수가 반환하는 객체입니다. 이 객체는 await 키워드를 사용하여 실행을 중단하고 다른 작업을 수행할 수 있습니다.
이벤트 루프: 비동기 작업을 실행하고 관리하는 주체입니다. 이벤트 루프는 여러 작업을 동시에 처리할 수 있게 해줍니다.

 

2. 주요 클래스 및 함수
비동기는 import asyncio를 통해 불러올 수 있습니다.

2.1 이벤트 루프
2.1.1 asyncio.get_event_loop(): 현재의 이벤트 루프를 반환합니다. 만약 이벤트 루프가 없으면 새로 생성합니다. 2.1.2 asyncio.run(coro): 주어진 코루틴(coro)을 실행하고 완료될 때까지 대기합니다. 이벤트 루프를 자동으로 관리합니다.
2.2 코루틴과 작업
2.2.1 asyncio.create_task(coro): 코루틴을 Task로 만들어 이벤트 루프에서 실행하도록 예약합니다. 반환값은 Task 객체입니다.
2.3 비동기 I/O 작업
2.3.1 asyncio.sleep(seconds): 지정한 시간(초)만큼 비동기로 대기합니다. 이 함수는 I/O 작업의 예시로 많이 사용됩니다.
3. 비동기 컨텍스트 관리자
asyncio.Lock: 비동기 작업 간의 동기화를 위한 잠금을 제공합니다. 다른 작업이 잠금이 해제될 때까지 대기할 수 있습니다.
4. 비동기 예외 처리
asyncio.CancelledError: 비동기 작업이 취소되었을 때 발생하는 예외입니다

병렬 처리
자, 병렬 처리에 앞서, 먼저 중요한 사실이 있습니다. 사실 병렬 처리는 동시성과 비슷합니다. 실제 코드를 보아도, 육안으로 구별이 어렵습니다. 왜냐하면 둘 다 똑같이 스레드와 프로세싱을 쓰기 때문입니다. 하지만 개념의 차이에서 두개가 나누어집니다. 이 두 개념만 이해하시면 됩니다. (지금은 좀 나에게는 어려운 개념......(포기))
동시성은 작업이 대기 중일 때 다른 작업을 처리하는 방식이며, 보통 I/O 바운드 작업에서 사용됩니다. -> 동시에 처리하는 것처럼 보이게 하는것.

 

병렬 처리는 여러 작업을 실제로 동시에 실행하여 CPU 바운드 작업의 성능을 높이는 방식입니다. -> 실제로 동시 처리

메모리 관리
관리는 컴퓨터 프로그래밍에서 중요한 주제 중 하나로 Python에서는 여러 가지 메모리 관리 기법이 있으며, 이들을 이해하는 것이 프로그램의 성능과 안정성에 매우 중요합니다. 메모리 관리를 통해 코딩을 좀더 효휼화해서 더 안전하고 빠르게 작업을 처리할 수 있습니다.

그럼이제, 메모리 관리 기법에 대해 알아봅시다.


 

메모리 할당
1. 정적 할당: 컴파일 시에 메모리 공간이 할당됩니다. 이 방법은 프로그램의 실행 중에 크기가 변하지 않는 경우에 사용됩니다. 정말 간단하게 변수를 선언하고, 그 안에다 값을 할당하는 경우입니다. 그러면 메모리에 변수의 이름과 함께 공간이 할당되고, 그 공간에 할당할 값이 들어가게 되는것입니다.

2. 동적 할당: 런타임 시에 메모리를 요청합니다. Python의 경우, list, dict, set과 같은 객체는 필요에 따라 메모리를 동적으로 할당받습니다. 이 경우는 실행을 시킬경우, 리스트의 특성과 크기가 맞게 메모리가 알맞게 커지고, 그 안에다 리스트나, 딕셔너리, 혹은 튜플같은 값들이 들어가게 되는것입니다.

3. 메모리 풀: 메모리 할당과 해제를 효율적으로 처리하기 위해 미리 메모리 블록을 할당해 두고, 필요할 때마다 이 블록에서 메모리를 가져다 쓰는 기법입니다. 미리 상자를 만들어놓고, 나중에 필요할 때 쓴다고 생각하시면 편합니다.

 

메모리 해제
메모리 해제는 더 이상 쓸모없는 메모리 공간을 반환하는 작업이라고 생각하시면 됩니다. 대표적으로 del이 있죠. 
1. 참조 카운팅 (Reference Counting)
메모리 관리 기법 중 하나로, 객체의 메모리 해제를 자동으로 관리하기 위해 객체에 대한 참조의 수를 추적하는 방법입니다.
참조 카운팅은 따로 코드가 존재하지 않고, Python에서 자동으로 관리해주는 즉, 내부적으로 동작하는 메모리 관리 기법입니다.
import sys
a = []
print(sys.getrefcount(a))
여기서 저 a의 참조 카운팅은 1입니다. a라는 변수에 []빈 리스트를 할당했기 때문에, a라는 것에 카운팅이 하나 들어가게 됩니다. 그럼 만약에 빈 리스트에 값을 넣으면 카운팅이 바뀔까요? 아닙니다. 리스트를 그 하나의 카운팅 하나로 보기 때문에, 여전히 카운팅은 1이 됩니다.
잠깐! 여기서 저 import sys란?
sys는 Python 표준 라이브러리의 모듈로, Python 인터프리터와 관련된 다양한 기능을 제공합니다. 게다가 인터프리터의 동작을 제어할 수 있는 여러 유용한 도구를 포함하고 있습니다.
1. 인터프리터 관련 정보: Python이 실행되는 운영체제, 버전, 플랫폼 등의 정보를 제공합니다.
2. 입출력 관리: 표준 입력(stdin), 출력(stdout), 에러 출력(stderr)을 제어할 수 있습니다.
3. 메모리 관리: 객체의 참조 카운트나 메모리 관련 정보를 확인할 수 있습니다.
4.명령행 인수: 프로그램 실행 시 전달된 명령행 인수를 처리합니다.
5. 예외 처리: 실행 중 발생한 예외에 대한 정보를 다루는 기능을 포함합니다.
sys.version: 현재 사용 중인 Python 버전을 확인할 수 있습니다.
예:
import sys
print(sys.version)

sys.argv: 프로그램 실행 시 전달된 명령행 인수를 리스트로 저장합니다.
예:
import sys
print(sys.argv)

sys.path: Python이 모듈을 찾는 경로 목록을 제공합니다. 
모듈을 어디서 찾는지 확인하거나 경로를 추가할 때 사용합니다.
예:
import sys
print(sys.path)

sys.getrefcount(): 특정 객체의 참조 카운트를 반환합니다.
예:
import sys
a = []
print(sys.getrefcount(a))  # 객체 a의 참조 카운트를 출력

sys.exit(): 프로그램을 종료할 때 사용합니다.
예:
import sys
sys.exit(0)  # 프로그램을 정상 종료

2. 가비지 컬렉터
일단 가비지 컬랙터는 사용하지 않는 객체를 메모리에서 제거한다는 기본 개념만 들고있으시길 바랍니다. 
import gc로 부르고 gc로 컨틀로 할 수 있습니다.
가비지 컬렉터에 대해 자세히 알려면, 순환 참조, 약한 참조, 강한 참조에 대한 개념이 필요한데.....(너무 복잡하고, 힘들어서 다음으로 미루는.....)

감사합니다! 오늘도 끝!