Package: parallel-utils
parallel-utilsThis library implements a classMonitor, as defined byPer
Brinch HansenandC.A.R. Hoare,
forsynchronization and concurrent management of threads and processes in Python. It also providesother
functions to ease the creation and collection of results for both threads and processes.Table of contentsInstallationUsageMonitor classFirst example@synchronizedSecond example@synchronized_priorityStaticMonitor classLaunching threads and processesContributingLicenseInstallationUse the package managerpipto installparallel-utils.pip3installparallel-utilsUsageMonitor classThere are two implementations of theMonitorclass: one is located in thethreadmodule and the other in theprocessmodule ofparallel-utils.Although it's safe to always use theMonitorclass located in theprocessmodule, even if you're working only with
threads, you will get slightly better performance when using the one located in thethreadmodule. Therefore, it is
recommended to use each one for what it is intended for.From now on, for ease of reading, every time we saythreadwe will also be includingprocessunless stated otherwise.Also, from now until the end of this section, when we sayfunction, we will not only refer to "whole" functions but we
will also include pieces of code contained within a function.A monitor essentially does two things:It controls the maximum number of threads that can simultaneously access a function.It organizes a set of functions so that they follow a strict order in their execution, regardless of the thread
from which they are called.First exampleIt allows controlling the maximum number of threads that can simultaneously access a function.To achieve this first goal, the Monitor class includes the following couple of functions:deflock_code(self,uid:Union[str,int],max_threads:int=1)defunlock_code(self,uid:Union[str,int])The first one,lock_code, must be called at the beginning of the piece of code for which we want to control the maximum
number of threads that can access it simultaneously.Theunlock_codefunction sets the limit of the scope of thelock_codefunction.To do this, both functions must share a same unique identifier (uid), that can be either a string or
an integer number. Let's see an example:importconcurrent.futuresfromtimeimportsleepfromparallel_utils.threadimportMonitor,create_threadm=Monitor()defprint_and_sleep():print('Hello')m.lock_code('example',max_threads=1)sleep(2)m.unlock_code('example')print('Goodbye')th1=create_thread(print_and_sleep)th2=create_thread(print_and_sleep)th3=create_thread(print_and_sleep)concurrent.futures.wait([th1,th2,th3])The example shown above takes 6 seconds to finalize its execution, since we have alock_codethat only allows one thread
each time to execute thesleepfunction, and we are launching three threads.If we set thelock_codeto allow up to three threads at the same time, then the code only needs 2 seconds to finalize
its execution, since all the three threads can make thesleepblocking call at the same time.We'll se more about thecreate_threadandcreate_processfunctions later.The last line,concurrent.futures.waitis a blocking call that waits until all the three threads finish running.@synchronizedIn the previous example, we just were protecting the piece of code wrapping thesleepcall. But, what if we want to wrap
the entire function?Of course we could calllock_codeon the first line, andunlock_codeon the last one, and that would work
just fine. Like this:defprint_and_sleep():m.lock_code('example',1)print('Hello')sleep(2)print('Goodbye')m.unlock_code('example')But, to simplify life for the programmer and for improving readability, there's some syntactic sugar we could use. And this is
where the@synchronizeddecorator comes in and turns the above code into this:fromparallel_utils.threadimportsynchronized@synchronized()defprint_and_sleep():print('Hello')sleep(2)print('Goodbye')Let's see the decorator prototype:@synchronized(max_threads:int=1)As you can see, the@synchronizeddecorator doesn't need an identifier, and by default only allows one thread to enter the
function at the same time. However, we can override that default behavior with the optionalmax_threadsargument.If we want, for example, to allow up to two threads to enter the function at the same time, we only need to write:@synchronized(2)defprint_and_sleep():print('Hello')sleep(2)print('Goodbye')Note thatthis decorator has its own namespace for uids, which is completely independent of the namespace of thelock_codeandunlock_codefunctions.Second example2. It organizes a set of functions so that they follow a strict order in their execution, regardless of the thread
from which they are called.To achieve the second goal, the Monitor class includes the following couple of functions:deflock_priority_code(uid:Union[str,int],order:int=1,total:int=1)defunlock_code(uid:Union[str,int],order:int)Yes, theunlock_codefunction is the same as before. And these two functions work quite similarly to the previous example,
wrapping the code snippet that we want to control and sharing the sameuidbetween them.The main difference is that, in this case, we have to specify theorderin which the code snippet will run and thetotalnumber of functions to sync with the supplieduid.Let's see an example:fromtimeimportsleepfromparallel_utils.processimportMonitor,create_processm=Monitor()defsay_hello(name):print('Entering hello')m.lock_priority_code('id1',order=1,total=2)print(f'Hello{name}!')m.unlock_code('id1')defsay_goodbye(name):print('Entering goodbye')m.lock_priority_code('id1',order=2)print(f'Goodbye{name}!')m.unlock_code('id1')create_process(say_goodbye,'Peter')sleep(3)create_process(say_hello,'Peter')this example will always print:Entering goodbye
. . . (3 secs after)
Entering hello
Hello Peter!
Goodbye Peter!even if you start thesay_goodbyefunction long before thesay_hellofunction. This is because the snippet insay_goobyehas
not the first turn, but the second, so it will make a blocking call and wait untilsay_hellocallsunlock_code.Thetotalargument must be suppliedat least oncein any of the calls tolock_priority_code.With these two functions, you can sort the execution of as many code snippets as you need.Note thatlock_codeandlock_priority_codeshare the same namespace for uids.@synchronized_priorityJust like before, there is a decorator that we can use to wrap a complete function and set its relative order of execution
with respect to others.This is its prototype:@synchronized_priority(uid:Union[str,int],order:int=1,total:int=None)With it, the last example would look like this:fromparallel_utils.processimportcreate_process,synchronized_priority@synchronized_priority('id1',order=1)defsay_hello(name):print('Entering hello')print(f'Hello{name}!')@synchronized_priority('id1',order=2,total=2)defsay_goodbye(name):print('Entering goodbye')print(f'Goodbye{name}!')create_process(say_goodbye,'Peter')sleep(3)create_process(say_hello,'Peter')Note that this time we've provided thetotalargument in the second call instead of the first one. Never mind. You
could even supply it in both.The above example will always print:. . . (sleeps 3 secs)
Entering hello
Hello Peter!
Entering goodbye
Goodbye Peter!Note thatthis decorator has its own namespace for uids, which is completely independent of the namespace of thelock_priority_codeandunlock_codefunctions.StaticMonitor classTheStaticMonitorclass has exactly the same methods as theMonitorclass. And it also has two implementations: one can
be imported fromparallel_utils.processand the other can be imported fromparallel_utils.thread.This class saves you the need to instantiate aMonitor, store it in a variable, and then use it. Instead of that, you can
just call its methods like this:fromparallel_utils.processimportStaticMonitordefsay_hello(name):print('Entering hello')StaticMonitor.lock_priority_code('id1',order=1,total=2)print(f'Hello{name}!')StaticMonitor.unlock_code('id1')Note that this class, since it is static, has a unique namespace for uids that is shared among all calls to its methods.Launching threads and processesThis library includes two very useful functions to quickly start processes and threads, and retrieve their results, which we
have already seen in our examples:defcreate_thread(func:Callable,*args:Any,**kwargs:Any)->Futuredefcreate_process(func:Callable,*args:Any,**kwargs:Any)->FutureLike the rest of classes and objects in this library, they are located inparallel_utils.threadandparallel_utils.processrespectively.Their first argument is a Callable that, in turn, is called with*argsand**kwargs.They both return aFutureobject, which encapsulates the asynchronous execution of a callable.Although theFutureclass has
several interesting methods that you should read in the official documentation, by far the most interesting one is probablytheresult()method, which makes a blocking call until the thread finishes and returns.Another interesting feature of these two methods is thatyou don't need to worry about zombie processes anymoredue to a
forgottenjoin()call, since all this logic is handled automatically. Just focus on making calls and retrieving results.In this example, we compute two factorials, each one in a different process, to bypass the Python GIL and have real
parallelism, and then we retrieve and print their results:fromparallel_utils.processimportcreate_processdeffactorial(n):res=1foriinrange(1,n+1):res*=ireturnresf1=create_process(factorial,5)f2=create_process(factorial,7)print(f1.result(),f2.result())ContributingPull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.LicenseThis library is licensed under theGNU General Public License v3 or later (GPLv3+)