From a2c3f22f1d582e6391cd8023180a8a2bd195079b Mon Sep 17 00:00:00 2001 From: Anthony Scemama Date: Sun, 21 Nov 2021 17:29:19 +0100 Subject: [PATCH] Threads OK --- parallelism_scemama.org | 163 +++++++++++++++++++++++++++++++++++++--- smp.png | Bin 0 -> 13598 bytes 2 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 smp.png diff --git a/parallelism_scemama.org b/parallelism_scemama.org index d02c16f..895666c 100644 --- a/parallelism_scemama.org +++ b/parallelism_scemama.org @@ -25,10 +25,9 @@ * Program NOEXPORT :noexport: - 14:00-14:30 : Supercomputers 14:30-15:30 : Parallelism -15:45-17:30 : OpenMP/MPI +15:45-17:30 : MPI / OpenMP 09:00-10:30 : Presentation IRP + QP Pour IRPF90, je peux faire une presentation assez generale. @@ -656,7 +655,7 @@ sys 0m3.172s - Processes interact only through system-provided communication mechanisms - Fork: creates a copy of the current process - Exec: switches to running another binary executable - - Spawn: =Fork=, then =exec= the child + - Spawn: =Fork=, =exec= the child and =wait= for its termination *** Thread - Exist as subsets of a process - Context switching between threads is fast @@ -783,9 +782,8 @@ def main(): # Read data from the child print("Reading from the child") s = r.read() - r.close() print("Read '%s' from the child"%(s)) - + r.close() ; os.wait() #+end_src #+LATEX: \end{column} #+LATEX: \begin{column}{0.4\textwidth} @@ -813,7 +811,6 @@ def main(): if __name__ == "__main__": main() - #+end_src #+LATEX: \end{column} #+LATEX: \begin{column}{0.4\textwidth} @@ -1172,8 +1169,7 @@ $ python pi_server_python.py & #+end_src - -* Message Passing Interface (MPI) +* Message Passing Interface (MPI) :noexport: ** Message Passing Interface @@ -1515,7 +1511,7 @@ if __name__ == "__main__": main() #+LATEX: \begin{columns} #+LATEX: \begin{column}{0.6\textwidth} #+begin_src text -$ mpiexec -n 8 python mpi_pi_v2.py 100000000 +$ mpiexec -n 4 python mpi_pi_v2.py 100000000 0 0.7853981783959749 Result = 3.1415926535901777 2 0.7853981583983196 @@ -1529,6 +1525,155 @@ Result = 3.1415926535901777 #+LATEX: \end{column} #+LATEX: \end{columns} +* Multi-threading +** Processes /vs/ threads + +*** Process + - Has its own memory address space + - Context switching between processes is slow + - Processes interact only through system-provided communication mechanisms + - Fork: creates a copy of the current process + - Exec: switches to running another binary executable + - Spawn: =Fork=, then =exec= the child +*** Thread + - Exist as subsets of a process + - Context switching between threads is fast + - Share the same memory address space : interact via shared memory + +** Threads + #+LATEX: \begin{columns} + #+LATEX: \begin{column}{0.5\textwidth} + #+ATTR_LATEX: :height 0.5\textheight + [[./smp.png]] + #+LATEX: \end{column} + #+LATEX: \begin{column}{0.5\textwidth} + - Concurrent programming + - Graphical user interfaces (progress bars, ...) + - Asynchronous I/O + - Standard library: POSIX threads (pthreads) + #+LATEX: \end{column} + #+LATEX: \end{columns} + +*** Communication time + - Low latency network latency : \sim 1.2 microsecond + - Random memory access : \sim 0.1 microsecond + +** Threads example (Python) + + #+begin_src python :tangle Codes/thread_python.py +#!/usr/bin/env python +import threading +import time + +class test: + def __init__(self, Nthreads): + self.Nthreads = Nthreads + self.data = [ i for i in range(Nthreads) ] + + def run_thread(self, j): + self.data[j] = 0 + time.sleep(j) + self.data[j] = j + #+end_src + +** Threads example (Python) + + #+begin_src python :tangle Codes/thread_python.py + def run(self): + thread = [ None ] * self.Nthreads + t0 = time.time() + print(self.data) + for i in range(self.Nthreads): + thread[i] = threading.Thread( target=self.run_thread, args=(i,) ) + thread[i].start() + for i in range(self.Nthreads): + thread[i].join() + print(time.time()-t0, "seconds. ", self.data) + +if __name__ == '__main__': + t = test(4) + t.run() + + #+end_src +------------------- + #+begin_src text +$ python thread_python.py +[0, 1, 2, 3] +0.0009775161743164062 seconds. [0, 0, 0, 0] +1.0018701553344727 seconds. [0, 1, 0, 0] +2.003377676010132 seconds. [0, 1, 2, 0] +3.004056930541992 seconds. [0, 1, 2, 3] + #+end_src + +** Computation of \pi with threads in Python + + #+begin_src python :tangle Codes/pi_thread_python.py +#!/usr/bin/env python +import os, sys, threading +from random import random, seed +from math import sqrt + +NMAX = 10000000 # Nb of MC steps/process +error_threshold = 1.0e-4 # Stopping criterion + +class pi_calculator: + def __init__(self, Nthreads): + self.Nthreads= Nthreads + self.results = [] + self.lock = threading.Lock() + + def compute_pi(self): + result = 0. + for i in range(NMAX): # Loop NMAX times + x,y = random(), random() # Draw 2 random numbers x and y + if x*x + y*y <= 1.: # Check if (x,y) is in the circle + result += 1 + with self.lock: + self.results.append(4.* float(result)/float(NMAX)) + #+end_src + +** Computation of \pi with threads in Python + + #+begin_src python :tangle Codes/pi_thread_python.py + def run(self): + thread = [None] * self.Nthreads + for i in range(self.Nthreads): + thread[i] = threading.Thread( target=self.compute_pi, args=() ) + thread[i].start() + print("All threads started") + + while True: + for i in range(self.Nthreads): + thread[i].join() + N = len(self.results) + average = sum(self.results)/N # Compute average + if N > 2: # Compute variance + l = [ (x-average)*(x-average) for x in self.results ] + variance = sum(l)/(N-1.) + else: + variance = 0. + error = sqrt(variance)/sqrt(N) # Compute error + print("%f +/- %f %d"%(average, error, N)) + #+end_src + +** Computation of \pi with threads in Python + + #+begin_src python :tangle Codes/pi_thread_python.py + if N > 2 and error < error_threshold: # Stopping condition + return + + for i in range(self.Nthreads): + thread[i] = threading.Thread( target=self.compute_pi, args=() ) + thread[i].start() + +if __name__ == '__main__': + calc = pi_calculator(4) + calc.run() + #+end_src + + Note: Inefficient in Python because of the Global Interpreter Lock + (GIL), but you got the idea. + * OpenMP * Exercises diff --git a/smp.png b/smp.png new file mode 100644 index 0000000000000000000000000000000000000000..a88f096c2645b2591914a58862840b780d95a9c0 GIT binary patch literal 13598 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 literal 0 HcmV?d00001