Pull to refresh

Python: потоки по-другому

Reading time4 min
Views16K

Введение

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

Tags:
Hubs:
Total votes 15: ↑3 and ↓12-7
Comments15

Articles