As @Avernial correctly noted, in the standard Python implementation, CPython, GIL takes place, the presence of which blocks the parallel execution of threads in Python programs. But at the same time, no one forbids using processes for parallelism.
A fairly convenient way to implement the launch of parallel code in processes is provided by the standard multiprocessing
library. Below is a sample code adapted to your:
from multiprocessing import Process from time import sleep class A: def __call__(self, count=10, sleep_time=0.5): for i in range(count): print('Working class A, i=%s' % i) sleep(sleep_time) class B: def __call__(self, count=10, sleep_time=0.5): for i in range(count): print('Working class B, i=%s' % i) sleep(sleep_time) if __name__ == '__main__': a = A() b = B() p1 = Process(target=a, kwargs={'sleep_time': 0.7}) p2 = Process(target=b, args=(12,)) p1.start() p2.start() p1.join() p2.join()
The output may look something like this:
Working class A, i=0 Working class B, i=0 Working class B, i=1 Working class A, i=1 Working class B, i=2 Working class A, i=2 Working class B, i=3 Working class B, i=4 Working class A, i=3 Working class B, i=5 Working class A, i=4 Working class B, i=6 Working class A, i=5 Working class B, i=7 Working class B, i=8 Working class A, i=6 Working class B, i=9 Working class A, i=7 Working class B, i=10 Working class B, i=11 Working class A, i=8 Working class A, i=9
I note that if you use the capabilities of the package threading
on the same code, ie
import threading import os from time import sleep class A: def __call__(self, count=10, sleep_time=0.5): for i in range(count): print('Working class A, i=%s' % i) sleep(sleep_time) class B: def __call__(self, count=10, sleep_time=0.5): for i in range(count): print('Working class B, i=%s' % i) sleep(sleep_time) if __name__ == '__main__': a = A() b = B() t1 = threading.Thread(target=a, kwargs={'sleep_time': 0.7}) t2 = threading.Thread(target=b, args=(12,)) t1.start() t2.start() t1.join() t2.join()
then the output will be similar to the previous one. The explanation of this effect is given below.
You can use the threading
module if at some point the calculation of the function is interrupted, for example, when reading data, waiting for user input or when calling the time.sleep()
function, since in these cases the GIL is removed and other threads can intercept control.
Here is a quote from the documentation :
It can be used to make it easier for you to do this. If you want to use multi-core machines, you are advised to use multiprocessing
or concurrent.futures.ProcessPoolExecutor
. However, multiple I / O-bound tasks simultaneously .
My free translation:
Details of the CPython interpreter implementation: in CPython, due to the presence of GIL, only one Python code stream can be executed (even though some performance-oriented libraries can bypass this limitation). If you want to improve your application by adding the ability to use the capabilities of a multi-core computer, we suggest you use the capabilities of the multiprocessing
package or concurrent.futures.ProcessPoolExecutor
. However, threading
is suitable if you want to run multiple I / O-bound tasks at the same time .
I decided to give a more interesting example of comparing the work of the module threading
and multiprocessing
.
We will show that if there is no I / O or other operations leading to the release of the GIL in one of the threads, then control will not be transferred.
To do this, create two files. In one, the threading
module mechanism will be used, and in the other - multiprocessing
. The class B
function code will perform lengthy calculations without interrupting I / O. The rest of the code will be the same.
File threading_test.py:
import threading import os from time import sleep class A: def __call__(self, count=10, sleep_time=0.5): for i in range(count): print('Working class A, i=%s' % i) sleep(sleep_time) class B: def __call__(self, count=10, sleep_time=0.5): for i in range(count): x = 10 for j in range(50000): x *= 10 ** 9 # какая-то долгая операция print('Working class B, i=%s' % i) if __name__ == '__main__': a = A() b = B() t1 = threading.Thread(target=a, kwargs={'sleep_time': 0.7}) t2 = threading.Thread(target=b, args=(12,)) t1.start() t2.start() t1.join() t2.join()
Multiprocessing_test.py file:
from multiprocessing import Process import os from time import sleep class A: def __call__(self, count=10, sleep_time=0.5): for i in range(count): print('Working class A, i=%s' % i) sleep(sleep_time) class B: def __call__(self, count=10, sleep_time=0.5): for i in range(count): x = 10 for j in range(50000): x *= 10 ** 9 # какая-то долгая операция print('Working class B, i=%s' % i) if __name__ == '__main__': a = A() b = B() p1 = Process(target=a, kwargs={'sleep_time': 0.7}) p2 = Process(target=b, args=(12,)) p1.start() p2.start() p1.join() p2.join()
After running the code, we get the following output from the threading_test.py script:
Working class A, i=0 Working class B, i=0 Working class B, i=1 Working class B, i=2 Working class B, i=3 Working class B, i=4 Working class A, i=1 Working class B, i=5 Working class B, i=6 Working class B, i=7 Working class B, i=8 Working class B, i=9 Working class B, i=10 Working class B, i=11 Working class A, i=2 Working class A, i=3 Working class A, i=4 Working class A, i=5 Working class A, i=6 Working class A, i=7 Working class A, i=8 Working class A, i=9
and in the multiprocess_test.py script:
Working class A, i=0 Working class A, i=1 Working class A, i=2 Working class A, i=3 Working class B, i=0 Working class A, i=4 Working class A, i=5 Working class B, i=1 Working class A, i=6 Working class A, i=7 Working class A, i=8 Working class B, i=2 Working class A, i=9 Working class B, i=3 Working class B, i=4 Working class B, i=5 Working class B, i=6 Working class B, i=7 Working class B, i=8 Working class B, i=9 Working class B, i=10 Working class B, i=11
It can be noted that in the case of threading
, the class B
function, having taken possession of the interpreter, practically did not let it go (switching occurred only if the class A
flow managed to switch to rare output operations), while in the second case, the class A
function it was quickly executed and the program expected only the completion of the function of class B
At the same time, no one interfered with each other and the processes were executed in parallel on different processor cores.