Есть ли python GIL в Flask?

#python #multithreading #flask

#python #многопоточность #flask

Вопрос:

Недавно я изучал python multiple thread и обнаружил, что GIL заставит python запускать один поток за один раз даже на многоядерных процессорах.

Итак, я сделал небольшой PoC.

Мой код следующий :

 import threading
import time

COUNT = 50000000
def count():
    i = 0
    print('thread id =', threading.get_ident())
    while(i < COUNT):
        i = i  1

start_time = time.time()
count()
count()
end_time = time.time()
print(f'execution time without multiple threading : {end_time - start_time}')

start_time = time.time()
t_1 = threading.Thread(target=count)
t_2 = threading.Thread(target=count)
t_1.start()
t_2.start()
t_1.join()
t_2.join()
end_time = time.time()
print(f'execution time with multiple threading : {end_time - start_time}')
  

И это мой результат:

 thread id = 2204
thread id = 2204
execution time without multiple threading : 5.695769786834717
thread id = 3492
thread id = 5260
execution time with multiple threading : 5.339878082275391
  

Это очень наглядно показывает, как работает GIL.

Но теперь я выполняю тот же процесс в Flask, и кажется, что GIL работает не так, как ожидалось. это мой код :

server.py

 from flask import Flask
import threading
import os

app = Flask(__name__)
COUNT = 50000000

@app.route('/')
def hello():
    print ('Thread id = ', threading.get_ident())
    print ('Process id = ', os.getpid())
    i = 0
    while(i < COUNT):
        i = i  1
    return "Hello World!"

if __name__ == '__main__':
    app.run()
  

client.py

 import requests
import time
import multiprocessing as mp

def send():
    start_time = time.time()
    r = requests.get('http://localhost:5000')
    end_time = time.time()
    print(f'response after {end_time - start_time}')

if __name__ == '__main__':
    p_1 = mp.Process(target=send)
    p_2 = mp.Process(target=send)
    p_1.start()
    p_2.start()
    p_1.join()
    p_2.join()
  

результат server.py:

 Thread id =  18428
Process id =  19000
Thread id =  17436
Process id =  19000
127.0.0.1 - - [14/Sep/2020 09:36:19] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [14/Sep/2020 09:36:19] "GET / HTTP/1.1" 200 -
  

результат client.py

 response after 7.361770153045654
response after 7.373677015304565
  

Кажется, что 2 клиента получают ответы одновременно, но с GIL мой ожидаемый результат должен быть
7.3 первый ответ и 14.xxx во втором ответе. Может ли кто-нибудь помочь мне проверить эту проблему?

Комментарии:

1. Скорее всего, flask (или что бы вы ни запускали под flask) разветвляет новые процессы для запросов на обслуживание. GIL повреждает потоки, но независимые процессы не затрагиваются.

2. GIL не блокирует потоки навсегда. Все еще возможно, что два потока, кажется, выполняются одновременно, но на самом деле быстро переключают управление.

3. @KlausD.: Не в этом случае; задержка здесь полностью связана с циклом, связанным с процессором (не блокирующая задача, в которой можно выполнять работу, пока другая заблокирована). Работа, выполняемая каждым потоком, не может быть выполнена одновременно, и они должны оплачивать стоимость контекстных переключений в дополнение к этому при каждом переключении элемента управления. Если бы работа была time.sleep(7) или что-то в этом роде, она могла бы работать так, как вы описываете, но если одному потоку потребовалось бы 7 секунд на чисто процессорную работу, добавление другого потока, выполняющего ту же работу снова, примерно удвоит время.

4. Уф .. сбой мобильной прокрутки.

5. @ShadowRanger Я думаю, что я просто нахожу ответ, я пытаюсь отправить запрос только один раз, и это занимает половину времени, так что это означает, что GIL действительно работает и на Flask!!!!!!!

Ответ №1:

В Python Flask тоже есть GIL В вопросе, который я задал, я не отправлял запрос отдельно на сервер. На самом деле требуется половина времени, чтобы получить ответ на отдельный запрос. И почему мы можем получить два ответа одновременно? Это потому, что в нем есть sys.getswitchinterval(), поэтому потоки будут переключаться каждые 0,005 секунды, пока они не завершат работу.

Ответ №2:

Как указано @ShadowRanger в комментариях, центральный принцип, лежащий в основе этого ответа (а именно, что два процесса отсчитывают одну и ту же переменную и требуется локальное хранилище потоков), неверен. Я, однако, оставлю ответ, поскольку он содержит некоторую информацию, которая была полезна для OP при поиске решения.

Используя GIL, интерпретатор Python действительно позволяет запускать только один поток одновременно — кроме операций ввода-вывода. У него есть «интервал переключения», который заключается в том, как часто он останавливает выполнение одного потока и смотрит на запуск другого потока на некоторое время. Вы можете проверить это с помощью:

 sys.getswitchinterval()
  

и в моей системе оно установлено на 5 мс.

Вы видите два потока, оба из которых выполняются примерно по 5 мс за раз, но оба увеличивают одну и ту же переменную, потому что у вас есть общее адресное пространство с потоками. Таким образом, нет 2 потоков, каждый из которых считает до 50000000, есть два потока, каждый из которых отсчитывает часть одного 50000000.

Что вам нужно, так это «локальное хранилище потоков», чтобы каждый увеличивал свой собственный счетчик:

 #!/usr/bin/env python3

from flask import Flask
import threading
import os, sys

app = Flask(__name__)
COUNT = 50000000

@app.route('/')
def hello():
    print ('Thread id = ', threading.get_ident())
    print ('Process id = ', os.getpid())
    mydata = threading.local()
    mydata.i = 0
    while(mydata.i < COUNT):
        mydata.i = mydata.i  1
    return "Hello World!"

if __name__ == '__main__':

    print(f'switch interval: {sys.getswitchinterval()}')
    app.run()
  

Если я запускаю ваш исходный код, я получаю:

 response after 5.990185022354126
response after 6.009223937988281
  

Если я запускаю свой код с i в локальном хранилище потоков, я получаю:

 response after 19.429346084594727 
response after 19.553659200668335
  

Комментарии:

1. Это ничего не объясняет; i является локальной переменной для функции, а не глобальной, поэтому она не является общей для потоков. Только COUNT является глобальным / общим, и он никогда не изменяется.

2. @ShadowRanger Спасибо за ваши мысли. Я оставлю это как есть на минуту (вместо того, чтобы удалять его), поскольку фактический ответ может быть связан, и мой ответ может подтолкнуть чьи-то идеи о возможных причинах.

3. Привет @MarkSetchell Спасибо за вашу помощь! интервал времени действительно помогает мне прояснить мою мысль