Статья для понимания процессов и потоков в Python

Python

Для операционной системы задача — это процесс. Например, при открытии браузера запускается процесс браузера, при открытии блокнота запускается процесс блокнота, а при открытии двух блокнотов запускаются две заметки. Словесный процесс.

использоватьtopКоманда для просмотра запущенных процессов:

Processes: 371 total, 2 running, 15 stuck, 354 sleeping, 3142 threads  17:27:27
Load Avg: 2.57, 2.29, 2.07  CPU usage: 5.26% user, 4.79% sys, 89.94% idle
SharedLibs: 151M resident, 18M data, 17M linkedit.
MemRegions: 107884 total, 6773M resident, 131M private, 2803M shared.
PhysMem: 15G used (2386M wired), 988M unused.
VM: 1564G vsize, 527M framework vsize, 20146031(0) swapins, 21455983(0) swapouts
Networks: packets: 39381163/32G in, 38877596/18G out.
Disks: 4762039/190G read, 7146822/247G written.

PID    COMMAND      %CPU TIME     #TH   #WQ  #PORT MEM    PURG   CMPRS  PGRP
96223- Microsoft Ex 0.1  15:30.25 356   2    2216  46M    0B     298M   96223
96200  Microsoft AU 0.0  00:11.45 5     0    169   5956K  0B     2480K  96200
91030  nwjs Helper  0.0  02:06.91 9     0    139   2920K  0B     15M    91027
91029  nwjs Helper  0.0  02:37:00 21    0    178   650M   10M    70M    91027
91028  nwjs Helper  0.0  19:01.66 5     0    76    34M    0B     9464K  91027

Некоторые процессы также выполняют несколько действий одновременно, например Word, который может выполнять набор текста, проверку орфографии, печать и т. д. одновременно. В процессе, если вы хотите делать несколько вещей одновременно, вам нужно одновременно запускать несколько «подзадач». Мы называем эти «подзадачи» в процессе потоками.

Поскольку каждый процесс должен делать по крайней мере одно действие, у процесса есть по крайней мере один поток. Конечно, какWordЭтот сложный процесс может иметь несколько потоков, и несколько потоков могут выполняться одновременно.Метод выполнения нескольких потоков такой же, как и у нескольких процессов.Операционная система быстро переключается между несколькими потоками, так что каждый поток на короткое время работает попеременно, как будто они были казнены одновременно. Конечно, настоящая одновременная многопоточность требует нескольких ядер.CPUбыть возможным.

Все программы Python, которые мы написали ранее, представляют собой процессы, выполняющие одну задачу, то есть существует только один поток. Что, если мы хотим выполнять несколько задач одновременно?

Есть два решения:

  • Первый заключается в запуске нескольких процессов.Хотя каждый процесс имеет только один поток, несколько процессов могут выполнять несколько задач одновременно.

  • Другой метод заключается в запуске процесса и запуске нескольких потоков внутри процесса, чтобы несколько потоков могли одновременно выполнять несколько задач.

  • Конечно, есть и третий способ, который заключается в запуске нескольких процессов, и каждый процесс запускает несколько потоков, чтобы одновременно выполнялось больше задач.Конечно, эта модель сложнее и редко используется на практике.

Подводя итог, можно выделить три способа реализации многозадачности:

  • многопроцессорный режим;
  • многопоточный режим;
  • Многопроцессный + многопоточный режим.

Поток — это наименьшая единица выполнения, а процесс состоит как минимум из одного потока. Как планировать процессы и потоки, полностью определяется операционной системой, и сама программа не может решать, когда выполнять и как долго выполнять.

Многопроцессорные и многопоточные программы включают синхронизацию и совместное использование данных, и их сложнее писать.

Давай, победи процессы и потоки и преодолей большое препятствие в изучении Python.

многопроцессорность

Вот закуска:

Операционные системы Unix/Linux обеспечиваютfork()Системный вызов, он очень особенный. Обычный вызов функции, вызов один раз, возврат один раз, ноfork()Вызовите один раз и дважды верните, потому что операционная система автоматически копирует текущий процесс (называемый родительским процессом) (называемый дочерним процессом), а затем возвращается в родительский процесс и дочерний процесс соответственно.

дочерний процесс всегда возвращается0, а родительский процесс возвращает дочернийID. Это объясняется тем, что родительский процесс можетforkСуществует много дочерних процессов, поэтому родительский процесс должен записывать значение каждого дочернего процесса.ID, а дочернему процессу достаточно вызватьgetppid()вы можете получить родительский процессID.

Модуль os Python инкапсулирует общие системные вызовы, включая fork, которые могут легко создавать подпроцессы в программах Python:

import os

print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
    print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
    print('I (%s) just created a child process (%s).' % (os.getpid(), pid))

Результаты приведены ниже:

Process (69673) start ...
I (69673) just created a child Process(69674)
I am child proces (69674) and my parent is 69673.

имеютforkВызов, процесс может скопировать дочерний процесс для обработки новой задачи, когда он получает новую задачу, общийApacheСервер является родительским процессом, прослушивающим порт, и всякий раз, когда появляется новый HTTP-запрос, он будет разветвлять дочерний процесс для обработки нового HTTP-запроса.

но этоforkНе существует такой вещи, как операционная система Windows. Значит есть лечениеforkОбщий модуль для обеспечения вызовов между разными операционными системами.

multiprocessingМодуль — это кроссплатформенная версия многопроцессорного модуля.

multiprocessingмодуль обеспечиваетProcessclass для представления объекта процесса, следующий пример демонстрирует запуск дочернего процесса и ожидание его завершения:

#!/usr/bin/env python
# coding=utf-8

from multiprocessing import Process
import os

"""
    子进程要执行的代码
"""
def run_proc(name):
    print('Run child process %s (%s)' % (name, os.getpid()))

if __name__ == '__main__':
    print('Parent process %s.' % os.getpid())
    p = Process(target=run_proc, args=('test_code',))
    print('Child process will start.')
    p.start()
    p.join()
    print('Child process end.')

Результат выполнения следующий:

$ python forkbymutilprocessing.py
Parent process 70227.
Child process will start.
Run child process test_code (70228)
Child process end.

При создании дочернего процесса вам нужно только передать функцию выполнения и параметры функции, создать экземпляр процесса и использоватьstart()метод запускается, так что процесс создается, чемfork()Еще проще.

join()Метод может дождаться завершения дочернего процесса, прежде чем продолжить работу, что обычно используется для синхронизации между процессами.

Пул процессов Пул

Если вы хотите запустить большое количество подпроцессов, вы можете создавать подпроцессы пакетами в пуле процессов:

from multiprocessing import Pool
import os, time, random

def long_time_task(name):
    print("Run task %s (%s)..." % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print("Task %s run %0.f seconds." % (name, (end - start)))

if __name__ == "__main__":
    print("Parent process %s." % os.getpid())
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print("Waiting for all subprocess done...")
    p.close()
    p.join()
    print("All subprocess done.")

Результаты:

➜ python3 testpool.py
Parent process 65899.
Waiting for all subprocess done...
Run task 0 (65900)...
Run task 1 (65901)...
Run task 2 (65902)...
Run task 3 (65903)...
Task 3 run 0 seconds.
Run task 4 (65903)...
Task 2 run 1 seconds.
Task 0 run 2 seconds.
Task 1 run 2 seconds.
Task 4 run 2 seconds.
All subprocess done.

Интерпретация кода:

правильноPoolвызов объектаjoin()Метод будет ждать завершения выполнения всех дочерних процессов, вызовjoin()должен быть вызван передclose(),перечислитьclose()После этого вы не сможете продолжать добавлять новыеProcess.

Обратите внимание на результаты вывода, задание0,1,2,3выполняется немедленно, и задача4Ожидание завершения предыдущей задачи перед выполнением, потому чтоPoolПо умолчанию установлен размер4(p = Pool(4)), что означает, что одновременно может выполняться до 4 процессов. ЭтоPoolПреднамеренное ограничение дизайна, а не ограничение операционной системы. Если изменить на:

p = Pool(5)

Вы можете запустить 5 процессов.

потому чтоPoolРазмер по умолчанию — это количество ядер ЦП.Если у вас 8-ядерный ЦП, отправьте не менее 9 дочерних процессов, чтобы увидеть описанный выше эффект ожидания.

дочерний процесс

Часто дочерний процесс является не самим собой, а внешним процессом. После того, как мы создали дочерний процесс, нам также необходимо контролировать ввод и вывод дочернего процесса. При попытке выполнить некоторые операции и обслуживание через python,subprocessПросто столб.

subprocessМодули позволяют нам легко запустить подпроцесс, а затем контролировать его ввод и вывод.

В следующем примере показано, как запустить команду в коде Python.nslookup <某个域名>, что равносильно запуску непосредственно из командной строки:

#!/usr/bin/env python
# coding=utf-8

import subprocess

print("$ nslookup www.yangcongchufang.com")
r = subprocess.call(['nslookup', 'www.yangcongchufang.com'])
print("Exit code: ", r)

Результаты:

➜ python subcall.py
$ nslookup www.yangcongchufang.com
Server:		219.141.136.10
Address:	219.141.136.10#53

Non-authoritative answer:
Name:	www.yangcongchufang.com
Address: 103.245.222.133

('Exit code: ', 0)

Если подпроцессу также требуется ввод, это можно сделать с помощьюcommunicate()Ввод метода:

#!/usr/bin/env python
# coding=utf-8

import subprocess

print("$ nslookup")
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b"set q=mx\nyangcongchufang.com\nexit\n")
print(output.decode("utf-8"))
print("Exit code:", p.returncode)

Приведенный выше код эквивалентен выполнению команды в командной строкеnslookup, затем вручную введите:

set q=mx
yangcongchufang.com
exit

межпроцессного взаимодействия

ProcessОпределенно существует потребность в общении между ними, и операционная система предоставляет множество механизмов для обеспечения взаимодействия между процессами. ПитонmultiprocessingМодули оборачивают базовые механизмы, обеспечиваяQueue,Pipesи другие способы обмена данными.

мы начинаем сQueueНапример, создайте два дочерних процесса в родительском процессе, один дляQueueзаписать данные, один изQueueЧтение данных в:

# _*_ coding:utf-8 _*_

from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码
def write(q):
    print("Process to write: %s" % os.getpid())
    for value in ['A', 'B', 'C']:
        print("Put %s to queue..." % value)
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码
def read(q):
    print("Process to read: %s" % os.getpid())
    while True:
        value = q.get(True)
        print("Get %s from queue." % value)

if __name__ == '__main__':
    # 父进程创建Queue,并传给各个子进程
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程pw,写入:
    pw.start()
    # 启动子进程pr,读取:
    pr.start()
    # 等待pw结束
    pw.join()
    # pr进程里的死循环,无法等待结束,只能强制终止
    pr.terminate()

Фактический эффект от реализации:

$ python process_comm.py
Process to write: 94327
Put A to queue...
Process to read: 94328
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.

существуетUnix/LinuxВниз,multiprocessingмодуль инкапсулированныйfork()позвони, чтобы нам не нужно было обращать вниманиеfork()подробности. потому чтоWindowsнетforkвызов, следовательно,multiprocessingНужно «симулировать»forkЭффект родительского процесса всеPythonобъект должен пройтиpickleСериализуется, а затем передается дочернему процессу, все, еслиmultiprocessingсуществуетWindowsВызов вниз не удался, мы должны сначала рассмотреть, так ли этоpickleНе удалось.

Краткое описание процесса

существуетUnix/Linuxниже вы можете использоватьfork()Вызов для реализации нескольких процессов.

Для достижения кросс-платформенной многопроцессорности вы можете использоватьmultiprocessingмодуль.

Межпроцессное взаимодействие осуществляется черезQueue,Pipesи т. д., которые нужно реализовать.

Многопоточность

Многозадачность может выполняться несколькими процессами или несколькими потоками внутри процесса.

Ранее мы упоминали, что процесс состоит из нескольких потоков, а процесс имеет по крайней мере один поток.

Поскольку потоки — это исполнительные блоки, напрямую поддерживаемые операционной системой, языки высокого уровня обычно имеют встроенную поддержку многопоточности, и Python не является исключением, а потоки Python — настоящие.Posix Threadвместо имитации потока.

Стандартная библиотека Python предоставляет два модуля:_threadа такжеthreading,_threadмодуль низкого уровня,threadingэто расширенный модуль, да_threadупакованный. В большинстве случаев нам нужно использовать толькоthreadingэтот расширенный модуль.

Чтобы запустить поток, нужно передать функцию и создатьThreadэкземпляр, а затем вызовитеstart()Начать выполнение:

# _*_ coding:utf-8 _*_
import time, threading

def loop():
    print('thread %s is running...' % threading.current_thread().name)
    n = 0
    while n < 5:
        n = n + 1
        print('thread %s >>> %s' % (threading.current_thread().name, n))
        time.sleep(1)
    print('thread %s ended.' % threading.current_thread().name)

print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)

# 执行效果:
$ python3 thread_test.py
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.

Поскольку любой процесс по умолчанию запускает поток, мы называем этот поток основным потоком, а основной поток может запускать новый поток.threadingмодуль имеетcurrent_thread()функция, которая всегда возвращает текущий экземпляр потока. Имя экземпляра основного потокаMainThread, имя дочернего потока указывается при его создании, мы используемLoopThreadНазовите дочерний поток. Имя используется только для отображения при печати и не имеет никакого другого значения.Если у вас нет имени, Python автоматически назовет поток какThread-1,Thread-2...

Lock

Самая большая разница между многопоточностью и многопроцессорностью заключается в том, что при многопоточности копия одной и той же переменной существует в каждом процессе, не влияя друг на друга, тогда как при многопоточности все переменные совместно используются всеми потоками, поэтому любая переменная может быть изменена любым потоком. Следовательно, самая большая опасность совместного использования данных между потоками заключается в том, что несколько потоков одновременно изменяют переменную и искажают содержимое.

Давайте посмотрим, как несколько потоков одновременно манипулируют переменной для изменения содержимого:

# _*_ coding:utf-8 _*_
import time, threading

# 假定这是你银行的存款
balance = 0

def change_it(n):
    # 先存后取,结果应该是0
    global balance
    balance = balance + n
    balance = balance - n
    print balance

def run_thread(n):
    for i in range(100000):
        change_it(n)

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

Мы определяем общую переменнуюbalance, начальное значение равно0, и запустить два потока, сначала сохранить, а затем получить, теоретически результат должен быть0, однако, поскольку планирование потоков определяется операционной системой, когдаt1,t2При выполнении попеременно, пока количество петель достаточно,balanceрезультат не обязательно0.

Фактический эффект от реализации:

$ python thread_share_var.py
31
$ python thread_share_var.py
42
$ python thread_share_var.py
-6
$ python thread_share_var.py
0
$ python thread_share_var.py
7
$ python thread_share_var.py
13

Причина в том, что для изменения баланса требуется несколько операторов, и когда эти операторы выполняются, поток может быть прерван, в результате чего несколько потоков изменяют содержимое одного и того же объекта.

Когда два потока вносят и снимают средства одновременно, баланс может быть неправильным. Вы определенно не хотите, чтобы ваш банковский баланс стал отрицательным без причины. Поэтому мы должны убедиться, что один поток изменяетbalanceвремя, другие потоки не должны быть изменены.

Если мы хотим убедитьсяbalanceРасчет правильный, надо датьchange_it()Последняя блокировка, когда поток начинает выполнятьсяchange_it()Когда мы говорим, что поток получил блокировку, другие потоки не могут выполняться одновременно.change_it(), вы можете только ждать, пока блокировка будет освобождена, и вы можете изменить ее после получения блокировки. Поскольку существует только одна блокировка, независимо от того, сколько потоков, максимум один поток удерживает блокировку одновременно, поэтому конфликта модификаций не будет. Создание блокировки выполняетсяthreading.Lock()реализовать:

balance = 0
lock = threading.Lock()

def run_thread(n):
    for i in range(100000):
        # 先要获取锁:
        lock.acquire()
        try:
            # 放心地改吧:
            change_it(n)
        finally:
            # 改完了一定要释放锁:
            lock.release()

Фактический эффект, достигнутый после блокировки:

$ python thread_share_var.py
0
$ python thread_share_var.py
0
$ python thread_share_var.py
0

Когда несколько потоков выполняются одновременноlock.acquire(), только один поток может успешно получить блокировку, а затем продолжить выполнение кода, другие потоки продолжают ждать, пока блокировка не будет получена.

После того, как поток, получивший блокировку, израсходован, блокировку необходимо снять, иначе потоки, ожидающие блокировки, будут ждать вечно и станут мертвыми потоками. Поэтому мы используемtry...finallyчтобы убедиться, что замок будет разблокирован.

Преимущество блокировок заключается в том, что определенный ключевой код может быть полностью выполнен только одним потоком от начала до конца.Конечно, есть много недостатков.Первый заключается в предотвращении одновременного выполнения нескольких потоков.Некоторый код, содержащий блокировки может выполняться только в однопоточном режиме.Выполнение, эффективность сильно снижается. Во-вторых, поскольку может быть несколько блокировок, когда разные потоки удерживают разные блокировки и пытаются получить блокировки, удерживаемые другой стороной, это может привести к взаимоблокировке, вызывая зависание нескольких потоков, ни выполнения, ни завершения. завершается операционной системой.

многоядерный процессор

Если вам не повезло иметь многоядерный процессор, вы определенно думаете, что несколько ядер должны иметь возможность выполнять несколько потоков одновременно.

Если написать бесконечный цикл, что произойдет?

ОткрытымMac OS XизActivity Monitor,илиWindowsизTask Manager, вы можете отслеживать использование ЦП процессом.

Мы можем следить за тем, чтобы поток бесконечного цикла100%Занимает один ЦП.

Если есть два потока бесконечного цикла, в многоядерном ЦП можно отслеживать, чтобы он занимал200%ЦП, то есть занимает два ядра ЦП.

Чтобы запустить все ядра N-ядерного процессора, необходимо запустить N потоков бесконечного цикла.

Попробуйте написать бесконечный цикл на Python:

# _*_ coding:utf-8 _*_
import threading, multiprocessing

def loop():
    x = 0
    while True:
        x = x ^ 1

for i in range(multiprocessing.cpu_count()):
    t = threading.Thread(target=loop)
    t.start()

Когда вы работаете, вы знаете, что это такое, и вы не можете остановить ошибки, которые пишете сами. Жизнь учит нас только смирению и трудолюбию, а жуки учат сойти с ума.

Вышеприведенная программа запускает N потоков с одинаковым количеством ядер ЦП.На 4-ядерном ЦП можно проследить, что коэффициент занятости ЦП составляет всего 102%, то есть используется только одно ядро.

Но используя C, C++ или Java для перезаписи одного и того же бесконечного цикла, вы можете напрямую запускать все ядра, 4 ядра работают на 400%, 8 ядер работают на 800%, почему бы не Python?

Поскольку поток Python — это настоящий поток, но когда интерпретатор выполняет код, возникаетGILЗамок:Global Interpreter Lock, перед выполнением любого потока Python должен получитьGILзамок, то,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。Эта глобальная блокировка GIL фактически блокирует исполняемый код всех потоков, поэтому многопоточность в Python может выполняться только попеременно, даже если 100 потоков выполняются на 100-ядерном процессоре, может использоваться только одно ядро.

GILЭто историческое наследие дизайна интерпретатора Python.Обычно интерпретатор, который мы используем, является официальной реализацией.CPython, чтобы действительно использовать преимущества нескольких ядер, если только вы не переписываете интерпретатор без GIL.

Таким образом, в Python вы можете использовать многопоточность, но не рассчитывайте на эффективное использование нескольких ядер. Если вам необходимо использовать несколько ядер через многопоточность, этого можно добиться только с помощью расширений C, но это потеряет простоту и удобство использования Python.

Однако не беспокойтесь слишком сильно: хотя Python не может использовать многопоточность для выполнения многоядерных задач, он может выполнять многоядерные задачи посредством многопроцессорности. Несколько процессов Python имеют свои собственные блокировки GIL, которые не влияют друг на друга.

в заключении.

Многопоточное программирование имеет сложную модель и подвержено конфликтам, оно должно быть изолировано блокировками, в то же время остерегаться взаимоблокировок.

Из-за глобальной блокировки GIL в конструкции интерпретатора Python многопоточность не может использовать преимущества многоядерности. Многопоточный параллелизм — прекрасная мечта Python.

ThreadLocal

В многопоточной среде каждый поток имеет свои собственные данные. Потоку лучше использовать свои собственные локальные переменные, чем глобальные, потому что локальные переменные могут быть видны только самому потоку и не будут влиять на другие потоки, а изменение глобальных переменных должно быть заблокировано.

Но есть и проблема с локальными переменными, то есть при вызове функции передать очень хлопотно:

def process_student(name):
    std = Student(name)
    # std是局部变量,但是每个函数都要用到它,因此必须传进去
    do_task_1(std)
    do_task_2(std)

def do_task_1(std):
    do_subtask_1(std)
    do_subtask_2(std)

def do_task_2(std):
    do_subtask_2(std)
    do_subtask_2(std)

Каждая функция вызывается слой за слоем, так как насчет передачи параметров? Использовать глобальные переменные? Также нет, потому что каждый поток обрабатывает по-разномуStudentОбъект, не может быть передан.

Если вы используете глобальныйdictхранить всеStudentобъект, тоthreadкак самого себяkeyполучить соответствующий потокStudentКак объект?

global_dict = {}

def std_thread(name):
    std = Student(name)
    # 把std放到全局变量global_dict中:
    global_dict[threading.current_thread()] = std
    do_task_1()
    do_task_2()

def do_task_1():
    # 不传入std,而是根据当前线程查找:
    std = global_dict[threading.current_thread()]
    ...

def do_task_2(arg):
    # 任何函数都可以查找出当前线程的std变量
    std = global_dict[threading.current_thread()]
    ...

Этот метод теоретически осуществим, и его самым большим преимуществом является то, что он устраняетstdПроблема передачи объектов в каждый слой функций, однако каждая функция получаетstdКод немного некрасивый.

Есть ли более простой способ?

ThreadLocalПоявился, не нужно искатьdict,ThreadLocalЧтобы помочь вам сделать это автоматически:

# _*_ coding:utf-8 _*_
import threading

# 创建全局ThreadLocal对象:
local_school = threading.local()

def process_student():
    # 获取当前线程关联的student
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))

def process_thread(name):
    # 绑定ThreadLocal的student
    local_school.student = name
    process_student()

t1 = threading.Thread(target=process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

# 执行结果
$ python thread_local.py
Hello, Alice (in Thread-A)
 Hello, Bob (in Thread-B)
$ python thread_local.py
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)

глобальная переменнаяlocal_schoolтолько одинThreadLocalобъект, каждыйThreadможет читать и писать в негоstudentсвойства, но не влияют друг на друга. ты можешь поставитьlocal_schoolрассматриваются как глобальные переменные, но каждое свойство, например.local_school.studentВсе они являются локальными переменными потока, которые можно читать и записывать произвольно, не мешая друг другу, и нет необходимости решать проблему блокировки.ThreadLocalбудет обрабатываться внутри.

Можно понимать как глобальную переменнуюlocal_schoolЯвляетсяdict, можно не только использоватьlocal_school.student, вы также можете привязать другие переменные, такие какlocal_school.teacherи т.п.

ThreadLocalНаиболее распространенным местом является привязка соединения с базой данных для каждого потока,HTTPЗапросы, идентификационная информация пользователя и т. д., все обработчики, вызываемые потоком, могут легко получить доступ к этим ресурсам.

ОдинThreadLocalХотя переменные являются глобальными переменными, каждый поток может читать и записывать только независимую копию своего собственного потока, не мешая друг другу.ThreadLocalРешена проблема передачи параметров друг другу между функциями в потоке.

Процесс против потока

Мы представили многопроцессорность и многопоточность — два наиболее распространенных способа реализации многозадачности. Теперь давайте обсудим плюсы и минусы обоих подходов.

Прежде всего, для достижения многозадачности мы обычно разрабатываем режим Мастер-Рабочий.Мастер отвечает за назначение задач, а Рабочий отвечает за выполнение задач.Поэтому в многозадачной среде обычно есть один Мастер и несколько Рабочих.

Если Мастер-Рабочий реализован с несколькими процессами, главный процесс является Мастером, а остальные процессы — Рабочими.

Если мастер-рабочий реализован с несколькими потоками, основной поток является мастером, а остальные потоки — рабочими.

Самым большим преимуществом многопроцессорного режима является высокая стабильность, поскольку сбой дочернего процесса не повлияет на основной процесс и другие дочерние процессы. (Конечно, основной процесс зависает на всех процессах, но процесс Master отвечает только за распределение задач, и вероятность зависания мала.) Знаменитый Apache первым принял многопроцессорный режим.

Недостатком многопроцессного режима является высокая стоимость создания процесса.В системах Unix/Linux можно использовать fork для вызова, но стоимость создания процесса в Windows огромна. Кроме того, количество процессов, которые операционная система может запускать одновременно, также ограничено.При ограничении памяти и ЦП, если одновременно выполняются тысячи процессов, операционная система даже будет иметь проблемы с планированием.

Многопоточный режим обычно немного быстрее, чем многопроцессорный, но ненамного быстрее, а фатальный недостаток многопоточного режима заключается в том, что зависание любого потока может напрямую привести к сбою всего процесса, потому что все потоки совместно используют память обработать. В Windows, если возникает проблема с кодом, выполняемым потоком, часто можно увидеть такое сообщение: «Программа выполнила недопустимую операцию и собирается закрыться». но операционная система принудительно завершит весь процесс.

В Windows эффективность многопоточности выше, чем у многопроцессорности, поэтому сервер IIS от Microsoft по умолчанию использует многопоточность. Из-за проблем со стабильностью многопоточности стабильность IIS не так хороша, как у Apache. Чтобы облегчить эту проблему, IIS и Apache теперь имеют смешанный режим многопроцессорности + многопоточности, что действительно усложняет проблему.

переключение потоков

Будь то многопроцессорный или многопоточный, пока число большое, эффективность точно не повысится, почему?

Возьмем аналогию. Предположим, вы, к сожалению, готовитесь к вступительным экзаменам в среднюю школу. Каждый вечер вам нужно делать домашнее задание по 5 предметам: китайский язык, математика, английский язык, физика и химия. Каждое домашнее задание занимает 1 час.

Если вы сначала потратите 1 час на выполнение домашнего задания по языку, а затем потратите 1 час на домашнее задание по математике, а затем сделаете все это по очереди, в общей сложности это займет 5 часов.Этот метод называется однозадачной моделью, или модель пакетной задачи.

Предположим, вы планируете перейти на многозадачную модель, вы можете делать китайский в течение 1 минуты, затем переключиться на домашнюю работу по математике, сделать 1 минуту, затем переключиться на английский и так далее, если скорость переключения достаточно высока, этот метод будет выполняться на одноядерном процессоре.Многозадачность та же.С точки зрения детсадовца,вы делаете 5 домашних заданий одновременно.

Однако смена домашней работы обходится дорого: например, при переключении с китайского на математику нужно сначала убрать китайские книги и ручки на столе (это называется сохранением сцены), затем открыть учебник по математике и найти линейка компаса (это называется подготовкой к новой среде) ), чтобы начать делать домашнюю работу по математике. То же самое происходит, когда операционная система переключает процессы или потоки: ей необходимо сохранить текущую исполняемую локальную среду (состояние регистра ЦП, страницу памяти и т. д.), а затем подготовить среду выполнения новой задачи (восстановить последнюю состояние регистра, переключение страниц памяти и т. д.), чтобы начать выполнение. Хотя этот процесс переключения является быстрым, он также требует времени. Если одновременно выполняются тысячи задач, операционная система может быть в основном занята переключением задач, и времени на выполнение задач не так много.

Поэтому, как только многозадачность достигает предела, она потребляет все ресурсы системы, что приводит к резкому падению эффективности, и все задачи не могут быть выполнены хорошо.

Интенсивные вычисления против интенсивного ввода-вывода

Вторым фактором многозадачности является тип задачи. Мы можем разделить задачи на интенсивные вычисления и интенсивные операции ввода-вывода.

Задачи с интенсивными вычислениями характеризуются большим количеством вычислений, которые потребляют ресурсы ЦП, например, вычисление числа пи, декодирование видео в высоком разрешении и т. д., все из которых зависят от вычислительной мощности ЦП. Хотя такого рода ресурсоемкие задачи также можно выполнять в режиме многозадачности, чем больше задач, тем больше времени тратится на переключение между задачами и тем ниже эффективность ЦП при выполнении задач.Количество одновременных задач должно быть равно количеству ядер процессора.

Задачи с интенсивными вычислениями в основном потребляют ресурсы ЦП, поэтому эффективность кода очень важна. Языки сценариев, такие как Python, неэффективны и совершенно не подходят для задач, требующих больших вычислительных ресурсов. Для ресурсоемких задач лучше всего писать на C.

Второй тип задач — интенсивный ввод-вывод.Все задачи, связанные с сетевым и дисковым вводом-выводом, являются задачами, интенсивными вводом-выводом.Этот тип задач характеризуется низким потреблением ресурсов ЦП и большую часть времени задачи ожидают завершения операции ввода-вывода ( т.к. скорость ввода-вывода намного ниже скорости процессора и памяти). Для задач с интенсивным вводом-выводом чем больше задач, тем выше эффективность ЦП, но есть предел. Наиболее распространенными задачами являются задачи с интенсивным вводом-выводом, такие как веб-приложения.

При выполнении задач с интенсивным вводом-выводом 99% времени тратится на ввод-вывод и очень мало времени затрачивается на процессор, поэтому совершенно невозможно заменить чрезвычайно медленный язык сценариев, такой как Python, чрезвычайно быстрым C языка Повысить эффективность работы. Для задач с интенсивным вводом-выводом наиболее подходящим языком является язык с наибольшей эффективностью разработки (наименьшее количество кода), язык сценариев — первый выбор, а язык C — наихудший.

Асинхронный ввод-вывод

Учитывая огромную разницу в скорости между процессором и вводом-выводом, задача тратит большую часть времени на ожидание операций ввода-вывода во время выполнения.Однопроцессная однопоточная модель предотвратит параллельное выполнение других задач.Поэтому нам нужна многопроцессная или многопоточная модель для поддержки одновременного выполнения нескольких задач.

Современные операционные системы внесли огромные улучшения в операции ввода-вывода, и самой большой функцией является поддержка асинхронного ввода-вывода. Если вы в полной мере используете поддержку асинхронного ввода-вывода, предоставляемую операционной системой, вы можете использовать модель с одним процессом и одним потоком для выполнения многозадачности. Эта новая модель называется моделью, управляемой событиями. Nginx — это веб-сервер, поддерживающий асинхронный IO. Он работает на одноядерном процессоре. Модель с одним процессом может эффективно поддерживать многозадачность. На многоядерном ЦП вы можете запускать несколько процессов (столько же, сколько ядер ЦП), используя все преимущества многоядерного ЦП. Поскольку общее количество процессов в системе очень ограничено, планирование операционной системы очень эффективно. Многозадачность с моделью программирования асинхронного ввода-вывода является основной тенденцией.

В соответствии с языком Python модель асинхронного программирования с одним процессом называется сопрограммами.Благодаря поддержке сопрограмм можно писать эффективные многозадачные программы, основанные на событиях. Позже мы обсудим, как писать сопрограммы.

распределенный процесс

существуетThreadа такжеProcess, следует предпочестьProcess,так какProcessболее устойчивый и,Processмогут быть распределены между несколькими машинами, в то время какThreadОн может быть распределен не более чем на несколько процессоров одной машины.

ПитонmultiprocessingМодуль поддерживает не только многопроцессорность, но иmanagersПодмодули также поддерживают распределение нескольких процессов на нескольких машинах. Сервисный процесс может действовать как планировщик, распределяя задачи между другими процессами, полагаясь на сетевое взаимодействие. потому чтоmanagersМодули хорошо инкапсулированы, и легко писать распределенные многопроцессорные программы, не зная деталей сетевого взаимодействия.

Как пример: если у нас уже есть пропускQueueМногопроцессорная программа для связи выполняется на одной машине.Теперь, поскольку процесс обработки задачи имеет тяжелую задачу, есть надежда, что процесс отправки задачи и процесс обработки задачи распределены по двум машинам. Как использовать распределенный процесс для достижения?

оригинальныйQueueтем не менее, может продолжать использоватьсяmanagersручка модуляQueueВыставив его через сеть, к нему могут получить доступ процессы других машин.Queue.

Давайте сначала посмотрим на сервисный процесс, сервисный процесс отвечает за запускQueue, зарегистрируйте Очередь в сети, а затем пропишите задачи в Очередь:

# _*_ coding:utf-8 _*_
import random, time, queue
from multiprocessing.managers import BaseManager

# the queue of send tasks
task_queue = queue.Queue()
# the queue of recive tasks
result_queue = queue.Queue()

# cong BaseManager jicheng de QueueManager
class QueueManager(BaseManager):
    pass

# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
QueueManager.register('get_task_queue', callable=lambda: task_queue)
QueueManager.register('get_result_queue', callable=lambda: result_queue)

# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('', 5000), authkey=b'abc')

# 启动Queue
manager.start()

# 获得通过网络访问的Queue对象
task = manager.get_task_queue()
result = manager.get_result_queue()

# 放几个任务进去
for i in range(10):
    n = random.randint(0, 10000)
    print('Put task %d...' % n)
    task.put(n)

# 从result队列读取结果:
print('Try get results...')
for i in range(10):
    r = result.get(timeout=10)
    print('Result: %s' % r)

# Close
manager.shutdown()
print('master exit.')

Обратите внимание, что когда мы пишем многопроцессорную программу на машине, созданныйQueueЕго можно использовать напрямую, но в распределенной среде с несколькими процессами добавьте задачи вQueueне напрямую к оригиналуtask_queueработать и обходитьсяQueueManagerпакет, должен пройтиmanager.get_task_queue()приобретенныйQueueдобавлен интерфейс.

Затем запустите процесс задачи на другой машине (возможен запуск и на этой машине):

# task_worker.py

import time, sys, queue
from multiprocessing.managers import BaseManager

# 创建类似的QueueManager:
class QueueManager(BaseManager):
    pass

# 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')

# 连接到服务器,也就是运行task_master.py的机器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 端口和验证码注意保持与task_master.py设置的完全一致:
m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
# 从网络连接:
m.connect()
# 获取Queue的对象:
task = m.get_task_queue()
result = m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
for i in range(10):
    try:
        n = task.get(timeout=1)
        print('run task %d * %d...' % (n, n))
        r = '%d * %d = %d' % (n, n, n*n)
        time.sleep(1)
        result.put(r)
    except Queue.Empty:
        print('task queue is empty.')
# 处理结束:
print('worker exit.')

Процессу задачи необходимо подключиться к сервисному процессу через сеть, поэтому укажите IP-адрес сервисного процесса.

Теперь пришло время попробовать, как работает распределенный процесс. начать первымtask_master.pyпроцесс обслуживания:

$ python3 task_master.py
Put task 3411...
Put task 1605...
Put task 1398...
Put task 4729...
Put task 5300...
Put task 7471...
Put task 68...
Put task 4219...
Put task 339...
Put task 7866...
Try get results...

task_master.pyПосле того, как процесс отправил задачу, он начинает ждатьresultрезультат очереди. начинай сейчасtask_worker.pyпроцесс:

$ python3 task_worker.py
Connect to server 127.0.0.1...
run task 3411 * 3411...
run task 1605 * 1605...
run task 1398 * 1398...
run task 4729 * 4729...
run task 5300 * 5300...
run task 7471 * 7471...
run task 68 * 68...
run task 4219 * 4219...
run task 339 * 339...
run task 7866 * 7866...
worker exit.

task_worker.pyпроцесс заканчивается, вtask_master.pyПроцесс будет продолжать распечатывать результаты:

Result: 3411 * 3411 = 11634921
Result: 1605 * 1605 = 2576025
Result: 1398 * 1398 = 1954404
Result: 4729 * 4729 = 22363441
Result: 5300 * 5300 = 28090000
Result: 7471 * 7471 = 55815841
Result: 68 * 68 = 4624
Result: 4219 * 4219 = 17799961
Result: 339 * 339 = 114921
Result: 7866 * 7866 = 61873956

Какая польза от этой простой модели Master/Worker? По сути, это простые, но настоящие распределенные вычисления.Небольшим изменением кода и запуском нескольких воркеров задачу можно распределить на несколько или даже десятки машин.К примеру, код для вычисления n*n заменяется отправкой писем ., реализована асинхронная отправка почтовой очереди.

Где хранятся объекты очереди? уведомлениеtask_worker.pyКода для создания Очереди вообще нет, поэтому объект Очереди хранится вtask_master.pyв ходе выполнения.

Причина, по которой Queue может быть доступна через сеть, заключается в том, чтоQueueManagerосуществленный. потому чтоQueueManagerУправляется более чем одна очередь, поэтому дайте имя интерфейсу сетевого вызова каждой очереди, напримерget_task_queue.

authkeyКакая польза? Это делается для того, чтобы две машины нормально обменивались данными и не подвергались злонамеренному вмешательству со стороны других машин. еслиtask_worker.pyизauthkeyа такжеtask_master.pyизauthkeyНесовместимо, определенно не связано.

Интерфейс распределенного процесса Python прост и хорошо упакован, что подходит для сред, в которых необходимо распределять тяжелые задачи на несколько машин.

Обратите внимание, что роль Очереди заключается в передаче задач и получении результатов, а объем данных описания для каждой задачи должен быть как можно меньше. Например, чтобы отправить задачу на обработку лог-файла, вместо отправки лог-файла размером в несколько сотен мегабайт отправьте полный путь, где хранится лог-файл, и рабочий процесс затем прочитает файл на общем диске.