Python: threads differently


Introduction

Do you know why I decided to write this article? I wrote a program where I used threads. While working with them Python more and more you convince yourself that everything is bad with them. No, it’s not that they don’t work well. Just using them, to put it mildly, is inconvenient. I decided to write a simple but more convenient library, and I’ll share the process here.

PS: At the end I will leave a link to GitHub

The first steps

The first thing I decided to start with is the convenient creation of threads from functions. To do this, I wrote a simple decorator, it looks like this:

# 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
# конец файла

That’s all. Now you can create streams very conveniently:

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

More convenient than before, right? But I didn’t get enough.

Thread Environments

It became not convenient for me to ensure the correct interaction of threads. For example, I need 2 loops to solve sequences of numbers for 3n+1. Then I had to suffer Python global variables. And I found a way!

PS: Maybe it is ThreadPoolExecutorwhich I have never used. If so, then perhaps my method will simply seem more convenient to someone.

Let’s explain. You create an environment for threads (multiple possible) and Little adapt streaming functions for yourself. library code:

# 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
# конец файла

An example of an error output:

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

How quickly the amount of code has grown compared to the previous version! So, an example of usage:

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}")

Conclusion:

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

So, this is where I will end this article. Thank you for your attention!

GitHub

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *