프로세스, 스레드 및 코 루틴의 차이점과 장단점

기술

프로세스 : 운영 체제에서 리소스를 할당하는 가장 작은 단위입니다. 리소스에는 CPU, 메모리, 디스크 등과 같은 IO 장치가 포함됩니다.

스레드 : CPU 스케줄링의 기본 단위입니다.

프로세스 : 시스템이 리소스를 할당하는 캐리어, 프로그램 작업의 인스턴스입니다.

스레드 : 프로그램 실행의 최소 단위. 프로그램을 실행하는 데 사용되는 프로세스의 엔티티입니다. 프로세스에는 여러 스레드가 있습니다.

어떤 사람들은 파이썬 멀티 스레딩이 맛이 없다고 말하는 이유는 무엇입니까?

 

우리의 상식에서 다중 프로세스와 다중 스레드는 프로그램의 효율성을 높이기 위해 하드웨어 리소스를 동시에 최대한 활용합니다. 어떻게 파이썬에서 맛이 없어 질 수 있습니까?

Python의 악명 높은 GIL 때문입니다.

그래서 GIL은 무엇입니까? GIL이있는 이유는 무엇입니까? 멀티 스레딩은 정말 맛이 없습니까? GIL을 제거 할 수 있습니까?

다중 스레딩은 맛이 없습니다.

실험하기 :

"1 억"숫자를 줄이면 프로그램이 종료됩니다. 단일 스레드를 사용하여이 작업을 실행하면 완료 시간은 어떻게됩니까?

단일 스레드, 4 코어 CPU 컴퓨터에서 단일 스레드에 소요되는 시간은 6.5 초입니다.

멀티 스레딩은 두 개의 하위 스레드 t1과 t2를 생성하고 각 스레드는 5 천만 개의 빼기 연산을 수행하며 두 스레드가 협력 적으로 실행되는 데 6.8 초가 걸리지 만 속도가 느려집니다.

 

논리적으로 말하면 두 스레드가 동시에 병렬로 실행되는 경우 시간이 줄어들지 않고 증가해야합니다. 그 이유는 GIL에 있습니다.

Python 멀티 스레딩에서 각 스레드의 실행 모드 :

1. GIL 얻기

2. 절전 모드 또는 Python 가상 머신이 일시 중지 할 때까지 코드를 실행합니다.

3. GIL 해제

쓰레드가 실행을 원하면 먼저 GIL을 얻어야한다는 것을 알 수 있는데, GIL을 "passport"로 간주 할 수 있으며 파이썬 프로세스에는 GIL이 하나뿐입니다. 패스를 얻을 수없는 스레드는 실행을 위해 CPU에 들어갈 수 없습니다 .

 

Cpython 인터프리터 (Python 언어의 주류 인터프리터)에는 Global Interpreter Lock이 있습니다. 인터프리터가 Python 코드를 해석하고 실행할 때 먼저이 잠금을 얻어야합니다. 즉, 언제든지 하나만있을 수 있음을 의미합니다. 코드를 실행하는 것입니다 . 다른 스레드 코드 명령어를 실행하기 위해 CPU를 구입하고자하는 경우에는 먼저 잠금을 확보해야합니다. 잠금이 다른 스레드에 의해 점유되는 경우, 스레드는 잠금 해제 잠금을 수용하는 스레드 때까지 기다릴 수 있습니다. 코드 지침의 가능성.

동시에, 거기에 하나의 스레드를 실행하고, 다른 스레드만을 기다릴 수있다.이 멀티 코어 CPU 경우에도 , 다수의 스레드를 병렬로 코드를 실행할 수있는 방법이 없다. 그것만 교대로 수행 될 수 있기 때문에, 멀티 스레딩은 온라인 텍스트 전환, 잠금 메커니즘 처리 (잠금 획득, 잠금 해제 등)를 포함하므로 멀티 스레드 실행은 빠르지 않고 느립니다.

GIL은 언제 출시됩니까?

스레드가 I / O 작업을 만나면 GIL을 해제합니다. CPU 바운드 스레드가 인터프리터의 틱을 100 번 실행하면 (단계는 대략 Python 가상 머신의 명령으로 간주 될 수 있음) GIL도 해제됩니다.

스텝 길이를 설정하여 스텝 길이를 볼 수 있습니다. 단일 스레드에 비해 멀티 스레딩으로 인한 추가 오버 헤드가 더 많습니다.

       CPython 인터프리터가 이러한 방식으로 설계된 이유는 무엇입니까? 멀티 스레딩에 문제가 있습니다. 여러 스레드가 공유 데이터에 액세스 할 때 두 스레드가 동시에 하나의 데이터를 수정할 수 있기 때문에 공유 데이터의 동기화 및 일관성 문제를 해결하는 방법입니다. 데이터 일관성을 보장하는 적절한 메커니즘이없는 경우, 그런 다음 프로그램은 결국 예외를 일으켰으므로 Python의 아버지는 데이터에 동기화 문제가 있는지 여부에 관계없이 전역 스레드 잠금에 참여했습니다. 어쨌든 전역 잠금은 데이터 보안을 보장하기 위해 모두 적합합니다. 이것이 멀티 스레딩이 세밀한 제어 데이터 보안을 가지고 있지 않지만이를 해결하기 위해 간단하고 무례한 방법을 사용하기 때문에 맛이없는 이유입니다.

      이 솔루션은 90 년대에 도입되었지만 문제가되지 않았습니다. 결국 당시의 하드웨어 구성은 여전히 ​​매우 초보적이었습니다. 단일 코어 CPU가 여전히 주류였으며 다중 스레드 응용 프로그램 시나리오가 많지 않았습니다. 그 당시에는 단일 스레드였습니다. 실행, 단일 스레드는 스레드 컨텍스트 전환을 포함하지 않으며 다중 스레드보다 효율적입니다 ( 다중 코어 환경에서는이 규칙이 적용되지 않음 ).

      따라서 데이터 일관성과 보안을 보장하기 위해 GIL을 사용하는 것이 반드시 바람직하지 않은 것은 아닙니다. 적어도 당시에는 매우 저렴한 구현이었습니다. 그렇다면 GIL을 제거 할 수 있습니까? 정말 많은 사람들이 있지만 그 결과는 실망 스럽습니다 .1999 년 Greg Stein과 Mark Hammond는 GIL을 제거하고 모든 가변 데이터 구조에 대해 GIL을보다 세분화 된 것으로 대체하는 Python 분기를 만들었습니다. 그러나 벤치마킹 후 GIL이없는 Python의 실행 효율성은 단일 스레드 조건에서 거의 2 배 더 느립니다.

Python의 아버지는 다음과 같이 말했습니다. 위의 고려 사항에 따라 GIL을 제거하는 것은 너무 많은 노력을 기울일 필요없이 큰 가치가 없습니다. 요약 CPython 인터프리터는 스레드 데이터 동기화를 보장하기 위해 GIL을 제공하므로 GIL에서도 여전히 스레드 동기화가 필요합니까? 다중 스레딩은 IO 집약적 인 작업에서 어떻게 수행됩니까? 모두에게 메시지를 남겨 주시기를 환영합니다.

 

파이썬의 멀티 스레딩이 유용합니까?

 

 

1. CPU 집약적 인 코드 (다양한 루프 처리, 계산 등).이 경우 틱 수는 곧 임계 값에 도달 한 다음 GIL의 릴리스 및 반복을 트리거합니다 (여러 스레드가 앞뒤로 전환, 물론 리소스를 소비해야합니다), 따라서 파이썬에서의 멀티 스레딩은 CPU 집약적 인 코드에 적합하지 않습니다 .
2. IO 집약적 인 코드 ( 파일 처리, 웹 크롤러 등 ), 멀티 스레딩은 효율성을 효과적으로 향상시킬 수 있습니다 (단일 스레드 IO 작업은 IO 대기를 수행하여 불필요한 시간 낭비를 유발하고 멀티 스레딩을 활성화하면 스레드 A가 대기 할 수 있음). 대기 중, CPU 리소스를 낭비하지 않고 자동으로 스레드 B로 전환하여 프로그램 실행의 효율성을 향상시킵니다). 따라서 파이썬의 멀티 스레딩은 IO 집약적 인 코드에 더 친숙합니다 .

python3.x에서 GIL은 계산을 위해 틱을 사용하지 않고 대신 타이머를 사용합니다 (실행 시간이 임계 값에 도달하면 현재 스레드가 GIL을 해제 함). CPU 집약적 인 프로그램에 더 친숙하지만 여전히 사용합니다. GIL로 인한 동시 문제를 해결하지 못하고 스레드를 실행할 수있는 문제이므로 효율성이 여전히 만족스럽지 않습니다.

멀티 코어 멀티 스레딩은 싱글 코어 멀티 스레딩보다 나쁘다. 그 이유는 싱글 코어 멀티 스레딩에서는 GIL이 해제 될 때마다 깨어 난 스레드가 GIL 잠금을 획득 할 수 있으므로 실행할 수 있기 때문이다. 원활하게하지만 멀티 코어에서는 CPU0이 GIL을 릴리스 한 후 다른 CPU의 스레드가 경쟁하지만 GIL은 CPU0에 의해 즉시 획득되어 다른 CPU의 깨어 난 스레드가 전환 시간까지 대기하고 보류 상태가됩니다. 다시 스레드 스 래싱 (스 래싱)이 발생하여 효율성이 떨어집니다.

원래의 질문으로 돌아가서 : 베테랑들이 "파이썬에서 멀티 코어 CPU를 최대한 활용하려면 멀티 프로세스를 사용하십시오"라는 말을 자주 듣습니다. 그 이유는 무엇입니까?
그 이유는 각 프로세스에는 서로 간섭하지 않는 자체 독립적 인 GIL이있어 실제 병렬로 실행될 수 있으므로 파이썬에서 다중 프로세스의 실행 효율성이 다중 스레드보다 낫습니다. 멀티 코어 CPU) .

결론은 다음과 같습니다. 다중 코어에서 효율성을 향상시키기 위해 병렬을 수행하려는 경우보다 일반적인 방법은 실행 효율성을 효과적으로 향상시킬 수있는 다중 프로세스를 사용하는 것입니다.

 

그렇긴하지만 CPU 집약적 인 프로그램에 파이썬을 사용하는 것은 그 자체로 적절하지 않습니다. C, Go 및 Java의 속도와 비교할 때 성능이 너무 나쁩니다. 물론, 진정한 멀티 스레딩을 달성하기 위해 C 확장을 작성하고 파이썬으로 호출 할 수 있습니다. 이것은 빠릅니다. 파이썬을 사용하는 이유는 개발 효율성이 매우 높고 빠르게 구현할 수 있기 때문입니다.

마지막으로 몇 가지 사항을 추가합니다.

  1. 파이썬에서 CPU를 잘 활용하려면 여러 프로세스를 사용해야합니다. 또는 코 루틴을 사용할 수 있습니다. 다중 처리와 gevent가 당신을 부르고 있습니다.
  2. GIL은 버그가 아니며 Guido는 그런 것에 국한되지 않습니다. Turtle 삼촌은 GIL을 사용하지 않고 스레드 안전을 수행하기 위해 다른 방법을 사용하려한다고 말한 적이 있습니다. 그 결과 파이썬 언어의 전반적인 효율성이 두 배가되었습니다. 장단점을 고려하면 GIL이 최선의 선택입니다. 제거하지만 일부러 유지합니다.
  3. 파이썬 계산을 더 빠르게 만들고 싶지만 C를 작성하고 싶지 않습니까? pypy를 사용하십시오. 이것은 진짜 살인자입니다.

 

다중 스레딩을 사용하는 것이 적합하지 않은 경우 다중 프로세스 또는 코 루틴을 사용하여 동시성을 향상 시키시겠습니까?

1. 다중 프로세스는 다중 코어 CPU를 더 잘 사용할 수 있습니다.

    그러나 다중 프로세스에는 자체 제한도 있습니다. 스레드에 비해 더 번거롭고 전환하는 데 더 오래 걸리며 Python의 다중 프로세스에서는 프로세스 수가 CPU 코어 수를 초과하지 않는 것이 좋습니다. 하나의 GIL 만 있으므로 프로세스는 하나의 CPU 만 실행할 수 있습니다. 프로세스가 하나의 CPU를 차지하면 시스템의 성능을 완전히 활용할 수 있지만 프로세스가 더 많을 때 프로세스 전환이 자주 발생하고 이득이 더 큽니다. 손실.

따라서 멀티 코어의 경우 CPU의 멀티 코어 기능을 최대한 활용하려면 CPU 코어 수와 동일한 수의 스레드를 사용하는 멀티 스레딩을 고려하십시오.

 

2. 언제 코 루틴이 필요합니까?

그러나 특수한 상황 (특히 IO 집약적 작업)에서는 다중 스레딩이 다중 프로세스보다 낫습니다.

예를 들어 200W URL을 제공하면 각 URL에 해당하는 페이지를 캡처하여 저장해야합니다.이 경우 여러 프로세스를 단독으로 사용하는 것은 분명히 좋지 않은 영향을 미칩니다. 왜?

예를 들어 각 요청의 대기 시간은 2 초이고 다음은 CPU 계산 시간 무시입니다.

1. 단일 프로세스 + 단일 스레드 : 2 초 * 200W = 400W 초 == 1111.11 시간 == 46.3 일이 걸립니다.이 속도는 분명히 허용되지 않습니다.

2. 단일 프로세스 + 멀티 스레딩 : 예를 들어이 프로세스에서 10 개의 멀티 스레드를 열었습니다. 이는 1에서보다 10 배 더 빠를 수 있습니다. 이는 약 4.63 일 내에 200W 크롤링을 완료 할 수 있음을 의미합니다. 여기서 실제 실행은 다음과 같습니다. : 스레드 1이 블로킹을 만나고, CPU가 스레드 2로 전환하여 실행하고, 블로킹을 만난 다음 스레드 3으로 전환합니다. 10 개의 스레드가 차단 된 후 프로세스가 차단되고 스레드가 차단 될 때까지 프로세스가 계속 실행될 수 있습니다. 따라서 속도는 약 10 배 증가 할 수 있습니다 (여기서는 스레드 전환으로 인한 오버 헤드는 무시되고 실제 증가는 10 배에 도달해서는 안 됨). 스레드 전환에도 오버 헤드가 있다는 점을 고려해야하므로 멀티를 시작할 수 없습니다. -무기한 스레딩 (200W 스레드를 여는 것은 확실히 신뢰할 수 없음)

3. 멀티 프로세스 + 멀티 스레드 : 여기에서 대단합니다. 일반적으로 많은 사람들이이 방법을 사용합니다. 멀티 프로세스에서는 각 프로세스가 CPU를 점유 할 수 있고, 멀티 스레드는 어느 정도 대기중인 블로킹을 우회합니다. 단일 프로세스에서 멀티 스레딩보다 낫습니다. 예를 들어, 10 개의 프로세스를 열고 각 프로세스에서 20W 스레드를 열면 실행 속도는 이론적으로 단일 프로세스에서 200W 스레드보다 10 배 이상 빠릅니다 (이유가 더 많은 이유). 주된 이유는 CPU 스위칭 200W 스레드의 소비가 20W 스레드 프로세스를 스위칭하는 것보다 훨씬 더 많기 때문 입니다.이 부분의 오버 헤드를 고려하면 10 배 이상입니다).

더 좋은 방법이 있습니까? 대답은 '예'입니다.

4. 코 루틴을 사용하기 전에 무엇을 / 왜 / 어떻게 (무엇을 / 사용 하는가 / 사용 하는가)에 대해 이야기합시다.
 

뭐:

코 루틴은 사용자 수준의 경량 스레드입니다. 코 루틴에는 자체 레지스터 컨텍스트와 스택이 있습니다. 코 루틴이 전환되도록 예약되면 레지스터 컨텍스트 및 스택을 다른 위치에 저장하고 다시 전환 할 때 이전에 저장된 레지스터 컨텍스트 및 스택을 복원합니다. 따라서:

코 루틴은 마지막 호출의 상태 (즉, 모든 로컬 상태의 특정 조합)를 유지할 수 있습니다. 프로세스가 다시 입력 될 때마다 마지막 호출의 상태를 입력하는 것과 같습니다. 즉, 논리 입력 마지막으로 떠난 시간 스트림 위치

동시 프로그래밍에서 코 루틴은 스레드와 유사합니다. 각 코 루틴은 실행 단위를 나타내며 자체 로컬 데이터를 가지며 글로벌 데이터 및 기타 리소스를 다른 코 루틴과 공유합니다.

왜:

현재 주류 언어는 기본적으로 동시성 기능으로 멀티 스레딩을 선택합니다. 스레드와 관련된 개념은 선점 형 멀티 태스킹이고 코 루틴과 관련된 개념은 협동 멀티 태스킹입니다.

프로세스인지 스레드인지에 관계없이 각 블록 또는 스위치는 시스템 호출에 트랩되어야합니다. 먼저 CPU가 운영 체제의 스케줄러를 실행하도록 한 다음 스케줄러가 실행할 프로세스 (스레드)를 결정합니다.
또한 선점 스케줄링의 실행 순서를 결정할 수 없기 때문에 쓰레드를 사용할 때 매우 신중하게 동기화 문제를 처리 할 필요가 있으며,이 문제는 코 루틴에 전혀 존재하지 않습니다 (이벤트 구동 및 비동기 프로그램은 동일합니다). 장점).

사용자가 코 루틴에 대한 스케줄링 로직을 작성하기 때문에, CPU의 경우 코 루틴은 실제로 단일 스레드이므로 CPU는 컨텍스트를 스케줄링하고 전환하는 방법에 대해 생각할 필요가 없으므로 CPU 스위칭 오버 헤드를 절약하므로 코 루틴은 다음과 같습니다. 특정에서는 멀티 스레딩보다 낫습니다.

어떻게:

파이썬에서 코 루틴을 사용하는 방법? 대답은 gevent를 사용하는 것입니다. 사용 방법은 여기를 참조하십시오.

코 루틴을 사용하면 쓰레드 오버 헤드의 제약을받을 수 없습니다. 한 번에 20W URL을 단일 프로세스 코 루틴에 넣어 보았는데 전혀 문제가 없었습니다.

따라서 가장 권장되는 방법은 다중 프로세스 + 코 루틴입니다 (각 프로세스에서 단일 스레드로 간주 될 수 있으며이 단일 스레드는 코 루틴입니다).

멀티 프로세스 + 코 루틴에서는 CPU 전환의 오버 헤드를 피하고 여러 CPU를 충분히 활용할 수있어 대용량 데이터와 파일 읽기 / 쓰기가 가능한 크롤러의 효율성을 크게 향상시킬 수 있습니다.


작은 예 :

 
#-*- coding=utf-8 -*-
 
import requests
 
from multiprocessing import Process
 
import gevent
 
from gevent import monkey; monkey.patch_all()
 
 
 
import sys
 
reload(sys)
 
sys.setdefaultencoding('utf8')
 
def fetch(url):
 
    try:
 
        s = requests.Session()
 
        r = s.get(url,timeout=1)#在这里抓取页面
 
    except Exception,e:
 
        print e 
 
    return ''
 
 
 
def process_start(url_list):
 
    tasks = []
 
    for url in url_list:
 
        tasks.append(gevent.spawn(fetch,url))
 
    gevent.joinall(tasks)#使用协程来执行
 
 
 
def task_start(filepath,flag = 100000):#每10W条url启动一个进程
 
    with open(filepath,'r') as reader:#从给定的文件中读取url
 
        url = reader.readline().strip()
 
        url_list = []#这个list用于存放协程任务
 
        i = 0 #计数器,记录添加了多少个url到协程队列
 
        while url!='':
 
            i += 1
 
            url_list.append(url)#每次读取出url,将url添加到队列
 
            if i == flag:#一定数量的url就启动一个进程并执行
 
                p = Process(target=process_start,args=(url_list,))
 
                p.start()
 
                url_list = [] #重置url队列
 
                i = 0 #重置计数器
 
            url = reader.readline().strip()
 
        if url_list not []:#若退出循环后任务队列里还有url剩余
 
            p = Process(target=process_start,args=(url_list,))#把剩余的url全都放到最后这个进程来执行
 
            p.start()
 
  
 
if __name__ == '__main__':
 
    task_start('./testData.txt')#读取指定文件
 
 

주의 깊은 학생들은 위의 예에서 숨겨진 문제가 있음을 알게 될 것입니다 : URL 수가 증가함에 따라 프로세스 수가 증가 할 것입니다. 프로세스 풀 다중 처리를 사용하지 않는 이유 여기에서 프로세스 수를 제어하기 위해 풀을 사용합니다. 충돌이 발생하면 multiprocessing.Pool과 gevent를 동시에 사용할 수 없지만 관심이있는 학생들은 gevent.pool 코 루틴 풀을 공부할 수 있습니다.
 

추천

출처blog.csdn.net/weixin_42575020/article/details/107689581