Введение

Знаете, почему я решил написать эту статью? Я писал программу, где использовал потоки. Во время работы с ними в 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

Итак, на этом и завершу эту статью. Спасибо за внимание!

GitHub

Комментарии (15)


  1. BasicWolf
    08.04.2022 23:39
    +7

    Товарищ, вы однако садист. Может немного окунуться в тему о синхронизации потоков, узнать о мьютексах, семафорах, очередях и т.д.? Тот же раздел Threading и concurrent.futures из документации почитать? На последний советую обратить особое внимание, т.к. лёгким движением руки заменив ThreadPoolExecutor на ProcessPoolExecutor можно запускать функции не в потоках, а в процессах!

    Ещё желательно обязательно PEP-8.

    И если идею первого декоратора (который, кстати, требует большой доработки напильником) ещё можно понять, то что до нас вы пытаетесь донести в остальной простыне кода?


    1. TalismanChet Автор
      09.04.2022 11:11

      Цель Thr.Env для меня в первую очередь - реализовать механизм на подобии ThreadPoolExecutor "своими руками", это скорее саморазвитие, или что-то в этом роде..


    1. TalismanChet Автор
      09.04.2022 17:07

      P.S.: Возможно, это и есть ThreadPoolExecutor, которым я никогда не пользовался. Если это так, то, возможно, просто кому-то мой метод покажется удобнее.


      1. BasicWolf
        10.04.2022 12:19
        +2

        Ваши начинания в Питоне заслуживают лишь похвалы! Искать собственное решение и видеть его в работе приносит огромное удовольствие. Но вам не кажется, что категорично заявлять о том, что "у Питона с потоками всё плохо" может лишь человек глубоко разбирающийся в предмете?

        Вы захотели поделиться с миром своими исследованиями. Отлино! Но тут ваш первый промах - вы показываете своё решение, не сравнивая его с решениями существующими. Открывая вашу статью, я ожидал увидеть:
        1. Постановку задачи.
        2. Существующие пути её решения и их анализ.
        3. Ваше решение.
        4. Сравнительный анализ.

        Но вместо этого - "Удобнее чем было, так ведь?". А ведь банальное гугление "start thread in decorator" выдаёт вопрос на StackOverflow 2013го года, рецепт с ActiveState 2009го года и ещё огромную кучу материала, в котором вопрос обсасывается со всех сторон.
        Куда уместнее ваш код был бы в контексте "ребята, я изучаю Питон и многопоточность и написал вот это, прокомментируйте пожалуйста".

        Я надеюсь вы не принимаете вышесказанное близко к сердцу. У меня самого парочка исследовательских проектов и статей о них на Хабре, которые писались именно в таком ключе. Не мы первые, не мы последние :) Успехов вам!


  1. shurshur
    09.04.2022 00:15
    +7

    Зачем везде вставлять pass? Число строк в качестве KPI было в моде совсем давно...


    1. yesworldd
      09.04.2022 11:07
      +1

      Походу он из Руби перешел на Пайтон


    1. TalismanChet Автор
      09.04.2022 11:13

      Это скорее привычка, которая помогает(мне лично) легче воспринимать границы блоков, даже где pass'ы не требуются.


  1. funca
    09.04.2022 06:46
    +5

    # функции нужен docstring

    Мы все постоянно чему-то учимся, пробуем, узнаем для себя что-то новое. Старайтесь, чтобы результат в итоге всегда был максимально качественным, не делайте себе поблажек. Если видите, что можно сделать лучше - делайте лучше. Это не гарантирует того, что через год вы сами не будете смотреть на когда-то казалось бы гениальное решение без слез. Но такой подход станет залогом вашего постоянного роста как специалиста.

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


  1. 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 # возвращение значений пока не предусмотрено
    


  1. Recosh
    09.04.2022 08:43
    +2

    В питоне есть подобный механизм, это использование asyncio и ThreadPoolExecutor или ProcessPoolExecutor. Можно настроить количество воркеров, ProcessPoolExecutor может вынести задачи в отдельный процесс для поддержки многоядерности приложением и т. д. Для глобальных переменных (что как по мне зло, и надо оборачивать грамотно контекст и прогрев кеша) можно юзать SharedMemory. Есть методы ожидания выполнения всех задач без создания цикла и механизм "запустить задачу и забыть", от чего приходится потом отказываться, так как в итоге надо проверить результат успешности выполнения фоновой задачи.


  1. Tiendil
    09.04.2022 09:38
    -2

    От кода повеяло Rust-ом.


  1. tmnhy
    09.04.2022 11:42
    +1

    Когда энергии много, а с asyncio разбираться почему-то не стал.


  1. 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()


  1. 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.


  1. TAFH
    10.04.2022 16:33

    # Класс для всего содержимого либы

    Не советую так делать. Питон все же не обьектно-ориентированный язык. И толку от класса никакого.

    # просто полезная функция

    Это не основной функционал. Лучше сделать его приватным.

    Также про соблюдения pep8 и pass после каждого блока - читаемость кода лучше не становится.