Паралелно Програмиране - лекция 2

Създаване на нишки чрез подклас на Thread

Създаване на нишки чрез подклас на Thread

Защо и Как?

Можем да управляваме по-фино стартирането на нишки ако създадем наш клас, който да наследява Thread класа.
Основните стъпки са:
Създаване на нов подклас на класа Thread.
Пренаписване на __init__(self [,args]) метода за да добавим собствени аргументи.
Пренаписване на run(self [,args]) метода за да укажем собствена функционалност при стартиране на нишка.

Пример 1 - get thread's target return value

Ако желаем да достъпим локална променлива във функция изпълнена в отделна нишка,то можем:
Да използваме глобална променлива
Да създадем наш клас в който да съхраним променливата като атрибут на нишката (предпочитан вариант)

			from threading import Thread, current_thread
			from time import sleep

			class CubTread(Thread):
				def __init__(self, group=None, target=None, name=None,
							args=(), kwargs={}, Verbose=None):
					Thread.__init__(self, group, target, name, args, kwargs)

					#self.value will store the value we need from thread's target
					self.value = None

				def run(self):
					self.value = self._target(*self._args, **self._kwargs)

				def join(self):
					print(f'{self.name} res={self.value}')
					return self.value

			def cub(x):
				return x**3


			tr1 = CubTread(target=cub, args=(1,))
			tr2 = CubTread(target=cub, args=(2,))

			tr1.start(); tr2.start()
			res1 = tr1.join()
			res2 = tr2.join()

			print(res1+res2)
		

Споделени Ресурси/Критична Секция

Споделени Ресурси/Критична Секция

Нишките могат да споделят общи глобални променливи. Процесите, чрез IPC могат да споделят обща памет. Най-общо, нишките и процесите могат да си сътрудничат чрез споделени ресурси.
Критична секция е тази част от код в която се достъпват споделените ресурси.
Едновременният достъп до споделен ресурс може да доведе до т.нар. Race Condition.

Критична Секция - Пример

Имаме 5 нишки и глобален брояч, който се увеличава от всяка една нишка по 10000000 пъти
В края на процеса, стойността на глобалният брояч би трябвало да е 5*10000000 пъти, но тъй като той е споделен ресурс, виждаме че резултата е непредсказуем.
в Python 3.10 има недокументирана промяна, която предотвратява този race condition, дори и без да се използва lock. За да видите проблема, трябва да стартирате кода с python < 3.10

			import threading
			import time

			def worker():
				global counter

				for i in range(1_000_000):
					counter += 1


			counter = 0

			# create some treads to count together:
			thread_pool = []
			for i in range(2):
				tr = threading.Thread(target=worker)
				thread_pool.append(tr)

				print(f"Counter before start of {tr.name}: {counter}")
				tr.start()

			# wait for tread to finish:
			for tr in thread_pool:
				tr.join()

			print("Workers counted:", counter)
		

Критична Секция - Диаграма на проблема

Solution: Lock the critical sections


			import threading

			def worker():
				global counter

				# lock the 'critical section':
				lock.acquire()
				for i in range(1_000_000):
					counter += 1
				lock.release()


			counter = 0
			lock = threading.Lock()

			# create some treads to count together:
			thread_pool = []
			for i in range(5):
				tr = threading.Thread(target=worker)
				thread_pool.append(tr)

				print(f"Counter before start of {tr.name}: {counter}")
				tr.start()

			# wait for tread to finish:
			for tr in thread_pool:
				tr.join()

			print("Workers counted:", counter)
		

Състояния на нишките и GIL

Състояния на нишките и GIL

Състояния на нишките

Една нишка може да се намира в едно от съществуващите състояния:
създадена (created,new) -нишката е създадена, но не е стартирана (не използва никакви ресурси)
готова за старт (runable) -нишката разполага с необходимите ресурси, но изчаква да бъде стартирана
стартирана (running) - нишката изпълнява дадената задача. От това състояние може да премине "завършено" или в "изчакващо".
изчакваща (waiting) - работата на нишката е на пауза, поради изчакване на I/O или поради изчакване на друга стартирана нишка.
"мъртва" (dead) - нишката e приключила своето изпълнение или е "убита" поради други причини.
thread_states.jpg

Global Interpreted Lock (GIL)

GIL механизмът в Python не позволява повече от една нишка да работи в даден момент от времето.
При multithreading нямаме истински паралелизъм а кооперативно изчисление
Когато една нишка e стартирана тя получава GIL, но когато тя е в състояние на пауза или I/O операции, тя освобождава GIL, който се подава на друга чакаща нишка.

CPU-Bound и I/O-Bound задачи

CPU-Bound и I/O-Bound задачи

Всяка една задача, решавана от съвременните компютри може да се определи като CPU-Bound или I/O-Bound.
Разбирайки типа на задачата ще можем да определим по-лесно коя от библиотеките за паралелно програмиране в Python да използваме (threading, multiprocessing, asyncio).

CPU-Bound задачи

Казваме че една задача е CPU-bound когато времето за нейното изпълнение зависи изключително от бързодействието на процесора.
Класически пример за такъв тип задачи са тези обвързани с огромни математически изчисления (например: сумата на числата от 0 до 100 милиарда).
При такива задачи увеличаването на процесорната мощ ще увеличи бързодействието на програмата.

CPUBound

I/O-Bound задачи

Когато времето за изпълнение на една задача се определя предимно от входно-изходни (I/O) операции, като четене/запис в големи файлове, се казва че задачата е I/O Bound
Класически пример за такъв тип задачи са тези свързани с четене/запис в големи файлове; множество HTTP и/или DB заявки към сървъри.
За да се увеличи бързодействието на програмата при такъв тип задачи е необходимо да се увеличи скоростта на RAM и външната памет, или мрежовата скорост.

IOBound.webp

Примери и упражнения

Създаване на множество голями файлове
Даунлоад на множество Web страници
Даунлоад на множество Youtube видеа

References

References

Readings

Threading vs Multiprocessing in Python by Pawan Pundir
Grok the GIL: How to write fast and thread-safe Python

Videos

Python Multithreading/Multiprocessing - 6 videos on theme by codebasics