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

Паралелно Програмиране с Процеси - Основни понятия

Паралелно Програмиране с Процеси - Основни понятия

Характеристики на Процесите

Всеки процес има собствен сегмент памет, който не може да бъде достъпен от останалите процеси.
За да могат множество процеси да обменят данни се използват различни техники, известни с общото име Inter Process Communication (IPC).
При стартирането на една Python програма се стартира поне един процес и поне една нишка за главната програма.
Множество процеси могат да работят едновременно на различни ядра. За разлика от "паралелизмът" чрез нишки, в Python, паралелизмът чрез процеси не е засегнат от GIL.

Характеристики на Процесите

Модулът Multiprocessing

multiprocessing е вграден модул в Python чрез който може да разпаралелим програмата си в множество Процеси.
Процес се създава чрез Process конструктора.
Веднъж създаден Процесът трябва да бъде стартиран чрез start() метода
Ако искаме да блокираме работата на процеса, създал даден процес, то е необходимо да изпозлваме метода join()
API-тата предоставени от multiprocessing модула са почти идентични с тези на threding модула, което прави използването на Процеси в Python изключително лесно.
Документация: multiprocessing — Process-based parallelism @docs.python.org

Създаване и стартиране на процес - Пример


			import multiprocessing as mp
			import time

			def worker(x):
				pid = mp.current_process().name;
				print("x = {} in {}".format(x, pid))
				time.sleep(2)


			if __name__ == '__main__':
				# create the process
				p = mp.Process(target=worker, args=(42,))

				# start the process:
				p.start()

				# wait until process completes:
				p.join()

				print("Worker did its job as separate Process!")
		

Programming guidelines for using multiprocessing

There are certain guidelines and idioms which should be adhered to when using multiprocessing: Programming guidelines @python3 docs.
But most important is to make sure that the main module can be safely imported by a new Python interpreter without causing unintended side effects (such as starting a new process)
I.e. always start a new process in main program, i.e. in if __name__ == '__main__':, and not in a file which will be imported

Процесите не споделят обща памет - пример


			import multiprocessing as mp

			def increment(r):
				global x

				for _ in r:
					x+=1


				print(f"x in {mp.current_process().name}: {x}")


			if __name__ == "__main__":
				x = 0

				pr1 = mp.Process(target=increment, args=(range(1000),))
				pr2 = mp.Process(target=increment, args=(range(1000),))

				pr1.start();pr2.start();
				pr1.join();pr2.join();


				print(f"x in {mp.current_process().name}: {x}")

				#OUTPUT
				# x in Process-1: 1000
				# x in Process-2: 1000
				# x in MainProcess: 0
		

Забележете, че всеки от процесите работи със собствено копие на x, което не е свързано с x в главния процес (главната програма).

Комуникация между процеси (Inter-Process Communication)

Когато използваме процеси, които работят с общи данни, обикновено се използват "съобщения" чрез които процесите обменят данните.
За целта в Python са имплементирани методите:
Pipe() - осъществява връзка между два процеса чрез която те могат да обменят съобщения.
Queue() - осъществява връзка между множество процеси, разграничени на "производители" и "консуматори" чрез която те могат да обменят данни.
И двата метода използват FIFO (First-In, First-Out) структура

Какво е FIFO структура от данни?

Абстрактна структура от данни която изпълнява условието "първият влязъл е първият излязъл"
Tова означава, че след като е добавен един елемент в края на опашката, той ще може да бъде извлечен (премахнат) единствено след като бъдат премахнати всички елементи преди него в реда, в който са добавени.
FIFO_Diagram.png

Какво е FIFO структура от данни?

В Python модулът queue предлага FIFO структура, чрез класа queue.Queue()

			import queue

			queue = queue.Queue()

			queue.put(1)
			queue.put(2)
			queue.put(3)


			el1 = queue.get()
			el2 = queue.get()
			el3 = queue.get()

			print(el1,el2,el3)

			#OUTPUT
			1 2 3
		

Inter-Process Communication (IPC) чрез класа multiprocessing.Queue

В multiprocessing модула е дефиниран клас Queue който предлага FIFO структура за комуникация между процесите.
multiprocessing.Queue има методи подобни на queue.Queue класа но е оптимизиран за работа с процеси.
Основни методи на multiprocessing.Queue са:
put() - за запис на данни в опашката
get() - за четене на данни от опашката

Inter-Process Communication (IPC) чрез класа multiprocessing.Queue - Пример


			import multiprocessing as mp

			def worker(q):
				# get data from queue:
				x = q.get()

				x+=1

				# save data to the queue
				q.put(x)
				print(f'x in {mp.current_process().name} = {x}')

			if __name__ == '__main__':
				# create a Queue object which will be shared among all processes
				queue = mp.Queue()

				# set the initial value for queue
				queue.put(0)

				processes = []

				# crate and start 3 processes:
				for _ in range(3):
					pr = mp.Process(target=worker, args=(queue,))
					pr.start()
					processes.append(pr)

				# wait for processes to end:
				for pr in processes:
					pr.join()

				# get element from queue:
				x = queue.get()

				print(f'x in {mp.current_process().name} = {x}')

		

Забележете, че обекта queue трябва да бъде споделен между процесите, които искат да имат достъп до тези данни.

The Pool object

The Pool object in multiprocessing module offers a convenient means of parallelizing the execution of a function across multiple input values, distributing the input data across processes (data parallelism)

			from multiprocessing import Pool
			import time

			def worker(n):
			  # for light work, the pool is not efficient, try with n**10
			  return n**1000

			if __name__ == '__main__':
			  t =time.time()

			  # create the Pool:
			  p = Pool(processes=5)
			  result = p.map(worker, range(100000))
			  p.close()
			  p.join()

			  print("Pool took: ", time.time() - t)

			  # serial processing:
			  t = time.time()
			  result = []
			  for x in range(100000):
			    result.append(worker(x))
			  # print("Result: ", result)
			  print("Serial processing took: ", time.time() - t)
		

Processes vs Threads - when to use which

Processes vs Threads - when to use which

Multiprocessing Pros and Cons

Multiprocessing Pros:
Takes advantage of multiple CPUs and cores
Avoids GIL limitations
Memory leaks in one process would not harm the others
Child processes could be killed
An intuitive and easy to use module APIs (very close to threading)
Very useful with cPython for CPU-bound processing
Cons:
Separate memory space is harder to manage.
Larger memory footprint

Threading Pros and Cons

Threading Pros:
Lightweight and low memory footprint
Shared memory between threads - easier to manage.
Perfect for responsive UIs, DB Querying, Online Data Retrieval, I/O-bound and other applications where a lot of background work is done
Cons:
A memory leak in one thread will corrupt all threads

calc_sum(start, end): Sequential, Threaded and Mutliprocessing Versions