1. python -m venv '폴더명'   ->  가상환경 생성

2. '폴더명' \ Scripts 로 이동해 activate.bat 실행

3. django-admin startproject config .     

  ->  Scripts 에서 빠져나오기 위해 cd .. 로 메인으로 이동한 후 마지막에 . 붙여줘야 해당 폴더에 project 생성할 수 있다.

4. config/settings.py 언어와 시간 수정

LANGUAGE_CODE = 'ko-kr'
TIME_ZONE = 'Asia/Seoul'

5. App 생성하기 

   -> python manage.py startapp 'App 이름' 

6. URL Mapping 

   -> config / urls.py 

 

 

* Migrations

1. python manage.py make makemigrations

2. python manage.py migrate

 

'Python 프로그래밍' 카테고리의 다른 글

PYQT 계산기 source code  (0) 2022.09.23
[Python] *args와 **kwargs  (0) 2022.07.22
[Python] Asyncio  (0) 2022.06.05
[Python] GIL  (0) 2022.06.03
[Python] Coroutine  (0) 2022.06.02
# importing libraries
from PyQt5.QtWidgets import *
from PyQt5 import QtCore, QtGui
from PyQt5.QtGui import *
from PyQt5.QtCore import *

import sys


class Window(QMainWindow):

    def __init__(self):
        super().__init__()
        self.setWindowTitle("호준이의 킹받는 계산기")
        self.setGeometry(100, 100, 360, 350)
        self.UiComponents()
        self.show()

    def UiComponents(self):
        self.label = QLabel(self)
        self.label.setGeometry(5, 5, 350, 70)
        self.label.setWordWrap(True)
        self.label.setStyleSheet("QLabel"
                                 "{"
                                 "border : 4px solid black;"
                                 "background : white;"
                                 "}")

        self.label.setAlignment(Qt.AlignRight)
        self.label.setFont(QFont('Arial', 15))

        push1 = QPushButton("1", self)
        push1.setGeometry(5, 150, 80, 40)
        push2 = QPushButton("2", self)
        push2.setGeometry(95, 150, 80, 40)
        push3 = QPushButton("3", self)
        push3.setGeometry(185, 150, 80, 40)
        push4 = QPushButton("4", self)
        push4.setGeometry(5, 200, 80, 40)
        push5 = QPushButton("5", self)
        push5.setGeometry(95, 200, 80, 40)
        push6 = QPushButton("6", self)
        push6.setGeometry(185, 200, 80, 40)
        push7 = QPushButton("7", self)
        push7.setGeometry(5, 250, 80, 40)
        push8 = QPushButton("8", self)
        push8.setGeometry(95, 250, 80, 40)
        push9 = QPushButton("9", self)
        push9.setGeometry(185, 250, 80, 40)
        push0 = QPushButton("0", self)
        push0.setGeometry(5, 300, 80, 40)
        push_equal = QPushButton("=", self)
        push_equal.setGeometry(275, 300, 80, 40)

        c_effect = QGraphicsColorizeEffect()
        c_effect.setColor(Qt.blue)
        push_equal.setGraphicsEffect(c_effect)

        push_plus = QPushButton("+", self)
        push_plus.setGeometry(275, 250, 80, 40)
        push_minus = QPushButton("-", self)
        push_minus.setGeometry(275, 200, 80, 40)
        push_mul = QPushButton("*", self)
        push_mul.setGeometry(275, 150, 80, 40)
        push_div = QPushButton("/", self)
        push_div.setGeometry(185, 300, 80, 40)
        push_point = QPushButton(".", self)
        push_point.setGeometry(95, 300, 80, 40)
        push_clear = QPushButton("Clear", self)
        push_clear.setGeometry(5, 100, 200, 40)
        push_del = QPushButton("Del", self)
        push_del.setGeometry(210, 100, 145, 40)

        push_minus.clicked.connect(self.action_minus)
        push_equal.clicked.connect(self.action_equal)
        push0.clicked.connect(self.action0)
        push1.clicked.connect(self.action1)
        push2.clicked.connect(self.action2)
        push3.clicked.connect(self.action3)
        push4.clicked.connect(self.action4)
        push5.clicked.connect(self.action5)
        push6.clicked.connect(self.action6)
        push7.clicked.connect(self.action7)
        push8.clicked.connect(self.action8)
        push9.clicked.connect(self.action9)
        push_div.clicked.connect(self.action_div)
        push_mul.clicked.connect(self.action_mul)
        push_plus.clicked.connect(self.action_plus)
        push_point.clicked.connect(self.action_point)
        push_clear.clicked.connect(self.action_clear)
        push_del.clicked.connect(self.action_del)

    def action_equal(self):
        equation = self.label.text()
        try:
            ans = eval(equation)
            # 킹받게 만들기.. (거꾸로 만들어서 보내주기..)
            kingVal = (str(ans)[::-1])
            self.label.setText(str(kingVal))
        except:
            self.label.setText("Wrong Input")

    def action_plus(self):
        text = self.label.text()
        self.label.setText(text + " + ")

    def action_minus(self):
        text = self.label.text()
        self.label.setText(text + " - ")

    def action_div(self):
        text = self.label.text()
        self.label.setText(text + " / ")

    def action_mul(self):
        text = self.label.text()
        self.label.setText(text + " * ")

    def action_point(self):
        text = self.label.text()
        self.label.setText(text + ".")

    def action0(self):
        text = self.label.text()
        self.label.setText(text + "0")

    def action1(self):
        text = self.label.text()
        self.label.setText(text + "1")

    def action2(self):
        text = self.label.text()
        self.label.setText(text + "2")

    def action3(self):
        text = self.label.text()
        self.label.setText(text + "3")

    def action4(self):
        text = self.label.text()
        self.label.setText(text + "4")

    def action5(self):
        text = self.label.text()
        self.label.setText(text + "5")

    def action6(self):
        text = self.label.text()
        self.label.setText(text + "6")

    def action7(self):
        text = self.label.text()
        self.label.setText(text + "7")

    def action8(self):
        text = self.label.text()
        self.label.setText(text + "8")

    def action9(self):
        text = self.label.text()
        self.label.setText(text + "9")

    def action_clear(self):
        self.label.setText("")

    def action_del(self):
        text = self.label.text()
        print(text[:len(text) - 1])
        self.label.setText(text[:len(text) - 1])

App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())

'Python 프로그래밍' 카테고리의 다른 글

Django Setting  (0) 2022.11.02
[Python] *args와 **kwargs  (0) 2022.07.22
[Python] Asyncio  (0) 2022.06.05
[Python] GIL  (0) 2022.06.03
[Python] Coroutine  (0) 2022.06.02

*args

 *args가 무엇인지 알아보기에 앞서, 파이썬 프로그램에서 해당 문법을 사용할 때 꼭 *args라고 작성할 필요는 없다. args는 변수명이기 때문에 어떠한 변수명을 사용해도 무관하다. 하지만 앞의 *(별표) 는 꼭 붙어있어야 한다. 즉, *hojun 으로 사용해도 전혀 문제가 없다.

 본격적으로 *args란 '가변인자'를 위한 변수입니다. 간단하게 풀어 설명하자면 함수의 인자를 즉, 파라미터를 몇 개 받을지 모르는 경우에 사용하게 됩니다. 

def lastName_and_FirstName(*args):
	for name in args:
    	print("%S %s" % (args[0], args[1:3], end=' ')
    print("\n")
    
lastName_and_FirstName('김호준', '김민구')
lastName_and_FirstName('김호준', '김민구', '김재우', '김종환')

# 출력 예시
# 김 호준 김 민구
# 김 호준 김 민구 김 재우 김 종환

 lastName_and_FirstName 함수에서는 *args를 인자로 받습니다. 하지만 *args는 *names나 *a, *b 등 아무 단어로 바꾸어도 상관없이 동작합니다. 또한 args를 출력해보면 tuple(튜플) 형태임을 알 수 있습니다. 여러 개의 인자로 함수를 호출할 때, 함수 내부에서는 튜블로 받은 것 처럼 인식한다는 것이죠

 

**kwargs

 kwargs는 keyword argument의 줄임말로 키워드를 제공합니다. 또한 kwargs는 args와 마찬가지로 변수명은 hojun처럼 아무렇게나 입력해 사용해도 무관하다. 하지만 앞의 ** 두개는 필수라는 것을 명심하자.

 또한 위의 args는 튜플 형태로 저장되지만, kwargs는 args와 비슷하면서 다르다. kwargs는 딕셔너리 형태로 값을 저장하며, args와 달리 파라미터명을 같이 보낼 수 있다. 

def name_and_age(**kwargs):
	print(kwargs)
    
name_and_age(name="김호준", age="28")

# 출력예시
# {'age':'50', 'name':'김호준'}

name_and_age 함수 안에 kwargs라는 딕셔너리를 만들고 그 안에 age와 name을 넣는다고 생각하면 될 것이다. kwargs를 조금 다르게 사용하는 예시를 아래에 작성해보았다.

def introduceEnglishName(**kwargs):
	for key, value in kwargs.items():
    	if 'ant' in kwargs.keys():
        	print("작성자님 반갑습니다. 오늘 기분이 어떠신가요?")
        else:
        	print("{0} is {1}" .format(key, value))
            
introduceEnglishName(MyName='Hojun')
introduceEnglishName(ant='Hojun')

# 출력예시
# MyName is Hojun
# 작성자님 반갑습니다. 오늘 기분이 어떠신가요?

 키워드(딕셔너리의 key 부분의 값) 가 ant로 들어왔을 때는 "작성자님 반갑습니다 ... "로 저만의 서비스를 작성해보았습니다. 

 

 *args와 **kwargs 동시에 사용하기

 args와 kwargs를 사용할 때 주의할 점은 순서이다. 일반 변수가 가장 먼저와야 하고 그다음은 args 마지막으로 kwargs가 위치해야 한다. 순서를 바꿔 args가 가장 앞에 위치할 경우 args의 인자가 어디까지인지 파이썬은 파악하지 못하기 때문이다. 

def number_and_name(*args, **kwargs):
    print(args, kwargs)

number_and_name(1, 2, 3, name="홍길동")

### 출력값 ###
(1, 2, 3) {'name': 'GilDong Hong'}

 함수에 키-값 형태로 된 인자를 전달하면 파이썬은 자동으로 kwargs에 저장해 준다. 또한 아래코드처럼 *args를 선언해놓고 사용하지 않을 수도 있다.

def name(*args, **kwargs):
    print(args, kwargs)

name(name="GilDong Hong")

### 출력값 ###
() {'name': 'GilDong Hong'}

이런 경우에는 args에는 아무 값도 저장되지 않고 kwargs에만 키-값 형태로 저장되는 것을 볼 수 있다.

 

'Python 프로그래밍' 카테고리의 다른 글

Django Setting  (0) 2022.11.02
PYQT 계산기 source code  (0) 2022.09.23
[Python] Asyncio  (0) 2022.06.05
[Python] GIL  (0) 2022.06.03
[Python] Coroutine  (0) 2022.06.02

 전통적으로 동시 프로그래밍(concurrent programming)은 여러 개의 쓰레드(thread)를 활용하여 이루어졌었습니다. 하지만 쓰레드를 이용해서 직접 코딩을 해보신 분이라면 겪어보셨겠지만, thread safe한 프로그램을 작성하는 것은 생각보다 쉬운 일이 아닙니다. 게다가 싱글 코어 프로세서에서 이러한 프로그램을 돌리면, 기대했던 동시 처리에 따른 성능 향상을 미미하거나 심지어 성능 저하되기도 합니다.

 이러한 이유로 최근에는 하나의 쓰레드로 동시 처리를 하는 비동기 프로그래밍(asynchronous programming)이 더욱 주목받고 있습니다.

 

비동기 프로그래밍

 웹 서버와 같은 애플리케이션을 생각해보면 CPU 연산 시간 대비 DB나 API와 연동 과정에서 발생하는 대기 시간이 훨씬 길다는 것을 알 수 있습니다. 비동기 프로그래밍은 이러한 대기 시간을 낭비하지 않고 그 시간에 CPU가 다른 처리를 할 수 있도록 하는데 이를 흔히 non-blocking하다고 합니다.

 자바스크립트와 같이 애초에 비동기 방식으로 동작하도록 설계된 언어에서는 익숙한 개념이지만, 파이썬과 같이 기본적으로 동기 방식으로 동작하는 언어에서는 이 기념이 생소하게 느껴질 수도 있습니다. 하지만 파이썬 3.4에서 asyncio가 표준 라이브러리로 추가되고, 파이썬 3.5에서 async/await 키워드가 문법으로 채택이 되면서, 파이썬도 이제 언어 자체적으로 비동기 프로그래밍이 가능해졌습니다.

 

핵심 문법

 def 키워드로 선언하는 모든 함수는 파이썬에서 기본적으로 동기 방식으로 동작한다고 생각하시면 됩니다.

 예를 들어, 다음과 같이 선언된 함수는 동기 함수입니다.

def do_sync():
    pass

 기존 def 키워드 앞에 async 키워드까지 붙이면 이 함수는 비동기 처리되며, 이러한 비동기 함수를 파이썬에서는 코루틴(coroutine)이라고도 부릅니다.

async def do_async():
    pass

 이러한 비동기 함수는 일반 동기 함수가 호출하듯이 호출하면 coroutine 객체가 리턴됩니다.

do_async() # <coroutine object do_async at 0x1038de710>

따라서 비동기 함수는 일반적으로 async로 선언된 다른 비동기 함수 내에서 await 키워드를 붙여서 호출해야 합니다.

async def main_async():
    await do_async()

자바스크립트에서 async로 선언된 비동기 함수를 호출할 때 await 키워드를 붙이지 않으면 Promise 객체를 리턴하는 것과 같은 이치입니다.

async로 선언되지 않은 일반 동기 함수 내에서 비동기 함수를 호출하려면 asyncio 라이브러리의 이벤트 루프를 이용해야합니다.

loop = asyncio.get_event_loop()
loop.run_until_complete(main_async())
loop.close()

파이썬 3.7 이상에서는 다음과 같이 한 줄로 간단히 비동기 함수를 호출 할 수도 있습니다.

asyncio.run(main_async())

 

 

동기와 비동기 실습

지금부터 사용자 관리 애플리케이션을 흉내내는 실습 코드를 작성하면서 동기 처리하는 코드와 비동기 처리를 하는 코드를 비교해보도록 하겠습니다.

억지스럽지만 시뮬레이션을 위해서 다음과 같은 가정을 해보겠습니다.

  • 애플리케이션을 사용자 데이터를 직접 보관하지 않고 외부 API를 호출해서 가져옵니다.
  • 외부 API는 1명의 사용자 데이터를 조회하는데 1초가 걸리고, 한 번에 여러 사용자의 데이터를 조회할 수 없습니다.
  • 각각 3명, 2명, 1명의 사용자 정보를 조회하는 요청 3개가 동시에 애플리케이션에 들어옵니다.

 

동기 Programming

먼저 사용자 데이터 조회를 전통적인 동기 방식으로 처리해주는 find_users_sync 함수를 작성합니다. 의도적으로 1초의 지연 시간을 발생시키기 위해서 time.sleep 함수를 사용하였습니다.

import time

def find_users_sync(n):
    for i in range(1, n + 1):
        print(f'{n}명 중 {i}번 째 사용자 조회 중 ...')
        time.sleep(1)
    print(f'> 총 {n} 명 사용자 동기 조회 완료!')

그 다음, 애플리케이션에 들어온 3개의 요청을 동기 처리하는 process_sync 함수를 작성합니다.

def process_sync():
    start = time.time()
    find_users_sync(3)
    find_users_sync(2)
    find_users_sync(1)
    end = time.time()
    print(f'>>> 동기 처리 총 소요 시간: {end - start}')

if __name__ == '__main__':
    process_sync()

이 함수를 호출해보면 find_users_sync 함수가 총 6초 동안 3번 순차적으로 실행됨을 알 수 있습니다.

3명 중 1번 째 사용자 조회 중 ...
3명 중 2번 째 사용자 조회 중 ...
3명 중 3번 째 사용자 조회 중 ...
> 총 3 명 사용자 동기 조회 완료!
2명 중 1번 째 사용자 조회 중 ...
2명 중 2번 째 사용자 조회 중 ...
> 총 2 명 사용자 동기 조회 완료!
1명 중 1번 째 사용자 조회 중 ...
> 총 1 명 사용자 동기 조회 완료!
>>> 동기 처리 총 소요 시간: 6.020448923110962

만약에 싱글 쓰레드의 웹 서버가 이러한 방식으로 동작한다면 실제 사용자는 얼마나 오랫동안 지연을 경험을 하게 될까요? 동기 처리에서는 첫 번째 함수의 실행이 끝나야 두 번째 함수가 실행되고, 마찬가지로 두 번째 함수가 끝나야 세 번째 함수가 실행됩니다. 즉, 첫 번쨰 요청이 처리되는데는 3초, 두 번째 요청은 5초(3 + 2), 세 번째 요청은 6초(3 + 2 + 1)가 걸릴 것입니다.

 

비동기 Programming

위에서 동기 처리되도록 작성된 코드를 파이썬의 async/await 키워드를 사용해서 한 번 비동기 처리될 수 있도록 개선해보도록 하겠습니다. 기존의 함수 선언에 async 키워드를 붙여서 일반 동기 함수가 아닌 비동기 함수(coroutine)로 변경하였으며, time.sleep 함수 대신에 asyncio.sleep 함수를 사용하여 1초의 지연을 발생시켰습니다.

time.sleep 함수는 기다리는 동안 CPU를 그냥 놀리는 반면에, asyncio.sleep 함수는 CPU가 놀지 않고 다른 처리를 할 수 있도록 해줍니다. 여기서 주의할 점은 asyncio.sleep 자체도 비동기 함수이기 때문에 호출할 때 반드시 await 키워드를 붙여야 한다는 것입니다.

import time
import asyncio

async def find_users_async(n):
    for i in range(1, n + 1):
        print(f'{n}명 중 {i}번 째 사용자 조회 중 ...')
        await asyncio.sleep(1)
    print(f'> 총 {n} 명 사용자 비동기 조회 완료!')

자 이제, 파이썬의 asyncio 라이브러리를 사용해서 위에서 작성한 함수를 비동기로 실행해보겠습니다. 먼저 이벤트 루프가 3개의 함수 호출을 알아서 스케줄하여 비동기로 호출할 수 있도록 asyncio.wait 함수의 배열 인자로 3개의 함수 리턴값, 즉 coroutine 객체를 넘겨주도록 수정합니다. 그리고 이렇게 수정된 process_async 비동기 함수를 호출할 때도, 함수의 리턴값인 coroutine 객체를, asyncio.run 함수에 넘겨줍니다.

async def process_async():
    start = time.time()
    await asyncio.wait([
        find_users_async(3),
        find_users_async(2),
        find_users_async(1),
    ])
    end = time.time()
    print(f'>>> 비동기 처리 총 소요 시간: {end - start}')

if __name__ == '__main__':
    asyncio.run(process_async())

비동기 처리되도록 재작성된 코드를 실행해보면 호출 순서와 무방하게 실행 시간이 짧은 수록 먼저 처리되는 것을 알 수 있습니다. 게다가 총 소요 시간도 6초에서 3초로 100% 단축되었음을 알 수 있습니다!

실제 사용자 관점에서 생각해보면 3초가 걸리는 요청을 기다리지 않고, 1초가 걸리는 요청은 1초 만에 응답이 오고, 2초가 걸리는 요청은 2초 만에 응답이 올테니 매우 이상적이지 않을 수 없습니다.

1명 중 1번 째 사용자 조회 중 ...
2명 중 1번 째 사용자 조회 중 ...
3명 중 1번 째 사용자 조회 중 ...
> 총 1 명 사용자 비동기 조회 완료!
2명 중 2번 째 사용자 조회 중 ...
3명 중 2번 째 사용자 조회 중 ...
> 총 2 명 사용자 비동기 조회 완료!
3명 중 3번 째 사용자 조회 중 ...
> 총 3 명 사용자 비동기 조회 완료!
>>> 비동기 처리 총 소요 시간: 3.0041661262512207

기본적으로 비동기 처리는 정확히 실행 순서가 보장되지 않기 때문에, 여러분 PC에서 실행했을 때는 저와 약간 실행 순서가 다를 수도 있습니다. 비록 동일한 실행 순서를 보장받지 못하더라도, 여기서 중요한 점은 CPU를 놀리지 않고 불필요한 지연없이 3개의 요청이 실행되어야 한다는 것입니다.

 

'Python 프로그래밍' 카테고리의 다른 글

PYQT 계산기 source code  (0) 2022.09.23
[Python] *args와 **kwargs  (0) 2022.07.22
[Python] GIL  (0) 2022.06.03
[Python] Coroutine  (0) 2022.06.02
[Python] Future  (0) 2022.06.01

Python Interpreter

 GIL을 이해하려면 먼저 Python 인터프리터란 것이 정확히 무엇인지 알아야 한다. Python 인터프리터란, Python으로 작성된 코드를 한 줄씩 읽으면서 실행하는 프로그램을 말한다. 그 프로그램의 구현체로는 여러 가지가 있을 수 있는데, 현재 Python 인터프리터의 표준 구현체로 받아들여지고 있는 것은 바로 CPython이다. CPython은 C 언어를 이용하여 구현한 Python 인터프리터이다. 이번 포스팅에서 다루는 내용은 전부 CPython을 기준으로 함을 미리 밝힌다.

 

GIL (Global Interpreter Lock)

In CPython, the global interpreter lock, or GIL, is 
a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once.
 This lock is necessary mainly because CPython's memory management is not thread-safe.

해석하자면, Python의 객체들에 대한 접근을 보호하는 일종의 뮤텍스(Mutex)로서, 여러 개의 쓰레드가 파이썬 코드를 동시에 실행하지 못하도록 하는 것이라고 한다. 즉, 한 프로세스 내에서, Python 인터프리터는 한 시점에 하나의 쓰레드에 의해서만 실행될 수 있다는 것이다. 멀티 쓰레딩이 불가능하다는 것이 아니다. 원래 멀티 코어라면 멀티 쓰레딩 시에 여러 개의 쓰레드가 여러 코어 상에서 병렬(Parallel) 실행될 수 있는데, Python에서는 그러한 병렬 실행이 불가능하다는 것뿐이다. 이를 그림으로 나타내면 다음과 같다.

GIL이 필요한 이유

 괜히 멀티 쓰레딩 시 병렬 실행만 불가능해지고, GIL이 왜 필요한가 싶을 수 있다. 사실, 앞서 소개한 Python 위키에서의 GIL 정의가 이 의문에 대해 답을 해준다. 그것은 바로, GIL이 Python의 객체들에 대한 접근을 보호하는 일종의 뮤텍스(Mutex)라는 것이다. 이것이 무슨 말인지 한 번 차근차근 자세히 알아보자.

먼저, Python에서 모든 것은 객체(Object)이다. 그리고 각 객체는 참조 횟수(Reference Count)를 저장하기 위한 필드를 갖고 있다. 참조 횟수란 그 객체를 가리키는 참조가 몇 개 존재하는지를 나타내는 것으로, Python에서의 GC(Garbage Collection)는 이러한 참조 횟수가 0이 되면 해당 객체를 메모리에서 삭제시키는 메커니즘으로 동작하고 있다. 참고로, sys 라이브러리의 getrefcount() 함수를 사용하면 특정 객체의 참조 횟수를 알아낼 수 있다.

import sys

# x의 참조 횟수 : 1
x = []

# x의 참조 횟수 : 2
y = x

# getrefcount() 함수의 매개변수 할당 시 x의 참조 횟수가 1 증가(3이 됨)
# getrefcount() 함수의 반환 시 x의 참조 횟수가 다시 1 감소(2가 됨)
sys.getrefcount(x)

# 출력 결과 : 3

 

그렇다면 이것이 GIL이랑 무슨 상관인 걸까? 참조 횟수에 기반하여 GC를 진행하는 Python의 특성상, 여러 개의 쓰레드가 Python 인터프리터를 동시에 실행하면 Race Condition이 발생할 수 있기 때문이다. Race Condition이란, 하나의 값에 여러 쓰레드가 동시에 접근함으로써 값이 올바르지 않게 읽히거나 쓰일 수 있는 상태를 말한다. 이러한 상황을 보고 Thread-safe 하지 않다고 표현하기도 한다.

즉, 여러 쓰레드가 Python 인터프리터를 동시에 실행할 수 있게 두면 각 객체의 참조 횟수가 올바르게 관리되지 못할 수도 있고, 이로 인해 GC가 제대로 동작하지 않을 수도 있다는 말이다. 물론 이러한 Race Condition은 뮤텍스(Mutex)를 이용하면 예방할 수 있다.

뮤텍스(Mutex)란, 멀티 쓰레딩 환경에서 여러 개의 쓰레드가 어떠한 공유 자원에 접근 가능할 때 그 공유 자원에 접근하기 위해 가지고 있어야 하는 일종의 열쇠와 같은 것이다. 만약 한 쓰레드가 어떠한 공유 자원에 대한 뮤텍스를 가지고 있다면, 다른 쓰레드들은 그 공유 자원에 접근하고 싶을 때도 그 공유 자원에 접근하고 있는 쓰레드가 뮤텍스를 풀어줄 때까지는 기다려야 한다.

그런데 앞서 말했듯이, Python에서 모든 것은 객체이고, 객체는 모두 참조 횟수를 가진다. 따라서 GC의 올바른 동작을 보장하려면 결국 모든 객체에 대해 뮤텍스를 걸어줘야 한다는 말이 된다. 이는 굉장히 비효율적이며, 만약 이를 프로그래머에게 맡길 경우 상당히 많은 실수를 유발할 수도 있는 문제이다.

그래서 결국 Python은 마음 편한 전략을 택하였다. 애초에 한 쓰레드가 Python 인터프리터를 실행하고 있을 때는 다른 쓰레드들이 Python 인터프리터를 실행하지 못하도록 막는 것이다. 이를 보고 "인터프리터를 잠갔다"라고 표현한다. 즉, Python 코드를 한 줄씩 읽어서 실행하는 행위가 동시에 일어날 수 없게 하는 것이다. 그러면 모든 객체의 참조 횟수에 대한 Race Condition을 고민할 필요도 없어진다. 뮤텍스를 일일이 걸어줄 필요도 없어지는 것이다. 이것의 GIL의 존재 이유이다.

 

Python에서 멀티쓰레딩은 나쁜것인가 ..?

위에서 설명한 것만 보면, Python에서는 GIL 때문에 멀티 쓰레딩을 쓰지 않는 게 좋아 보인다. 실제로, CPU 연산의 비중이 큰 작업을 할 때 멀티 쓰레딩은 오히려 성능을 떨어뜨린다. 병렬적인 실행은 불가능한데 괜히 문맥 전환(Context Switching) 비용만 잡아먹기 때문이다. 다음 예시 코드를 보자. 멀티 쓰레딩을 사용하니 오히려 더 느려진 걸 볼 수 있다.

import time
import threading

def loop():
    for i in range(50000000):
        pass

# Single Thread
start = time.time()
loop()
loop()
end = time.time()
print('[Single Thread] total time : {}'.format(end - start))

# Multi Thread
start = time.time()
thread1 = threading.Thread(target=loop)
thread2 = threading.Thread(target=loop)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
end = time.time()
print('[Multi Thread] total time : {}'.format(end - start))

# [Single Thread] total time : 2.3374178409576416
# [Multi Thread] total time : 3.4128201007843018

하지만 GIL은 CPU의 연산 과정에서 공유 자원에 대해 발생할 수 있는 Race Condition 문제 때문에 필요했다는 걸 상기할 필요가 있다. 따라서 Python에서는 외부 연산(I/O, Sleep 등)을 하느라 CPU가 아무것도 하지 않고 기다리기만 할 때는 다른 쓰레드로의 문맥 전환을 시도하게 되어 있다. 이때는 다른 쓰레드가 실행되어도 공유 자원의 Race Condition 문제가 발생하지 않기 때문이다.

이러한 이유로, CPU 연산의 비중이 적은, 즉 외부 연산(I/O, Sleep 등)의 비중이 큰 작업을 할 때는 멀티 쓰레딩이 굉장히 좋은 성능을 보인다. 따라서 Python에서 멀티 쓰레딩이 무조건 안 좋다는 말은 사실이 아니다. 실제로, I/O 혹은 Sleep 등의 외부 연산이 대부분이라면 멀티 쓰레딩을 통해 큰 성능 향상을 기대할 수 있다. 다음 예시 코드를 보자. 멀티 쓰레딩을 통해 더 빨라진 걸 볼 수 있다.

import time
import threading

def sleep_for_2s():
    time.sleep(2)

# Single Thread
start = time.time()
sleep_for_2s()
sleep_for_2s()
end = time.time()
print('[Single Thread] total time : {}'.format(end - start))

# Multi Thread
start = time.time()
thread1 = threading.Thread(target=sleep_for_2s)
thread2 = threading.Thread(target=sleep_for_2s)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
end = time.time()
print('[Multi Thread] total time : {}'.format(end - start))

# [Single Thread] total time : 4.017191171646118
# [Multi Thread] total time : 2.002999782562256

 

 

GIL을 우회하는 방법

일반적인 CPU 연산에 대하여, 병렬 처리를 하려면 크게 두 가지 방법을 생각해볼 수 있다.

먼저, 멀티 프로세싱을 이용하는 것이다. 한 프로세스의 여러 쓰레드들은 서로 자원을 공유하지만, 여러 프로세스들은 각자 독자적인 메모리 공간을 가져서 서로 자원을 공유하지 않기 때문이다. 물론 자원을 공유하려면 할 수는 있지만, 별도의 처리가 필요하다. 다만, 멀티 프로세싱은 메모리를 더 필요로 하고 문맥 전환의 비용이 꽤 된다는 단점이 있다.

다음으로, CPython이 아닌 다른 Python 인터프리터 구현체를 사용하는 것이다. 예를 들면 Jython 등이 있다. 그러나 흔히 사용하는 방법은 아니므로 권장하지 않는다.

'Python 프로그래밍' 카테고리의 다른 글

[Python] *args와 **kwargs  (0) 2022.07.22
[Python] Asyncio  (0) 2022.06.05
[Python] Coroutine  (0) 2022.06.02
[Python] Future  (0) 2022.06.01
[Python] Object Reference  (0) 2022.05.27

 보통 아래와 같이 메인루틴 안의 서브루틴들로 구성된 기능에 익숙할 것이다. 주로 구현한 함수들은 한 번 실행되고 종료되는 함수들이었다. Python의 서브루틴은 이러한 서브 루틴의 사용을 조정하는 메인 함수에 의해서 호출된다.

 

Coroutine

 Python의 코루틴(Coroutine)이란 Cooperative Routine을 의미하며, 서로 협력하는 루틴이라는 뜻이다. 메인루틴과 서브루틴처럼 종속된 관계가 아니라 대등한 관계로서 동작하며, 특정시점마다 상대방의 코드를 실행한다. 즉, 동시성 프로그램을 가능하도록 한 기술이라고 이해하면 될 것이다.

 ( Main Routine이 대기중일 때, Sub Routine을 통해 연산한 이후에 다시 Main Routine으로 돌아오도록 하는 기술 )

 

 위의 그림처럼 Coroutine은 함수가 종료되지 않은 상태로 메인 루틴의 코드를 실행한 뒤 다시 돌아와서 코루틴의 코드를 실행하게 되는 것이다. 일반 함수를 호출하면 코드를 한 번만 실행할 수 있지만, 코루틴은 코드를 여러 번 실행할 수 있는 것이다. 참고로 함수의 코드를 실행하는 지점을 진입점(Entry Point)라고 하는데, 코루틴은 진입점이 여러개인 함수인 것이다.

 

Coroutine에 값 보내기

 코루틴(Coroutine)은 제너레이터(Generator)의 특별한 형태로, 제너레이터에서 yield를 이용해 값을 발생시켰다면, 코루틴에선 (yield) 형식을 통해 값을 받아올 수 있다.

  • 코루틴객체.send(값)
  • 변수 = (yield)
def number_coroutine():
	while Ture:		# 코루틴을 계속 유지하기 위해서 무한루프 사용
    	x = (yield)	# 코루틴 외부에서 값을 받아옴, yield를 괄호로 묶어야 한다.
        print(x)
        
co = number_coroutine()
next(co)			# 코루틴 안의 yield까지 코드 실행(최초실행)

co.send(1)			# 코루틴에 숫자 1을 보냄
co.send(2)			# 코루틴에 숫자 2을 보냄
co.send(3)			# 코루틴에 숫자 3을 보냄

 코루틴은 while을 이용한 무한 루프를 통해 메인 루틴을 유지하고, 코루틴 외부에서 코루틴에 값을 보내는 형태로 이루어진다. 또한 코루틴을 무한루프상태에서 종료하기 위해서는 객체.close()를 선언해 주면 된다. 

def number_coroutine():
	while True:
    	x = (yield)
        print(x, end=' ')
        
co = number_coroutine()
next(co)

for i in range(20):
	co.send(i)
    
co.close()

# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

 

 

GeneratorExit 예외처리

 코루틴 객체에서 close() Method를 호출하면 종료될 때 GeneratorExit 예외가 발생하게 된다. 따라서 이 예외를 처리하면 코루틴의 종료시점을 파악할 수 있다.

def number_coroutine():
	try:
    	while Ture:
        	x = (yield)
            print(x, end=' ')
		except GeneratorExit		#코루틴이 종료될 때 GeneratorExit 예외 발생
    		print()
            print('코루틴 종료')
            
co = number_coroutine()
next(co)

for i in rnage(20):
	co.send(i)
    
co.close()

# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# 코루틴 종료

 

Coroutine과 스레드의 차이점

 스레드는 비동기로, 여러 스레드가 있다면 한꺼번에 동시에 실행되는 반면, 코루틴은 프로그램이 실행 중일 때 특정 시점에 코루틴으로 이동하고 그 전에 진행 중이던 루틴은 정지합니다. 즉, 한번에 하나의 코드만 실행됩니다. 이는 기존의 프로그래밍과 유사한 성격으로 보일 수 있습니다. 하지만 기존의 프로그래밍은 에러가 나지 않는 이상 실행 중인 코드를 빠져나올 수 있는 부분은 return과 같이 가장 마지막 부분이지만 코루틴은 실행 중간에 해당 함수를 빠져 나와 다른 코드를 실행할 수 있고 다시 실행 중이였던 코드 라인으로 이동할 수도 있습니다. 다시 말해서 코루틴은 yield 키워드로 실행을 중지할 수 있고 yield 키워드를 호출했던 곳으로 와서 실행을 재 시작합니다.

 싱글코어에선 코루틴은 이동 시점이 더 잘 조절되고 context switching이 적어 성능면에서 멀티 스레드보다 좋을 수 있지만 멀티코어 프로세서를 활용할 수 없다는 문제점이 있으며 스레드보다 성능이 떨어지게 됩니다.

'Python 프로그래밍' 카테고리의 다른 글

[Python] Asyncio  (0) 2022.06.05
[Python] GIL  (0) 2022.06.03
[Python] Future  (0) 2022.06.01
[Python] Object Reference  (0) 2022.05.27
[Python] mutable과 immutable  (0) 2022.05.26

 Python의 동시성을 처리하는 기능 중 하나인, Future 클래스에 대해 알아보고자 한다. Future는 concurrent.futures와 asyncio 내부에 있는 핵심 컴포넌트입니다.

사용자가 직접 다룰 일은 거의 없지만, 지연된 계산을 표현하기 위해 사용됩니다. 이 때, 그 객체의 계산은 완료되었을 수도 있고, 완료되지 않았을 수도 있습니다.

Future는 대기 중인 작업을 큐에 넣고, 완료 상태를 조사하고, 결과 혹은 예외를 가져올 수 있도록 캡슐화합니다.

주의할 점은, Future의 실행을 스케줄링하는 프레임워크만이 어떤 일이 일어나는지 확실히 알 수 있기 때문에, 사용자가 Future 객체를 직접 생성하거나 변경해서는 안된다는 것입니다. 이 주의사항을 무시한다면 큰 고통에 빠질 수 있습니다.

 

Future

 concurrent.futures.Future 비동기로 호출된 함수 콜이 객체로 캡슐화된 형태이다. Executor 클래스 인스턴스의 .submit() 호출에 의해 인스턴스가 만들어진다. 특히 이 객체는 asyncio의 Future 클래스와 유사한 API를 가지고 있다. (둘이 호환되는 객체는 아니다.)

 따라서 단일 스레드 비동기 코루틴을 사용하는 방식과 concurrent.futures를 이용한 병렬처리 방식은 매우 비슷한 형태로 사용 가능하다. 이 Future 클래스는 자바스크립트의 Promise API와 매우 비슷하다. 아직 완료되지 않은 (혹은 완료되었는지 당장은 모르는) 작업을 외부에서 객체로 다룰 수 있게 된다.

 다음의 메소드들이 지원된다. 특히 하나의 작업에 대해서 하나 이상의 완료 콜백을 추가할 수 있다는 점이 흥미롭다.

  • cancel() : 작업 취소를 시도한다. 만약 현재 실행중이고 취소가 불가능할 경우 False를 리턴한다. 작업이 취소되었다면 True가 리턴된다.
  • canceled() : 취소가 완료된 작업이면 True를 리턴한다.
  • running(): 실행 중인 경우 True를 리턴한다.
  • done(): 작업이 완료되어고 정상적으로 종료되었다면 True를 리턴한다.
  • result(): 해당 호출의 결과를 리턴한다. 만약 작업이 아직 완료되지 않았다면 최대 타임아웃시간까지 기다린다음, None을 리턴한다.
  • exception(): 해당 호출이 던진 예외를 반환한다. 역시 작업이 완료되지 않았다면 타임아웃 시간까지 기다린다.
  • add_done_callback(): 콜백함수를 Future 객체에 추가한다. 이 함수는 future 객체하나를 인자로 받는 함수이다. 콜백은 취소되거나 종료된 경우에 모두 호출된다.
# flags_threadpool.py
from concurrent import futures
# Reuse functions in flags.py
from flags import save_flag, get_flag, main 

# 하나의 국기 이미지를 다운받는 함수. 각 worker가 이 함수를 수행함
def download_one(cc: str):
	image = get_flag(cc)    
	save_flag(image, f'{cc}.gif')    
	print(cc, end=' ', flush=True)    
	return cc 

def download_many(cc_list: list[str]) -> int:
	# context manager로서 ThreadPoolExecutor를 인스턴스화 합니다.    
	# executor.__exit__() 메소드는 executor.shutdown(wait=True)를 호출하는데,    
	# 이는 스레드가 완료될 때까지 블락시킵니다.   
	with futures.ThreadPoolExecutor() as executor:        
		# map 메소드는 내장 함수 map과 유사합니다.        
		# 첫 번째 인수인 download_one 함수는 여러 스레드에서 동시에 호출됩니다.        
		# map 메소드는 각 함수 호출에서 리턴되는 값들을 반복할 수 있는 제너레이터를 반환합니다.      
		# 여기서는 country code를 반환        
		res = executor.map(download_one, sorted(cc_list))        

	return len(list(res)) 


if __name__ == '__main__':    
	# 이 스크립트에서 구현한 downlaod_many 콜러블을 인수로 전달하여,    
	# flags.py에서 구현한 main 함수 호출   
	main(download_many)

 

'Python 프로그래밍' 카테고리의 다른 글

[Python] GIL  (0) 2022.06.03
[Python] Coroutine  (0) 2022.06.02
[Python] Object Reference  (0) 2022.05.27
[Python] mutable과 immutable  (0) 2022.05.26
[Programming] 암호화 알고리즘 (Encryption Algorithms)  (0) 2022.05.22

 여러 프로그래밍 언어들에서 사용되는 함수 인수 전달방식에는 크게 3가지로 구분된다. 

  1. Call By Value
  2. Call By Reference
  3. Call By Object Reference

 

Call by Value

 함수에 인수를 전달하는 방식으로 변수의 값을 복사해 함수의 인자로 전달한다. 따라서, 함수 내에서 전달된 인자를 조작해도 함수 외부의 변수에는 영향을 미치지 않는다. C언어에서 이러한 함수인수전달 방식을 사용한다. 

 

Call by Reference

 함수에 인수를 전달하는 방식이다. JAVA언어나 C언어의 포인터로 함수의 인자에 전달하는 방식이 Call By Reference 같은 방식이다. 변수가 가리키는 주소값을 함수의 인자로 전달하게 된다. 그렇기에 함수 내에서 전달된 인자를 조작하면 함수 외부에 있는 원본 변수의 주소값에 있는 값을 바꿀 수 있다. 따라서, 함수 내에서 변경된 사항이 함수 외부에도 영향을 미치게 되는 것이다. 

 

Call by Object Reference

 파이썬은 Call by Value 같기도.. Call by Reference같기도 하다.. 결론부터 정의하자면 어떠한 데이터를 매개변수로 넘기는지에 따라 달라지는 것이라고 볼 수 있다. 

 파이썬은 모든 것이 객체이기 때문에 immutable(불변)한 자료형을 넘긴다면 Call by Value가 되는 것이고, mutable(가변)한 자료를 넘긴다면 Call by Reference가 되는 것이다. 그렇기 때문에 파이썬은 Passed by Assignment라고도 한다 ! 

 python에서 변수를 선언할 때, a="galaxy"라는 선언문이 있다고 생각해보자. 선언문이 실행되면 "galaxy"라는 문자열 객체가 생기고, 그 객체에 대하여 a라는 이름표를 붙이게 된다. 즉, python에서 변수는 위에서처럼 특정 메모리 공간을 할당받은 컨테이너 개념이 아니라, 어떤 객체에 붙여진 이름표일 뿐이다.

 물건에는 위치가 존재할 수 있다. 예를 들어 갤럭시워치을 남영역 31번 사물함에 숨겨뒀다면 "갤럭시워치"은 객체, "남영역 31번 사물함"은 갤럭시워치의 위치, 즉 주소 값이 된다. 그러나 갤럭시워치에 붙여진 이름표, 즉 변수에 대해서도 위치(주소값)를 지정해 두지는 않는다. "갤럭시워치"에는 여러 가지 이름표(변수)가 붙어있을 수도 있다. 예를 들어 해당 인형에 "호준이의 갤럭시워치", "복학생의 갤럭시워치"등의 여러 이름표가 붙어있다고 하자. 이때 변수"호준이의 갤럭시워치"의 주소 값(위치)과 변수"복학생의 갤럭시워치"의 주소 값(위치)은 객체 "갤럭시워치"의 주소 값(위치)으로 같다.

 다시 Call by object-reference이야기로 돌아가자면, python에서는 global인지 local인지 영역에 따라 변수들의 정보를 저장하는 namespace가 따로 있다. 즉, 전역 변수를 함수에서 인자로 받아오더라도 함수 내에서는 지역변수(이름표)에 불과하다. 함수 내에서 이름표를 떼서 다른 객체에 붙인다고 하더라도, 그 이름표는 함수 내에서만 사용하는 이름표일 뿐이다. 결국 함수 호출이 끝나면 전역 변수(이름표)가 여전히 그 객체에 붙어있다.

 예를 들어 list 1 = [1,2,3,4] 일 때, 함수 내에서 list 1을 [5,6,7,8]이라는 새로운 객체랑 binding 한다고 해도 함수 호출이 끝나면 list 1은 그대로 [1,2,3,4]이다.

 주의할 점은 이름표(변수)만 떼고 붙이는 것이 아니라, 이름표가 붙여진 물건(객체)의 구성품을 직접 조작하는 경우이다.  예를 들어 위의 예시와 같이 list 1 = [1,2,3,4] 일 때, 함수 내에서 list 1이라는 이름표가 붙여진 객체 [1,2,3,4]에 대하여  list 1 [0] = 5 이와 같이 객체 내의 요소(element)를 조작할 수 있다.

이 경우에 함수의 호출이 끝나서 지역 이름표가 전역 이름표로 바뀐다고 하더라도 객체가 변한 상태이므로 list 1은 [5,2,3,4]가 된다.

"복학생의 갤럭시워치" 이름표가 붙은 갤럭시워치의 끈을 초록색으로 염색했다면

"호준이의 갤럭시워치" 이름표가 붙은 갤럭시워치의 끈도 초록색인 게 당연하지 않겠는가?

(하나의 갤럭시워치에 두 개의 다른 이름표가 붙어있으므로)

 

 이때, 객체 자체를 바꾸려면 당연히 객체가 mutable, 즉 가변적인 포맷이어야 한다. 따라서 Call by object-reference 방식은 immutable 한 포맷의 객체(tuple 등)는 변경할 수 없지만, mutable한 포맷의 객체(list, dictionary, 직접 만든 클래스 등)는 변경할 수 있다는 특성을 갖는다.

'Python 프로그래밍' 카테고리의 다른 글

[Python] Coroutine  (0) 2022.06.02
[Python] Future  (0) 2022.06.01
[Python] mutable과 immutable  (0) 2022.05.26
[Programming] 암호화 알고리즘 (Encryption Algorithms)  (0) 2022.05.22
[Python(Web)] Robots.txt  (1) 2022.05.21

+ Recent posts