Курс, связанный с многоядерным и многопоточным параллелизмом и микшированием Python.

задняя часть Python TensorFlow малиновый пирог

Оригинальный адрес:blogof33.com/post/8/

предисловие

В недавнем проекте к Raspberry Pi (четырехъядерный процессор ARM Cortex-A53, память 1 ГБ) подключены две ультразвуковые волны, которые необходимо постоянно измерять. Raspberry Pi используется в качестве робота-мастера для управления движением робота.Кроме того, Raspberry Pi также запускает камеру и веб-сервер (используемый для передачи видеопотоков), а модель распознавания изображений Tensorflow находится в фоновом режиме. и изображения будут выполняться с регулярными интервалами по мере необходимости. Поскольку конфигурация Raspberry Pi невысока, четыре ядра распознавания изображений Tensorflow будут работать на полную мощность каждый раз, когда вы запускаете Tensorflow, поэтому, чтобы ультразвуковая волна не влияла на распознавание Tensorflow и предотвращала долгосрочную загрузку ЦП. из-за того, что плата расширения слишком высока, чтобы сжечь ее, необходимо уменьшить использование ультразвукового процессора.

Python GIL

Поскольку есть два ультразвука для одновременного измерения расстояния, поскольку программа управления написана на питоне, наша первая мысль — использовать Python.threadМетод start_new_thread в библиотеке создает дочерний процесс:

thread.start_new_thread(checkdist,(GPIO_R1,var1,))
thread.start_new_thread(checkdist,(GPIO_R2,var2,))

checkdist — это ультразвуковая программа, подробности см. в предыдущей статье.Ультразвуковой дальномер Raspberry Pi 3B Python GPIO/WPI/BCM, три способа.

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

6tpgz.png

Почему вся программа работает на одном ядре? Я проверил информацию и обнаружил, что именно из-за существования GIL в переводчике Cpython.

что такое ГИЛ

Первое, что нужно уяснить, это то, что **GIL (глобальная блокировка интерпретатора, глобальная блокировка интерпретатора)** не является функцией Python, это концепция, введенная при реализации интерпретатора Python (CPython, реализованного на языке C). Существует множество видов интерпретаторов, и один и тот же фрагмент кода может выполняться в разных средах выполнения Python, таких как CPython, PyPy и JPython. Нет такого GIL, как JPYTHON. Однако, поскольку CPython является средой выполнения Python по умолчанию в большинстве сред. Поэтому в представлении многих людей CPython — это Python, и это считается само собой разумеющимся.GILЭто сводится к недостатку языка Python. Итак, давайте сначала проясним: GIL не является функцией Python, и Python может быть полностью независимым от GIL.

Весь код C в интерпретаторе CPython должен удерживать эту блокировку при выполнении Python. Гвидо ван Россум, отец Python, добавил эту блокировку, потому что в те дни многоядерность не была распространена, а блокировка была достаточно проста в использовании, гарантируя, что только один поток может получить доступ к общим ресурсам в одно и то же время (здесь, при переключении потоки, CPython Он может выполнять совместную многозадачность или вытесняющую многозадачность.эта статья), но это также делает python неспособным достичь многоядерности и многопоточности.С приходом эры многоядерности GIL обнажает присущие ему недостатки, не только делая программу неспособной работать параллельно с многоядерными и многопоточными -threading, но и многоядерность повлияет на эффективность многопоточности.

Итак, python развивается по сей день, почему нельзя удалить GIL и заменить его более качественной реализацией?

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

Итак, как нам обойти ограничения GIL? Я предпринял несколько попыток.

Обход GIL

multiprocessing

Чтобы программа работала на нескольких ядрах одновременно, если вы используете только python, обычно используйтеmultiprocessingНапишите многопроцессорную программу:

from multiprocessing import Process
p1=Process(target=checkdist,args=(GPIO_R1,var1,))
P2=Process(target=checkdist,args=(GPIO_R2,var2,))
P1.start()
P2.start()
P1.join()
P2.join()

Использование процессора:

6tM5Q.png

Выяснилось, что ультразвуковая часть программы работает на двух ядрах, а родительский процесс — на другом ядре, поэтому робот не застревает в движении! Тем не менее, оба ядра заполнены, и хотя проблема с задержкой решена, загруженность еще хуже. Когда возникает такая проблема, процесс слишком тяжелый? Осматриваясь в Интернете, многие люди говорят, что ультразвуковая бесконечная петля действительно заполнит ядро.

Итак, нет ли обходного пути? В это время я подумал о языке C. Могу ли я использовать язык C для обхода ограничений GIL?

Танцы с С

«Питон иногда бывает швейцарским армейским ножом». Официальный интерпретатор CPython реализован на языке C, поэтому Python имеет возможность интеграции с C/C++. Если вы используете язык C для ультразвукового определения расстояния и используете Python для вызова, сможете ли вы добиться многоядерности и многопоточности?

у питона естьctypesБиблиотека, которая может реализовать требования Python, вызывающего функции языка C. После прочтения официального документа мы сначала напишем функцию на языке C, как показано ниже. Подробное описание ультразвуковой функции см. в предыдущей статье.Ультразвуковой дальномер Raspberry Pi 3B Python GPIO/WPI/BCM, три способа.

//checkdist.c文件
//这里我的树莓派拓展板只能使用bcm编码方式
#include<stdio.h>
#include <bcm2835.h>
#include<termio.h>
#include<sys/time.h>
#include<stdlib.h>


 

void checkdist(int GPIO_R,int *var,int *signal,int GPIO_S,void(*t_stop)(int)){
	struct timeval tv1;  
    struct timeval tv2; 
	long start, stop; 
	double dis;
	if(!bcm2835_init()){  
        printf("setup bcm2835 failed !");  
        return;   
    } 
	bcm2835_gpio_fsel(GPIO_S, BCM2835_GPIO_FSEL_OUTP);
	bcm2835_gpio_fsel(GPIO_R, BCM2835_GPIO_FSEL_INPT);
	while(1){
		if(*signal==1)
			continue;
		else
			*signal=1;
		printf("GPIO:%d\n",GPIO_R);
		bcm2835_gpio_write(GPIO_S,HIGH);
		bcm2835_delayMicroseconds(15);//延时15us
		bcm2835_gpio_write(GPIO_S,LOW);
		while(!bcm2835_gpio_lev(GPIO_R));

		gettimeofday(&tv1, NULL);
		while(bcm2835_gpio_lev(GPIO_R));

		gettimeofday(&tv2, NULL);
		start = tv1.tv_sec * 1000000 + tv1.tv_usec;   //微秒级的时间  
    		stop  = tv2.tv_sec * 1000000 + tv2.tv_usec;
		dis = ((double)(stop - start) * 34000 / 2)/1000000;  //求出距离 
		if(dis<5){
			*var=1;
			t_stop(1);
		}
		else
			*var=0;
		printf("dist:%lfcm\n",dis);
		*signal=0;
		bcm2835_delay(100);//延时15ms
		
	}
	
}

Затем используйте следующую команду для создания объектного файла:

gcc checkdist.c -shared -fPIC -o libcheck.so

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

Затем используйте следующий код Python для реализации функции потока языка C:

#checkdist函数原型为:
#void checkdist(int GPIO_R,int *var,int *signal,int GPIO_S,void(*t_stop)(int));
#t_stop为python函数,函数原型为:def t_stop(t_time)

lib=cdll.LoadLibrary("/home/pi/Raspbarry_Tensorflow_Car/Servo/MotorHAT/libcheck.so")#加载DLL
stop_func=CFUNCTYPE(None,c_int)
#CFUNCTYPE的第一个参数是函数的返回值,void则为NULL,函数的其他参数紧随其后
func=stop_func(t_stop)#回调函数,func为函数指针类型,指向python中的t_stop函数
signal=c_int(0)#c语言中的int类型
var1=c_int(0)
var2=c_int(0)
#创建两个子线程,线程函数为C语言函数checkdist
thread.start_new_thread(lib.checkdist,(GPIO_R1,byref(var1),byref(signal),GPIO_S,func,))
thread.start_new_thread(lib.checkdist,(GPIO_R2,byref(var2),byref(signal),GPIO_S,func,)) 

Вот некоторые детали приведенного выше кода.

lib=cdll.LoadLibrary("/home/pi/Raspbarry_Tensorflow_Car/Servo/MotorHAT/libcheck.so")

Эта строка кода загружает объектный файл языка C libcheck.so.

Библиотека ctypes предоставляет три объекта для простой загрузки библиотек динамической компоновки: cdll, windll и oledll. Доступ к свойствам этих трех объектов позволяет вызывать функции библиотеки динамической компоновки. Среди них cdll в основном используется для загрузки метода вызова языка C (cdecl), windll в основном используется для загрузки метода вызова WIN32 (stdcall), а oledll использует метод вызова WIN32 (stdcall), а возвращаемое значение — значение HRESULT. вернулся в Windows. В языке C параметры передаются в стеке, а порядок — справа налево.Разница между cdll и двумя последними заключается в том, что при очистке стека cdll использует вызывающую программу для очистки стека, поэтому переменный параметр Функции могут использовать только это соглашение о вызовах, в то время как Windll и oledll используют очистку вызываемого объекта, и вызываемая функция очищает стек переданных параметров перед возвратом, а количество параметров функции является фиксированным. Следующий рисунок может быть хорошо отражен:

8KPEO.png

Здесь используется метод cdll для предотвращения ненужных ошибок.

Затем вызовите функцию обратного вызова:

stop_func=CFUNCTYPE(None,c_int)
#CFUNCTYPE的第一个参数是函数的返回值,void则为NULL,函数的其他参数紧随其后
func=stop_func(t_stop)#回调函数,func为函数指针类型,指向python中的t_stop函数

Эти две строки предназначены для преобразованияt_stopФункция передается в следующую функцию C, чтобы можно было вызвать функцию языка Ct_stopфункция.

Затем следующая строкаsignal=c_int(0)Эквивалент языка Cint singel=0;Следующие строки совпадают.

И наконец:

#创建两个子线程,线程函数为C语言函数checkdist
thread.start_new_thread(lib.checkdist,(GPIO_R1,byref(var1),byref(signal),GPIO_S,func,))
thread.start_new_thread(lib.checkdist,(GPIO_R2,byref(var2),byref(signal),GPIO_S,func,)) 

Создайте поток, функция параметра - это checkdist функции языка C,byref(var1)эквивалентно&var1, передайте адрес var1, остальные аналогичны.

Затем запустим и посмотрим, что получится:

6t4ah.png

Хороший! Мы видим, что результаты превзошли ожидания, а эффект оказался намного лучше ожидаемого! Были достигнуты не только многоядерность и многопоточность, но и значительно снижена загрузка ЦП. Уровень занятости двухъядерных процессоров в основном колеблется от 2% до 30%, что намного лучше предыдущего.

Эпилог

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

Поделиться с тобой.