From a3bf91bc59daed1f5dec43e05431841f6564abb8 Mon Sep 17 00:00:00 2001 From: Luciano Ramalho Date: Fri, 29 Jan 2021 21:05:03 -0300 Subject: [PATCH] ch20 files --- 20-concurrency/primes/log-procs.txt | 260 +++++++++++ 20-concurrency/primes/procs.py | 45 +- 20-concurrency/primes/run_procs.sh | 2 + .../primes/spinner_prime_async_broken.py | 40 ++ .../primes/spinner_prime_async_nap.py | 59 +++ 20-concurrency/primes/spinner_prime_proc.py | 44 ++ 20-concurrency/primes/spinner_prime_thread.py | 43 ++ 20-concurrency/primes/stats-procs.ipynb | 437 ++++++++++++++++++ 20-concurrency/primes/threads.py | 43 +- 9 files changed, 945 insertions(+), 28 deletions(-) create mode 100644 20-concurrency/primes/log-procs.txt create mode 100755 20-concurrency/primes/run_procs.sh create mode 100644 20-concurrency/primes/spinner_prime_async_broken.py create mode 100644 20-concurrency/primes/spinner_prime_async_nap.py create mode 100644 20-concurrency/primes/spinner_prime_proc.py create mode 100644 20-concurrency/primes/spinner_prime_thread.py create mode 100644 20-concurrency/primes/stats-procs.ipynb diff --git a/20-concurrency/primes/log-procs.txt b/20-concurrency/primes/log-procs.txt new file mode 100644 index 0000000..0212f6a --- /dev/null +++ b/20-concurrency/primes/log-procs.txt @@ -0,0 +1,260 @@ +1 42.05 +2 22.81 +3 15.95 +4 13.28 +5 12.27 +6 10.38 +7 11.00 +8 11.41 +9 12.17 +10 12.69 +11 11.34 +12 11.35 +13 10.88 +14 11.63 +15 11.79 +16 11.49 +17 11.29 +18 10.78 +19 10.92 +20 11.07 +1 40.47 +2 22.93 +3 15.88 +4 13.25 +5 12.22 +6 10.95 +7 10.81 +8 11.26 +9 11.96 +10 12.43 +11 11.19 +12 10.91 +13 11.03 +14 10.56 +15 10.62 +16 11.73 +17 11.00 +18 12.81 +19 12.38 +20 11.28 +1 40.81 +2 22.79 +3 15.83 +4 13.27 +5 12.33 +6 10.65 +7 10.94 +8 11.48 +9 11.78 +10 12.78 +11 11.31 +12 10.97 +13 10.80 +14 10.93 +15 10.69 +16 10.57 +17 10.54 +18 10.55 +19 10.79 +20 10.64 +1 40.85 +2 22.67 +3 16.05 +4 13.21 +5 12.53 +6 10.84 +7 10.80 +8 11.31 +9 11.69 +10 12.51 +11 11.22 +12 11.35 +13 11.00 +14 10.64 +15 10.89 +16 10.49 +17 10.55 +18 10.76 +19 10.54 +20 10.75 +1 40.41 +2 22.75 +3 15.87 +4 13.19 +5 12.33 +6 10.50 +7 10.84 +8 11.55 +9 11.79 +10 12.53 +11 11.24 +12 11.13 +13 10.89 +14 10.52 +15 10.74 +16 10.68 +17 10.88 +18 10.61 +19 11.07 +20 10.71 +1 40.45 +2 22.73 +3 16.35 +4 13.09 +5 12.19 +6 10.39 +7 11.01 +8 11.30 +9 11.81 +10 12.24 +11 11.21 +12 11.13 +13 10.66 +14 10.56 +15 10.91 +16 10.49 +17 10.58 +18 10.61 +19 10.60 +20 10.69 +1 40.36 +2 22.64 +3 15.95 +4 13.20 +5 12.27 +6 10.34 +7 10.47 +8 11.34 +9 11.68 +10 12.30 +11 11.04 +12 10.85 +13 10.78 +14 10.64 +15 10.63 +16 10.58 +17 10.67 +18 10.64 +19 10.71 +20 10.68 +1 40.70 +2 22.71 +3 15.81 +4 13.10 +5 12.29 +6 10.34 +7 10.40 +8 11.40 +9 11.71 +10 12.56 +11 11.29 +12 10.99 +13 10.51 +14 10.69 +15 10.53 +16 11.04 +17 10.67 +18 10.71 +19 10.86 +20 10.78 +1 40.69 +2 22.80 +3 15.88 +4 13.13 +5 12.19 +6 10.36 +7 10.77 +8 11.32 +9 11.66 +10 12.29 +11 11.06 +12 10.89 +13 10.66 +14 10.61 +15 10.36 +16 10.57 +17 10.94 +18 10.57 +19 10.81 +20 10.72 +1 40.81 +2 22.76 +3 15.84 +4 13.10 +5 12.25 +6 10.33 +7 10.58 +8 11.51 +9 11.69 +10 12.45 +11 11.51 +12 11.53 +13 10.61 +14 10.52 +15 10.57 +16 10.57 +17 10.76 +18 10.60 +19 10.66 +20 10.73 +1 40.84 +2 22.83 +3 15.86 +4 13.27 +5 12.39 +6 10.45 +7 10.87 +8 11.42 +9 11.70 +10 12.55 +11 11.43 +12 10.98 +13 10.81 +14 10.69 +15 10.68 +16 10.71 +17 10.80 +18 10.76 +19 10.90 +20 11.02 +1 40.99 +2 22.99 +3 16.10 +4 13.46 +5 12.45 +6 10.47 +7 10.79 +8 11.74 +9 11.64 +10 12.37 +11 11.20 +12 11.09 +13 10.82 +14 10.61 +15 10.56 +16 10.53 +17 10.60 +18 10.81 +19 10.72 +20 10.62 +1 40.94 +2 23.09 +3 16.03 +4 13.40 +5 12.51 +6 10.38 +7 10.58 +8 11.37 +9 11.75 +10 12.87 +11 12.11 +12 11.37 +13 11.84 +14 11.30 +15 11.29 +16 11.36 +17 11.01 +18 11.37 +19 11.07 +20 10.94 \ No newline at end of file diff --git a/20-concurrency/primes/procs.py b/20-concurrency/primes/procs.py index 3238fda..27263d7 100644 --- a/20-concurrency/primes/procs.py +++ b/20-concurrency/primes/procs.py @@ -1,8 +1,9 @@ # tag::PRIMES_PROC_TOP[] from time import perf_counter -from typing import Tuple, List, NamedTuple -from multiprocessing import Process, SimpleQueue # <1> +from typing import Tuple, NamedTuple +from multiprocessing import Process, SimpleQueue, cpu_count # <1> from multiprocessing import queues # <2> +import sys from primes import is_prime, NUMBERS @@ -10,33 +11,47 @@ class Result(NamedTuple): # <3> flag: bool elapsed: float -JobQueue = queues.SimpleQueue[Tuple[int, Result]] # <4> +JobQueue = queues.SimpleQueue[int] # <4> +ResultQueue = queues.SimpleQueue[Tuple[int, Result]] # <5> -def check(n: int) -> Result: # <5> +def check(n: int) -> Result: # <6> t0 = perf_counter() res = is_prime(n) return Result(res, perf_counter() - t0) -def job(n: int, results: JobQueue) -> None: # <6> - results.put((n, check(n))) # <7> +def worker(jobs: JobQueue, results: ResultQueue) -> None: # <7> + while n := jobs.get(): # <8> + result = check(n) # <9> + results.put((n, result)) # <10> # end::PRIMES_PROC_TOP[] # tag::PRIMES_PROC_MAIN[] def main() -> None: + if len(sys.argv) < 2: # <1> + workers = cpu_count() + else: + workers = int(sys.argv[1]) + t0 = perf_counter() - results: JobQueue = SimpleQueue() # <1> - workers: List[Process] = [] # <2> + jobs: JobQueue = SimpleQueue() # <2> + results: ResultQueue = SimpleQueue() - for n in NUMBERS: - worker = Process(target=job, args=(n, results)) # <3> - worker.start() # <4> - workers.append(worker) # <5> + print(f'Checking {len(NUMBERS)} numbers with {workers} processes:') - for _ in workers: # <6> + for n in NUMBERS: # <3> + jobs.put(n) + + for _ in range(workers): + proc = Process(target=worker, args=(jobs, results)) # <4> + proc.start() # <5> + jobs.put(0) # <6> + + while True: n, (prime, elapsed) = results.get() # <7> label = 'P' if prime else ' ' - print(f'{n:16} {label} {elapsed:9.6f}s') - + print(f'{n:16} {label} {elapsed:9.6f}s') # <8> + if jobs.empty(): # <9> + break time = perf_counter() - t0 print('Total time:', f'{time:0.2f}s') diff --git a/20-concurrency/primes/run_procs.sh b/20-concurrency/primes/run_procs.sh new file mode 100755 index 0000000..9f57e47 --- /dev/null +++ b/20-concurrency/primes/run_procs.sh @@ -0,0 +1,2 @@ +#/bin/bash +for i in {1..20}; do echo -n $i; python3 procs.py $i | tail -1; done diff --git a/20-concurrency/primes/spinner_prime_async_broken.py b/20-concurrency/primes/spinner_prime_async_broken.py new file mode 100644 index 0000000..2290ece --- /dev/null +++ b/20-concurrency/primes/spinner_prime_async_broken.py @@ -0,0 +1,40 @@ +# spinner_async_experiment.py + +# credits: Example by Luciano Ramalho inspired by +# Michele Simionato's multiprocessing example in the python-list: +# https://mail.python.org/pipermail/python-list/2009-February/675659.html + +import asyncio +import itertools + +import primes + +async def spin(msg: str) -> None: + for char in itertools.cycle(r'\|/-'): + status = f'\r{char} {msg}' + print(status, flush=True, end='') + try: + await asyncio.sleep(.1) + except asyncio.CancelledError: + break + print('THIS WILL NEVER BE OUTPUT') + +async def check(n: int) -> int: + return primes.is_prime(n) # <4> + +async def supervisor(n: int) -> int: + spinner = asyncio.create_task(spin('thinking!')) # <1> + print('spinner object:', spinner) # <2> + result = await check(n) # <3> + spinner.cancel() # <5> + return result +# end::SPINNER_ASYNC_EXPERIMENT[] + +def main() -> None: + n = 5_000_111_000_222_021 + result = asyncio.run(supervisor(n)) + msg = 'is' if result else 'is not' + print(f'{n:,} {msg} prime') + +if __name__ == '__main__': + main() diff --git a/20-concurrency/primes/spinner_prime_async_nap.py b/20-concurrency/primes/spinner_prime_async_nap.py new file mode 100644 index 0000000..c1f82ad --- /dev/null +++ b/20-concurrency/primes/spinner_prime_async_nap.py @@ -0,0 +1,59 @@ +# spinner_async_experiment.py + +# credits: Example by Luciano Ramalho inspired by +# Michele Simionato's multiprocessing example in the python-list: +# https://mail.python.org/pipermail/python-list/2009-February/675659.html + +import asyncio +import itertools +import math + +# tag::SPINNER_ASYNC_NAP[] +async def is_prime(n): + if n < 2: + return False + if n == 2: + return True + if n % 2 == 0: + return False + + sleep = asyncio.sleep # <1> + root = int(math.floor(math.sqrt(n))) + for i in range(3, root + 1, 2): + if n % i == 0: + return False + if i % 100_000 == 1: # <2> + await sleep(0) + return True +# end::SPINNER_ASYNC_NAP[] + + +async def spin(msg: str) -> None: + for char in itertools.cycle(r'\|/-'): + status = f'\r{char} {msg}' + print(status, flush=True, end='') + try: + await asyncio.sleep(.1) + except asyncio.CancelledError: + break + blanks = ' ' * len(status) + print(f'\r{blanks}\r', end='') + +async def check(n: int) -> int: + return await is_prime(n) # <4> + +async def supervisor(n: int) -> int: + spinner = asyncio.create_task(spin('thinking!')) # <1> + print('spinner object:', spinner) # <2> + result = await check(n) # <3> + spinner.cancel() # <5> + return result + +def main() -> None: + n = 5_000_111_000_222_021 + result = asyncio.run(supervisor(n)) + msg = 'is' if result else 'is not' + print(f'{n:,} {msg} prime') + +if __name__ == '__main__': + main() diff --git a/20-concurrency/primes/spinner_prime_proc.py b/20-concurrency/primes/spinner_prime_proc.py new file mode 100644 index 0000000..e1454d1 --- /dev/null +++ b/20-concurrency/primes/spinner_prime_proc.py @@ -0,0 +1,44 @@ +# spinner_prime_proc.py + +# credits: Adapted from Michele Simionato's +# multiprocessing example in the python-list: +# https://mail.python.org/pipermail/python-list/2009-February/675659.html + +from multiprocessing import Process, Event +from multiprocessing import synchronize +import itertools +import time + +from primes import is_prime + +def spin(msg: str, done: synchronize.Event) -> None: # <1> + for char in itertools.cycle(r'\|/-'): # <2> + status = f'\r{char} {msg}' # <3> + print(status, end='', flush=True) + if done.wait(.1): # <4> + break # <5> + blanks = ' ' * len(status) + print(f'\r{blanks}\r', end='') # <6> + +def check(n: int) -> int: + return is_prime(n) + +def supervisor(n: int) -> int: # <1> + done = Event() # <2> + spinner = Process(target=spin, + args=('thinking!', done)) # <3> + print('spinner object:', spinner) # <4> + spinner.start() # <5> + result = check(n) # <6> + done.set() # <7> + spinner.join() # <8> + return result + +def main() -> None: + n = 5_000_111_000_222_021 + result = supervisor(n) # <9> + msg = 'is' if result else 'is not' + print(f'{n:,} {msg} prime') + +if __name__ == '__main__': + main() diff --git a/20-concurrency/primes/spinner_prime_thread.py b/20-concurrency/primes/spinner_prime_thread.py new file mode 100644 index 0000000..11db5d9 --- /dev/null +++ b/20-concurrency/primes/spinner_prime_thread.py @@ -0,0 +1,43 @@ +# spinner_prime_thread.py + +# credits: Adapted from Michele Simionato's +# multiprocessing example in the python-list: +# https://mail.python.org/pipermail/python-list/2009-February/675659.html + +from threading import Thread, Event +import itertools +import time + +from primes import is_prime + +def spin(msg: str, done: Event) -> None: # <1> + for char in itertools.cycle(r'\|/-'): # <2> + status = f'\r{char} {msg}' # <3> + print(status, end='', flush=True) + if done.wait(.1): # <4> + break # <5> + blanks = ' ' * len(status) + print(f'\r{blanks}\r', end='') # <6> + +def check(n: int) -> int: + return is_prime(n) + +def supervisor(n: int) -> int: # <1> + done = Event() # <2> + spinner = Thread(target=spin, + args=('thinking!', done)) # <3> + print('spinner object:', spinner) # <4> + spinner.start() # <5> + result = check(n) # <6> + done.set() # <7> + spinner.join() # <8> + return result + +def main() -> None: + n = 5_000_111_000_222_021 + result = supervisor(n) # <9> + msg = 'is' if result else 'is not' + print(f'{n:,} {msg} prime') + +if __name__ == '__main__': + main() diff --git a/20-concurrency/primes/stats-procs.ipynb b/20-concurrency/primes/stats-procs.ipynb new file mode 100644 index 0000000..ce8cae7 --- /dev/null +++ b/20-concurrency/primes/stats-procs.ipynb @@ -0,0 +1,437 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "([42.05,\n", + " 40.47,\n", + " 40.81,\n", + " 40.85,\n", + " 40.41,\n", + " 40.45,\n", + " 40.36,\n", + " 40.7,\n", + " 40.69,\n", + " 40.81,\n", + " 40.84,\n", + " 40.99,\n", + " 40.94],\n", + " [11.07,\n", + " 11.28,\n", + " 10.64,\n", + " 10.75,\n", + " 10.71,\n", + " 10.69,\n", + " 10.68,\n", + " 10.78,\n", + " 10.72,\n", + " 10.73,\n", + " 11.02,\n", + " 10.62,\n", + " 10.94])" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lines = \"\"\"1 42.05\n", + "2 22.81\n", + "3 15.95\n", + "4 13.28\n", + "5 12.27\n", + "6 10.38\n", + "7 11.00\n", + "8 11.41\n", + "9 12.17\n", + "10 12.69\n", + "11 11.34\n", + "12 11.35\n", + "13 10.88\n", + "14 11.63\n", + "15 11.79\n", + "16 11.49\n", + "17 11.29\n", + "18 10.78\n", + "19 10.92\n", + "20 11.07\n", + "1 40.47\n", + "2 22.93\n", + "3 15.88\n", + "4 13.25\n", + "5 12.22\n", + "6 10.95\n", + "7 10.81\n", + "8 11.26\n", + "9 11.96\n", + "10 12.43\n", + "11 11.19\n", + "12 10.91\n", + "13 11.03\n", + "14 10.56\n", + "15 10.62\n", + "16 11.73\n", + "17 11.00\n", + "18 12.81\n", + "19 12.38\n", + "20 11.28\n", + "1 40.81\n", + "2 22.79\n", + "3 15.83\n", + "4 13.27\n", + "5 12.33\n", + "6 10.65\n", + "7 10.94\n", + "8 11.48\n", + "9 11.78\n", + "10 12.78\n", + "11 11.31\n", + "12 10.97\n", + "13 10.80\n", + "14 10.93\n", + "15 10.69\n", + "16 10.57\n", + "17 10.54\n", + "18 10.55\n", + "19 10.79\n", + "20 10.64\n", + "1 40.85\n", + "2 22.67\n", + "3 16.05\n", + "4 13.21\n", + "5 12.53\n", + "6 10.84\n", + "7 10.80\n", + "8 11.31\n", + "9 11.69\n", + "10 12.51\n", + "11 11.22\n", + "12 11.35\n", + "13 11.00\n", + "14 10.64\n", + "15 10.89\n", + "16 10.49\n", + "17 10.55\n", + "18 10.76\n", + "19 10.54\n", + "20 10.75\n", + "1 40.41\n", + "2 22.75\n", + "3 15.87\n", + "4 13.19\n", + "5 12.33\n", + "6 10.50\n", + "7 10.84\n", + "8 11.55\n", + "9 11.79\n", + "10 12.53\n", + "11 11.24\n", + "12 11.13\n", + "13 10.89\n", + "14 10.52\n", + "15 10.74\n", + "16 10.68\n", + "17 10.88\n", + "18 10.61\n", + "19 11.07\n", + "20 10.71\n", + "1 40.45\n", + "2 22.73\n", + "3 16.35\n", + "4 13.09\n", + "5 12.19\n", + "6 10.39\n", + "7 11.01\n", + "8 11.30\n", + "9 11.81\n", + "10 12.24\n", + "11 11.21\n", + "12 11.13\n", + "13 10.66\n", + "14 10.56\n", + "15 10.91\n", + "16 10.49\n", + "17 10.58\n", + "18 10.61\n", + "19 10.60\n", + "20 10.69\n", + "1 40.36\n", + "2 22.64\n", + "3 15.95\n", + "4 13.20\n", + "5 12.27\n", + "6 10.34\n", + "7 10.47\n", + "8 11.34\n", + "9 11.68\n", + "10 12.30\n", + "11 11.04\n", + "12 10.85\n", + "13 10.78\n", + "14 10.64\n", + "15 10.63\n", + "16 10.58\n", + "17 10.67\n", + "18 10.64\n", + "19 10.71\n", + "20 10.68\n", + "1 40.70\n", + "2 22.71\n", + "3 15.81\n", + "4 13.10\n", + "5 12.29\n", + "6 10.34\n", + "7 10.40\n", + "8 11.40\n", + "9 11.71\n", + "10 12.56\n", + "11 11.29\n", + "12 10.99\n", + "13 10.51\n", + "14 10.69\n", + "15 10.53\n", + "16 11.04\n", + "17 10.67\n", + "18 10.71\n", + "19 10.86\n", + "20 10.78\n", + "1 40.69\n", + "2 22.80\n", + "3 15.88\n", + "4 13.13\n", + "5 12.19\n", + "6 10.36\n", + "7 10.77\n", + "8 11.32\n", + "9 11.66\n", + "10 12.29\n", + "11 11.06\n", + "12 10.89\n", + "13 10.66\n", + "14 10.61\n", + "15 10.36\n", + "16 10.57\n", + "17 10.94\n", + "18 10.57\n", + "19 10.81\n", + "20 10.72\n", + "1 40.81\n", + "2 22.76\n", + "3 15.84\n", + "4 13.10\n", + "5 12.25\n", + "6 10.33\n", + "7 10.58\n", + "8 11.51\n", + "9 11.69\n", + "10 12.45\n", + "11 11.51\n", + "12 11.53\n", + "13 10.61\n", + "14 10.52\n", + "15 10.57\n", + "16 10.57\n", + "17 10.76\n", + "18 10.60\n", + "19 10.66\n", + "20 10.73\n", + "1 40.84\n", + "2 22.83\n", + "3 15.86\n", + "4 13.27\n", + "5 12.39\n", + "6 10.45\n", + "7 10.87\n", + "8 11.42\n", + "9 11.70\n", + "10 12.55\n", + "11 11.43\n", + "12 10.98\n", + "13 10.81\n", + "14 10.69\n", + "15 10.68\n", + "16 10.71\n", + "17 10.80\n", + "18 10.76\n", + "19 10.90\n", + "20 11.02\n", + "1 40.99\n", + "2 22.99\n", + "3 16.10\n", + "4 13.46\n", + "5 12.45\n", + "6 10.47\n", + "7 10.79\n", + "8 11.74\n", + "9 11.64\n", + "10 12.37\n", + "11 11.20\n", + "12 11.09\n", + "13 10.82\n", + "14 10.61\n", + "15 10.56\n", + "16 10.53\n", + "17 10.60\n", + "18 10.81\n", + "19 10.72\n", + "20 10.62\n", + "1 40.94\n", + "2 23.09\n", + "3 16.03\n", + "4 13.40\n", + "5 12.51\n", + "6 10.38\n", + "7 10.58\n", + "8 11.37\n", + "9 11.75\n", + "10 12.87\n", + "11 12.11\n", + "12 11.37\n", + "13 11.84\n", + "14 11.30\n", + "15 11.29\n", + "16 11.36\n", + "17 11.01\n", + "18 11.37\n", + "19 11.07\n", + "20 10.94\"\"\"\n", + "\n", + "from collections import defaultdict\n", + "\n", + "times = defaultdict(list)\n", + "for line in lines.split('\\n'):\n", + " n, s = line.strip().split()\n", + " n, s = int(n), float(s)\n", + " times[n].append(s)\n", + "\n", + "times[1], times[20]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "10.39" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from statistics import median, mean\n", + "\n", + "median_times = []\n", + "mean_times = []\n", + "for procs, ts in times.items():\n", + " median_times.append(median(ts))\n", + " mean_times.append(mean(ts))\n", + " \n", + "min(median_times)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "df=pd.DataFrame(\n", + " {'procs': range(1,21),\n", + " 'median': median_times})" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'processes')" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot('procs', 'median', data=df, marker='+')\n", + "plt.ylim([0, 50])\n", + "plt.ylabel('time (s)')\n", + "plt.xlabel('processes')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Dúvidas\n", + "\n", + "1. O eixo x são valores discretos: a quantidade de processos em uso. Como faço para que o eixo x seja marcado com os números de 1 a 20, um por um (20 ticks)? \n", + "2. Como alargar o retângulo do gráfico uns 50%, para que os números no eixo x não fiquem muito apertados?\n", + "3. Como exibir uma linha horizontal na altura do valor mínimo, que é 10.39?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/20-concurrency/primes/threads.py b/20-concurrency/primes/threads.py index 8e9a6b9..7ac275a 100644 --- a/20-concurrency/primes/threads.py +++ b/20-concurrency/primes/threads.py @@ -1,38 +1,55 @@ from time import perf_counter -from typing import Tuple, List, NamedTuple +from typing import Tuple, NamedTuple from threading import Thread from queue import SimpleQueue +import sys +import os from primes import is_prime, NUMBERS -class Result(NamedTuple): # <3> +class Result(NamedTuple): flag: bool elapsed: float -JobQueue = SimpleQueue[Tuple[int, Result]] # <4> +JobQueue = SimpleQueue[int] +ResultQueue = SimpleQueue[Tuple[int, Result]] -def check(n: int) -> Result: # <5> +def check(n: int) -> Result: t0 = perf_counter() res = is_prime(n) return Result(res, perf_counter() - t0) -def job(n: int, results: JobQueue) -> None: # <6> - results.put((n, check(n))) # <7> +def worker(jobs: JobQueue, results: ResultQueue) -> None: + while n := jobs.get(): + result = check(n) + results.put((n, result)) def main() -> None: + if len(sys.argv) < 2: # <1> + workers = os.cpu_count() or 1 # make mypy happy + else: + workers = int(sys.argv[1]) + t0 = perf_counter() - results: JobQueue = SimpleQueue() # <1> - workers: List[Thread] = [] # <2> + jobs: JobQueue = SimpleQueue() # <2> + results: ResultQueue = SimpleQueue() - for n in NUMBERS: - worker = Thread(target=job, args=(n, results)) # <3> - worker.start() # <4> - workers.append(worker) # <5> + print(f'Checking {len(NUMBERS)} numbers with {workers} threads:') - for _ in workers: # <6> + for n in NUMBERS: # <3> + jobs.put(n) + + for _ in range(workers): + proc = Thread(target=worker, args=(jobs, results)) # <4> + proc.start() # <5> + jobs.put(0) # <6> + + while True: n, (prime, elapsed) = results.get() # <7> label = 'P' if prime else ' ' print(f'{n:16} {label} {elapsed:9.6f}s') + if jobs.empty(): # <8> + break time = perf_counter() - t0 print('Total time:', f'{time:0.2f}s')