
Введение
Знаете, почему я решил написать эту статью? Я писал программу, где использовал потоки. Во время работы с ними в Python всё больше убеждаешь себя, что тут с ними всё плохо. Нет, не то, чтобы они плохо работали. Просто использовать их, мягко говоря, неудобно. Я решил написать простую, но более удобную библиотеку, и здесь поделюсь процессом.
P.S.: В конце оставлю ссылку на GitHub
Первые шаги
Первое, с чего я решил начать - это удобное создание потоков из функций. Для этого я написал простенький декоратор, выглядит он так:
# thr.py
from threading import Thread as thrd
__all__ = ['Thr']
# Класс для всего содержимого либы
class Thr:
# Собственно, декоратор
def thread(fn):
def thr(*args, **kwargs):
thrd(target = fn, args = (*args,), kwargs={**kwargs,}).start()
pass
return thr
pass
# конец файла
Вот и всё. Теперь можно очень удобно создавать потоки:
from thr import Thr
# пример использования
@Thr.thread
def func1(a, b, c):
if a+b > c:
if a+c > b:
if b+c > a:
print("треугольник существует")
pass
pass
pass
print("треугольник не существует")
pass # возвращение значений пока не предусмотрено
for a, b, c in zip(range(1, 10), range(6, 15), range(11, 20)):
func1(a, b, c)
pass
Удобнее чем было, так ведь? Но мне стало мало.
Среды для потоков
Мне стало не удобно обеспечивать верное взаимодействий потоков. Например, мне надо чтобы 2 цикла решали последовательности чисел для 3n+1. Тогда, мне приходилось мучить Python глобальными переменными. И я нашел выход!
P.S.: Возможно, это и есть ThreadPoolExecutor, которым я никогда не пользовался. Если это так, то, возможно, просто кому-то мой метод покажется удобнее.
Давайте объясню. Вы создаёте среду для потоков(можно несколько) и немного адаптируете потоковые функции под себя. код библиотеки:
# thr.py
from curses.ascii import isalnum
from threading import Thread as thrd
from random import randrange
from sys import exit as exitall
__all__ = ['Thr']
# f-str не позволяет использовать "\" напрямую,
# пришлось выкручиваться =)
nl = "\n"
bs = "\b"
tb = "\t"
rt = "\r"
# просто полезная функция
def strcleanup(s: str = ""):
while s[0] == ' ': s = s[1:]
while s[-1] == ' ': s = s[:-1]
if not isalnum(s[0]): s = '_' + s
s = s.replace(' ', '_')
for i in range(len(s)):
if not isalnum(s[i]):
s = s.replace(s[i], '_')
pass
pass
s += f"{randrange(100, 999)}"
return s
# Класс для всего содержимого либы
class Thr:
# класс для сред потоков
class Env(object):
# поля
# потоки
thrs: list = None
# возвращаемые значения
rets: dict = None
# название среды
name: str = None
# методы
# инициализация
def __init__(self, name):
self.thrs = []
self.rets = {}
self.__name__ = self.name = name
# self.name на всякий случай.
# __name__ - магическая переменная, вдруг поменяется.
pass
# в строку
__str__ = lambda self:\
f"""ThreadSpace "{self.name}": {len(self.thrs)} threads"""
# тоже в строку, но скорее для дебага, чем для печати юзеру
__repr__ = lambda self:\
f"""ThreadSpace "{self.name}"
threads:
{(nl+" ").join(self.thrs)}
total: {len(self.thrs)}
"""
def __add__(self, other):
self.thrs = {**self.thrs, **other.thrs}
pass
# Декоратор/метод для добавления в список потоков.
def append(self, fn):
# функции нужен docstring
ID = strcleanup(fn.__doc__.casefold())
self.thrs += [ID]
self.rets[ID] = None
#
class Thrd(object):
ID = None
space = None
fn = None
thr = None
runned = None
ret = None
def __init__(slf, ID, self, fn):
slf.ID = ID
slf.space = self
slf.fn = fn
slf.thr = None
slf.runned = False
slf.ret = False
pass
def run(slf, *args):
if slf.runned:
print(f"Exception: Thread \"{slf.ID[:-3]}\" of threadspace \"{slf.space.name}\" already started")
exitall(1)
pass
slf.thr = thrd(target = slf.fn, args = (slf, slf.space, slf.ID, *args,))
slf.thr.start()
slf.runned = True
pass
def join(slf):
if not slf.runned:
print(f"Exception: Thread \"{slf.ID[:-3]}\" of threadspace \"{slf.space.name}\" not started yet")
exitall(1)
pass
slf.thr.join()
slf.runned = False
pass
def get(slf):
if not slf.ret:
print(f"Exception: Thread \"{slf.ID[:-3]}\" of threadspace \"{slf.space.name}\" didn`t return anything yet")
exitall(1)
pass
slf.runned = False
return slf.space.rets[slf.ID]
def getrun(slf, *args):
slf.run(*args)
slf.join()
return slf.get()
pass
return Thrd(ID, self, fn)
pass
# Декоратор для "голого" потока
def thread(fn):
def thr(*args, **kwargs):
thrd(target = fn, args = (*args,), kwargs={**kwargs,}).start()
pass
return thr
pass
# конец файла
Пример вывода об ошибке:
Traceback (most recent call last):
File "test.py", line 37, in <module>
loop.run()
File "thr.py", line 93, in run
raise Exception(...)
Exception: Thread "3n_1_mainloop" of threadspace "3n+1" already started
Как быстро вырос объём кода по сравнению с предыдущим вариантом! Итак, пример использования:
from random import randint
from thr import Thr
Space = Thr.Env("3n+1")
@Space.append
def hdl(t, spc, ID, num):
"""3n+1_handle"""
if num % 2 == 0:
# значения возвращать так
t.ret = True
spc.rets[ID] = num/2
return
# значения возвращать так
t.ret = True
spc.rets[ID] = 3*num+1
return
@Space.append
def loop(t, spc, ID, num):
"""3n+1_mainloop"""
steps = 0
while num not in (4, 2, 1):
num = hdl.getrun(num)
steps += 1
pass
# значения возвращать так
t.ret = True
spc.rets[ID] = steps
return
print()
print(Space)
print()
print(repr(Space))
ticks = 0
num = randint(5, 100)
loop.run(num)
while not loop.ret:
ticks += 1
pass
print(f"loop reached 4 -> 2 -> 1 trap,\n"
f"time has passed (loop ticks):\n"
f"{ticks}, steps has passed: {loop.get()}, start number: {num}")
Вывод:
ThreadSpace "3n+1": 2 threads
ThreadSpace "3n+1"
threads:
3n_1_handle840
3n_1_mainloop515
total: 2
loop reached 4 -> 2 -> 1 trap,
time has passed (loop ticks):
13606839, steps has passed: 33, start number: 78
Итак, на этом и завершу эту статью. Спасибо за внимание!
Комментарии (15)
shurshur
09.04.2022 00:15+7Зачем везде вставлять pass? Число строк в качестве KPI было в моде совсем давно...
TalismanChet Автор
09.04.2022 11:13Это скорее привычка, которая помогает(мне лично) легче воспринимать границы блоков, даже где pass'ы не требуются.
funca
09.04.2022 06:46+5# функции нужен docstring
Мы все постоянно чему-то учимся, пробуем, узнаем для себя что-то новое. Старайтесь, чтобы результат в итоге всегда был максимально качественным, не делайте себе поблажек. Если видите, что можно сделать лучше - делайте лучше. Это не гарантирует того, что через год вы сами не будете смотреть на когда-то казалось бы гениальное решение без слез. Но такой подход станет залогом вашего постоянного роста как специалиста.
Без относительно реализации, в статье хотелось бы видеть описание исходной ситуации, объяснения зачем вообще понадобилось писать этот код, ход рассуждений как вы пришли к такому решению или что-то в этом духе.
ivankudryavtsev
09.04.2022 07:51+3Первый пост, которому я поставил "-", при этом плюсанул карму — ведь могет же автор что-то сделать нетривиальное) извращенное — да, но ведь почитать было интересно… а так, да — читайте документацию. Ну и проблема потоков в python не в том, что вы пишете, а в том, что их использовать (не в process) можно только ддя заполнения ожидания, но не для вычислений.
Бесполезно:def func1(a, b, c): if a+b > c: if a+c > b: if b+c > a: print("треугольник существует") pass pass pass print("треугольник не существует") pass # возвращение значений пока не предусмотрено
Recosh
09.04.2022 08:43+2В питоне есть подобный механизм, это использование asyncio и ThreadPoolExecutor или ProcessPoolExecutor. Можно настроить количество воркеров, ProcessPoolExecutor может вынести задачи в отдельный процесс для поддержки многоядерности приложением и т. д. Для глобальных переменных (что как по мне зло, и надо оборачивать грамотно контекст и прогрев кеша) можно юзать SharedMemory. Есть методы ожидания выполнения всех задач без создания цикла и механизм "запустить задачу и забыть", от чего приходится потом отказываться, так как в итоге надо проверить результат успешности выполнения фоновой задачи.
Savvy
09.04.2022 12:13+5Код, да, хочет ревью, но отдельно хочу отметить Shlemiel the painter’s algorithm
while s[0] == ' ': s = s[1:]
while s[-1] == ' ': s = s[:-1]Не делайте так со строками, им больно. Даже если бы не было str.strip()
SergeiMinaev
09.04.2022 14:29+4Делать такое для обучения - полезно. Ну и по себе знаю, что иногда настолько лень разбираться в дебрях имеющихся инструментов, что хочется сделать что-то своё. Главное, не питать на этот счёт особых иллюзий. Но всё впечатление портит первый абзац с наездом на потоки питона, после которого идёт код, по которому напрашивается вывод о нехватке опыта или о том, что вы пришли недавно из другого языка. А такие заявления, как в первом азбаце, как по мне, можно писать только если тема действительно изучена вдоль и поперёк.
1) Разнесение трёх if-ов на шесть строк - прям перебор.
2) Вfunc1()
print("Треугольник не существует")
будет выводиться всегда.
3) Про использованиеwhile s[0] == ' ': s = s[1:]
иwhile s[-1] == ' ': s = s[:-1]
вместоs.strip()
я молчу.
4) Никогда не видел, чтобы в ф-и, которая ничего не возвращает, в конце ставили pass. Если уж требуется обозначить её конец, можно возвращать None, т.е. писать просто return.
TAFH
10.04.2022 16:33# Класс для всего содержимого либы
Не советую так делать. Питон все же не обьектно-ориентированный язык. И толку от класса никакого.
# просто полезная функция
Это не основной функционал. Лучше сделать его приватным.
Также про соблюдения pep8 и pass после каждого блока - читаемость кода лучше не становится.
BasicWolf
Товарищ, вы однако садист. Может немного окунуться в тему о синхронизации потоков, узнать о мьютексах, семафорах, очередях и т.д.? Тот же раздел Threading и concurrent.futures из документации почитать? На последний советую обратить особое внимание, т.к. лёгким движением руки заменив
ThreadPoolExecutor
наProcessPoolExecutor
можно запускать функции не в потоках, а в процессах!Ещё
желательнообязательно PEP-8.И если идею первого декоратора (который, кстати, требует большой доработки напильником) ещё можно понять, то что до нас вы пытаетесь донести в остальной простыне кода?
TalismanChet Автор
Цель
Thr.Env
для меня в первую очередь - реализовать механизм на подобииThreadPoolExecutor
"своими руками", это скорее саморазвитие, или что-то в этом роде..TalismanChet Автор
BasicWolf
Ваши начинания в Питоне заслуживают лишь похвалы! Искать собственное решение и видеть его в работе приносит огромное удовольствие. Но вам не кажется, что категорично заявлять о том, что "у Питона с потоками всё плохо" может лишь человек глубоко разбирающийся в предмете?
Вы захотели поделиться с миром своими исследованиями. Отлино! Но тут ваш первый промах - вы показываете своё решение, не сравнивая его с решениями существующими. Открывая вашу статью, я ожидал увидеть:
1. Постановку задачи.
2. Существующие пути её решения и их анализ.
3. Ваше решение.
4. Сравнительный анализ.
Но вместо этого - "Удобнее чем было, так ведь?". А ведь банальное гугление "start thread in decorator" выдаёт вопрос на StackOverflow 2013го года, рецепт с ActiveState 2009го года и ещё огромную кучу материала, в котором вопрос обсасывается со всех сторон.
Куда уместнее ваш код был бы в контексте "ребята, я изучаю Питон и многопоточность и написал вот это, прокомментируйте пожалуйста".
Я надеюсь вы не принимаете вышесказанное близко к сердцу. У меня самого парочка исследовательских проектов и статей о них на Хабре, которые писались именно в таком ключе. Не мы первые, не мы последние :) Успехов вам!