├── .gitignore ├── README.md ├── Makefile ├── libs ├── thread.h └── queue.h ├── 02_is_prime_parallel_by_composition.cpp ├── 01_is_prime_sequential.cpp ├── 13_fixme.cpp ├── 06_sumatoria_with_locks_raii.cpp ├── 05_sumatoria_with_mutex.cpp ├── 07_sumatoria_with_monitor.cpp ├── 04_sumatoria_with_race_conditions.cpp ├── 08_monitor_interface_critical_section.cpp ├── 10_blocking_queue_with_busy_wait_and_polling.cpp ├── 03_is_prime_parallel_by_inheritance.cpp ├── 09_non_blocking_queue.cpp ├── tests └── queue.cpp ├── 12_how_to_close_a_queue.cpp └── 11_blocking_queue_with_conditional_variables.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | test_queue 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tutoriales interactivos de programación multithreading 2 | 3 | En este *hands-on* veras como tener programas multithreading, 4 | los problemas que trae (race conditions, deadlocks) y las herramientas 5 | para resolverlos (mutexes, locks, conditional variables y queues) 6 | 7 | Como bonus veras functors y caso de aplicación de RAII en C++ 8 | 9 | Lee el código fuente de los archivos en orden, 10 | comenzando por `01_is_prime_sequential.cpp` 11 | 12 | En cada tutorial encontraras comentarios para guiarte en los ejercicios. 13 | Debes seguirlos en orden comenzando por `[1]`, `[2]`, ... 14 | 15 | Este es un tutorial *interactivo* y depende de vos hacerlo; contiene 16 | información muy *valiosa* para entender como los threads funcionan 17 | así como también los principales errores y técnicas para solucionarlos. 18 | 19 | Otras primitivas de sincronización (read-write locks, semáforos, 20 | reentrant locks) no están incluidas en este *hands-on* así como tampoco 21 | performance (cache invalidation, false sharing) ni problemas clásicos 22 | de concurrencia ni queues más avanzadas. 23 | 24 | Eso quedaran para *hands-on*s futuros. 25 | 26 | ## Como compilar / correr los tests? 27 | 28 | Solo tenes que correr `make` 29 | 30 | ## Licencia 31 | 32 | GPL v2 33 | 34 | ## Puedo usar este código en el Trabajo Práctico? 35 | 36 | Si, pero tenes que decir de donde lo sacaste y respetar la licencia. 37 | 38 | En `libs/` tendrás la clase `Thread` y la clase `Queue` para que 39 | uses. 40 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: chklibs f1.1 f2.1 f3.1 f4.1 f5.1 f6.1 f7.1 f8.1 f9.1 f10.1 f11.1 f12.1 f13.1 2 | 3 | clean: 4 | rm -Rf *.o *.a *.so *.exe a.out test_queue 5 | 6 | chklibs: 7 | g++ -std=c++17 -pedantic -Wall -ggdb -o test_queue tests/queue.cpp 8 | cppcheck --enable=all --language=c++ --std=c++17 --error-exitcode=1 --suppress=unmatchedSuppression --suppress=duplInheritedMember --suppress=missingIncludeSystem --suppress=unusedFunction --inline-suppr libs/*.h libs/*.cpp 9 | ./test_queue 10 | 11 | f1.1: 12 | g++ -std=c++17 -pedantic -Wall -ggdb -o 01_is_prime_sequential.exe 01_is_prime_sequential.cpp 13 | 14 | f2.1: 15 | g++ -std=c++17 -pedantic -Wall -ggdb -o 02_is_prime_parallel_by_composition.exe 02_is_prime_parallel_by_composition.cpp -pthread 16 | 17 | f3.1: 18 | g++ -std=c++17 -pedantic -Wall -ggdb -o 03_is_prime_parallel_by_inheritance.exe 03_is_prime_parallel_by_inheritance.cpp -pthread 19 | 20 | f4.1: 21 | g++ -std=c++17 -pedantic -Wall -ggdb -o 04_sumatoria_with_race_conditions.exe 04_sumatoria_with_race_conditions.cpp -pthread 22 | 23 | f5.1: 24 | g++ -std=c++17 -pedantic -Wall -ggdb -o 05_sumatoria_with_mutex.exe 05_sumatoria_with_mutex.cpp -pthread 25 | 26 | f6.1: 27 | g++ -std=c++17 -pedantic -Wall -ggdb -o 06_sumatoria_with_locks_raii.exe 06_sumatoria_with_locks_raii.cpp -pthread 28 | 29 | f7.1: 30 | g++ -std=c++17 -pedantic -Wall -ggdb -o 07_sumatoria_with_monitor.exe 07_sumatoria_with_monitor.cpp -pthread 31 | 32 | f8.1: 33 | g++ -std=c++17 -pedantic -Wall -ggdb -o 08_monitor_interface_critical_section.exe 08_monitor_interface_critical_section.cpp -pthread 34 | 35 | f9.1: 36 | g++ -std=c++17 -pedantic -Wall -ggdb -o 09_non_blocking_queue.exe 09_non_blocking_queue.cpp -pthread 37 | 38 | f10.1: 39 | g++ -std=c++17 -pedantic -Wall -ggdb -o 10_blocking_queue_with_busy_wait_and_polling.exe 10_blocking_queue_with_busy_wait_and_polling.cpp -pthread 40 | 41 | f11.1: 42 | g++ -std=c++17 -pedantic -Wall -ggdb -o 11_blocking_queue_with_conditional_variables.exe 11_blocking_queue_with_conditional_variables.cpp -pthread 43 | 44 | f12.1: 45 | g++ -std=c++17 -pedantic -Wall -ggdb -o 12_how_to_close_a_queue.exe 12_how_to_close_a_queue.cpp -pthread 46 | 47 | f13.1: 48 | g++ -std=c++17 -pedantic -Wall -ggdb -o 13_fixme.exe 13_fixme.cpp -pthread 49 | 50 | -------------------------------------------------------------------------------- /libs/thread.h: -------------------------------------------------------------------------------- 1 | #ifndef THREAD_H_ 2 | #define THREAD_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class Runnable { 9 | public: 10 | virtual void start() = 0; 11 | virtual void join() = 0; 12 | virtual void stop() = 0; 13 | virtual bool is_alive() const = 0; 14 | 15 | virtual ~Runnable() {} 16 | }; 17 | 18 | class Thread : public Runnable { 19 | private: 20 | std::thread thread; 21 | 22 | // Subclasses that inherit from Thread will have access to these 23 | // flags, mostly to control how Thread::run() will behave 24 | std::atomic _keep_running; 25 | std::atomic _is_alive; 26 | 27 | protected: 28 | bool should_keep_running() const { 29 | return _keep_running; 30 | } 31 | 32 | public: 33 | Thread () : _keep_running(true), _is_alive(false) {} 34 | 35 | void start() override { 36 | _is_alive = true; 37 | _keep_running = true; 38 | thread = std::thread(&Thread::main, this); 39 | } 40 | 41 | void join() override { 42 | thread.join(); 43 | } 44 | 45 | void main() { 46 | try { 47 | this->run(); 48 | } catch(const std::exception &err) { 49 | std::cerr << "Unexpected exception: " << err.what() << "\n"; 50 | } catch(...) { 51 | std::cerr << "Unexpected exception: \n"; 52 | } 53 | 54 | _is_alive = false; 55 | } 56 | 57 | // Note: it is up to the subclass to make something meaningful to 58 | // really stop the thread. The Thread::run() may be blocked and/or 59 | // it may not read _keep_running. 60 | void stop() override { 61 | _keep_running = false; 62 | } 63 | 64 | // Note: asking for is_alive is well defined *only if* the thread 65 | // was started (you called Thread::start()) 66 | bool is_alive() const override { 67 | return _is_alive; 68 | } 69 | 70 | virtual void run() = 0; 71 | virtual ~Thread() {} 72 | 73 | Thread(const Thread&) = delete; 74 | Thread& operator=(const Thread&) = delete; 75 | 76 | Thread(Thread&& other) = delete; 77 | Thread& operator=(Thread&& other) = delete; 78 | }; 79 | 80 | #endif 81 | -------------------------------------------------------------------------------- /02_is_prime_parallel_by_composition.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | [1] 3 | Ejemplo de como ejecutar una función/functor en 4 | un hilo separado en C++ 5 | 6 | Se ejecutan varios functors en paralelo en donde 7 | el objeto thread tiene una referencia al objeto 8 | functor (usamos composición) 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #define N 10 16 | 17 | class IsPrime { 18 | private: 19 | unsigned int n; 20 | bool &result; 21 | 22 | public: 23 | IsPrime(unsigned int n, bool &result) : 24 | n(n), 25 | result(result) {} 26 | 27 | void operator()() { 28 | for (unsigned int i = 2; i < n; ++i) { 29 | if (n % i == 0) { 30 | result = false; 31 | return; 32 | } 33 | } 34 | 35 | result = true; 36 | } 37 | }; 38 | 39 | 40 | 41 | int main() { 42 | unsigned int nums[N] = { 0, 1, 2, 132130891, 43 | 132130891, 4, 13, 44 | 132130891, 132130891, 45 | 132130871 }; 46 | bool results[N]; 47 | 48 | // [4] 49 | // Notar q estamos usando un *vector* de std::thread 50 | // 51 | // Esto esta bien por que los functors IsPrime no 52 | // tienen punteros a si mismo (this) y por ende 53 | // no seran afectados cuando el vector se resizee 54 | // y se mueva. 55 | // 56 | // No lo ves? No pasa nada!! En el proximo ejemplo 57 | // lo vas a ver mejor. 58 | // 59 | // Toma nota! 60 | std::vector threads; 61 | 62 | for (int i = 0; i < N; ++i) { 63 | /* [2] Acá es donde usamos composición donde 64 | "std::thread tiene un IsPrime" 65 | 66 | El operator call del functor se ejecutara 67 | en el hilo lanzado por el constructor del 68 | objeto std::thread 69 | 70 | std::thread recibe una **función** pero 71 | en la practica, como las funciones no tienen 72 | estados, es mucho más **útil** pasarle 73 | un **functor** 74 | 75 | Es por esta razón que tuvimos que 76 | sobrecargar el operador call de nuestro functor: 77 | para que std::thread sepa que correr. 78 | */ 79 | threads.push_back(std::thread { 80 | IsPrime(nums[i], 81 | results[i]) 82 | }); 83 | } 84 | 85 | /* ************************************** */ 86 | /* Ahora: Todos los hilos están corriendo */ 87 | /* ************************************** */ 88 | 89 | /* [3] Esperamos a que cada hilo termine. 90 | Cada join bloqueara al hilo llamante (main) 91 | hasta que el hilo sobre el cual se le hace 92 | join (threads[i]) termine 93 | 94 | Siempre es necesario hacer un join para 95 | liberar los recursos. No hacer un join 96 | implica leaks (solo en muy exóticos y 97 | más que justificados casos se puede 98 | prescindir de un join). 99 | */ 100 | for (int i = 0; i < N; ++i) { 101 | threads[i].join(); 102 | } 103 | 104 | /* ********************************** */ 105 | /* Ahora: Todos los hilos terminaron */ 106 | /* ********************************** */ 107 | 108 | for (int i = 0; i < N; ++i) { 109 | std::cout << results[i] << " "; 110 | } 111 | std::cout << "\n"; 112 | 113 | return 0; 114 | } 115 | 116 | /* [5] 117 | Corre el ejecutable con "time": 118 | time ./02_is_prime_parallel_by_composition.exe 119 | 120 | Compara los tiempos con la ejecución de 121 | 01_is_prime_sequential.exe 122 | 123 | Mejoro el tiempo "real"? y el "user"? 124 | 125 | Has llegado al final del ejercicio, continua 126 | con el siguiente. 127 | */ 128 | 129 | -------------------------------------------------------------------------------- /01_is_prime_sequential.cpp: -------------------------------------------------------------------------------- 1 | /* [1] 2 | 3 | Antes de arrancar con threads vamos a ver el concepto de 4 | "functor": la encapsulación de una 5 | función o algoritmo en un objeto. 6 | 7 | Para darle una sintaxis copada (y para que sea 8 | compatible con la librería estándar de C++) 9 | vamos a sobrecargarle el operador call. 10 | */ 11 | 12 | 13 | #include 14 | #define N 10 15 | 16 | /* [2] Functor: una función hecha objeto */ 17 | class IsPrime { 18 | private: 19 | unsigned int n; 20 | bool &result; 21 | 22 | public: 23 | /* [3] Un functor permite desacoplar el 24 | pasaje de los parámentros de la 25 | ejecución de la función/algoritmo. 26 | 27 | En este caso, el functor recibe 2 parámetros: 28 | - n, el número a determinar si es o no primo 29 | - result, donde guardar el resultado 30 | */ 31 | IsPrime(unsigned int n, bool &result) : 32 | n(n), 33 | result(result) {} 34 | 35 | /* [4] El algoritmo para saber si un número 36 | es primo o no (version simplificada) 37 | 38 | Nótese como el algoritmo **no** recibe ningún 39 | parámetro explicito sino que estos fueron 40 | pasados por el constructor. 41 | */ 42 | void run() { 43 | for (unsigned int i = 2; i < n; ++i) { 44 | if (n % i == 0) { 45 | result = false; 46 | return; 47 | } 48 | } 49 | 50 | result = true; 51 | } 52 | 53 | /* [5] 54 | Sobrecarga del operator call. Esto 55 | permite llamar a IsPrime con la misma 56 | sintaxis que se invoca a una función. 57 | 58 | Ej: 59 | 60 | IsPrime f(n, r); // <- instancio el objeto 61 | 62 | f(); // <- lo llamo como si 63 | // fuera una función 64 | 65 | Aunque pueda parecer solo syntax sugar, 66 | la librería estándar de C++ espera esta 67 | sintaxis en algunos casos. 68 | */ 69 | void operator()() { 70 | this->run(); /* [6] 71 | podríamos haber puesto 72 | el código de 73 | IsPrime::run aquí directamente 74 | */ 75 | } 76 | 77 | }; 78 | 79 | 80 | 81 | int main() { 82 | unsigned int nums[N] = { 0, 1, 2, 132130891, 83 | 132130891, 4, 13, 84 | 132130891, 132130891, 85 | 132130871 }; 86 | bool results[N]; 87 | 88 | for (int i = 0; i < N; ++i) { 89 | /* [7] 90 | Creamos un functor (function object) 91 | con los argumentos de la función pero 92 | esta no se invoca aquí 93 | */ 94 | IsPrime is_prime = IsPrime(nums[i], 95 | results[i]); 96 | 97 | /* [8] 98 | Recién aquí se invoca a la función 99 | "is prime". 100 | 101 | Los functors permiten retrasar las 102 | llamadas a funciones: el pasaje de 103 | argumentos se desacopla de la invocación 104 | del algoritmo. 105 | 106 | En este caso le pasamos todos los parámetros 107 | al constructor y ninguno a la llamada. 108 | En otros casos tal vez quieras pasar algunos 109 | en el constructor y otros en la llamada. 110 | */ 111 | is_prime(); // <- equivale a is_prime.run(); 112 | } 113 | 114 | 115 | for (int i = 0; i < N; ++i) { 116 | std::cout << results[i] << " "; 117 | } 118 | std::cout << "\n"; 119 | 120 | return 0; 121 | } 122 | 123 | /* [9] 124 | Corre el ejecutable con "time": 125 | time ./01_is_prime_sequential.exe 126 | 127 | Que significan esas mediciones?: 128 | real 129 | user 130 | sys 131 | 132 | Lee la página de manual con: 133 | man time 134 | 135 | Functors es un tópico ligeramente exótico presente 136 | en lenguajes como C++ y Java q **no** ven a las funciones/métodos 137 | como objetos puros. 138 | 139 | El functor encapsula dicha función/método. 140 | 141 | Lenguajes como Python ven a las función/métodos como objetos directamente 142 | y el concepto de functor es menos explicito. 143 | 144 | Has llegado al final del ejercicio, continua 145 | con el siguiente. 146 | */ 147 | 148 | 149 | -------------------------------------------------------------------------------- /13_fixme.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Ejercicio final! 3 | * 4 | * Se tienen N alumnos (clase Student) que son objetos activos (Thread). 5 | * 6 | * Los alumnos tienen acceso a una lista de asistencia (clase Attendance) 7 | * en la que deben agregarse si no esta y marcar que están presentes. 8 | * 9 | * La clase Attendance ya tiene los métodos implementados 10 | * (que **no** podes modificar) y la clase Student tienen la lógica. 11 | * 12 | * Dado que Attendance esta **compartida** hay una **race condition** 13 | * y tu objetivo sera arreglar el código. 14 | * 15 | * Tenes 2 opciones: 16 | * 17 | * - Podes implementar un Monitor haciendo uso de mutexes y locks para 18 | * proteger al objeto Attendance. 19 | * Tendrás que descubrir cual/cuales son las **critical sections** 20 | * y proteger el acceso. (Nota: la clase Attendance **no** la podes 21 | * modificar, por lo que tendras q crear un AttendanceProtected 22 | * y modificar a los Student). 23 | * 24 | * - La otra opción es **no** compartir la lista de asistencia y 25 | * en cambio hacer que todos los alumnos (Student) compartan 26 | * una **única** blocking queue. 27 | * El main() pusheara a la queue la lista y cada alumno hara 28 | * un pop(), hara lo que tenga que hacer con la lista y 29 | * la devolverá haciendo un push(). 30 | * Como veras, en ningún momento la lista de asistencia es 31 | * compartida "simultáneamente" por los threads. 32 | * 33 | * Ambas opciones son válidas y equivalentes: ninguna es mejor 34 | * q la otra (aunque creo que la de Monitor "para este caso" tiene 35 | * algunos puntos técnicos a favor, pero es debatible). 36 | * 37 | * 38 | * Good luck!! 39 | * 40 | * */ 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | 48 | // Misma clase Thread que en 03_is_prime_parallel_by_inheritance.cpp 49 | #include "libs/thread.h" 50 | 51 | // Misma clase Queue que en 12_how_to_close_a_queue.cpp 52 | // excepto que es una clase template 53 | #include "libs/queue.h" 54 | 55 | namespace { 56 | const int STUDENTS_CNT = 100; 57 | } 58 | 59 | // NO MODIFICAR (no hagas trampa) 60 | class Attendance { 61 | private: 62 | std::map list; 63 | 64 | public: 65 | bool is_student_in_list(int student_id) { 66 | return list.count(student_id) == 1; 67 | } 68 | 69 | void add_student_to_list(int student_id) { 70 | if (is_student_in_list(student_id)) { 71 | throw std::runtime_error("You cannot add the same student twice"); 72 | } 73 | 74 | list[student_id] = false; 75 | } 76 | 77 | void mark_attendance_of_student(int student_id) { 78 | if (not is_student_in_list(student_id)) { 79 | throw std::runtime_error("The student must be added to the list first"); 80 | } 81 | 82 | list[student_id] = true; 83 | } 84 | 85 | void print(std::ostream& out) { 86 | out << "There are " << list.size() << " students in the list\n"; 87 | 88 | int present_cnt = std::count_if(list.begin(), list.end(), [](auto const& pair) { 89 | return pair.second == true; 90 | }); 91 | 92 | out << "There are " << present_cnt << " present students and "; 93 | out << list.size() - present_cnt << " absent students\n"; 94 | } 95 | }; 96 | 97 | void sleep_a_little(std::default_random_engine& generator) { 98 | std::uniform_int_distribution get_random_int(100, 500); 99 | 100 | auto random_int = get_random_int(generator); 101 | auto milliseconds_to_sleep = std::chrono::milliseconds(random_int); 102 | std::this_thread::sleep_for(milliseconds_to_sleep); // sleep some "pseudo-random" time 103 | } 104 | 105 | class Student : public Thread { 106 | private: 107 | int id; 108 | Attendance& list; 109 | 110 | public: 111 | explicit Student(int id, Attendance& list) : id(id), list(list) {} 112 | 113 | virtual void run() override { 114 | std::default_random_engine generator; 115 | 116 | // Simulamos algo de tiempo para q los alumnos quieran 117 | // firma a la vez pero con algo de randomness 118 | sleep_a_little(generator); 119 | 120 | if (not list.is_student_in_list(id)) { 121 | list.add_student_to_list(id); 122 | } 123 | 124 | list.mark_attendance_of_student(id); 125 | 126 | sleep_a_little(generator); 127 | } 128 | }; 129 | 130 | 131 | int main(int argc, char *argv[]) { 132 | Attendance list; 133 | 134 | std::vector students(STUDENTS_CNT); 135 | 136 | for (int i = 0; i < STUDENTS_CNT; ++i) { 137 | students[i] = new Student(i, list); 138 | students[i]->start(); 139 | } 140 | 141 | for (int i = 0; i < STUDENTS_CNT; ++i) { 142 | students[i]->join(); 143 | delete students[i]; 144 | } 145 | 146 | list.print(std::cout); 147 | 148 | return 0; 149 | } 150 | -------------------------------------------------------------------------------- /06_sumatoria_with_locks_raii.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | [1] Ejemplo de RAII encapsulando la toma y 3 | liberación de un mutex: clase Lock 4 | 5 | El ejemplo debería imprimir por pantalla el 6 | número 479340. 7 | for i in {0..1000} 8 | do 9 | ./06_sumatoria_with_locks_raii.exe 10 | done | uniq 11 | 12 | */ 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #define N 10 21 | #define ROUNDS 1 22 | 23 | // Misma clase Thread que en 03_is_prime_parallel_by_inheritance.cpp 24 | #include "libs/thread.h" 25 | 26 | /* [2] Encapsulación RAII del recurso 27 | "mutex tomado" 28 | 29 | Como pueden ver, la memoria no es 30 | el único recurso que hay que liberar. 31 | 32 | C++11 ya ofrece el mismo objeto std::lock_guard 33 | pero mostramos esta implementación para 34 | que quede como ejemplo de como RAII puede 35 | servirnos para crear construcciones de alto 36 | nivel. 37 | */ 38 | class Lock { 39 | private: 40 | std::mutex &m; 41 | 42 | public: 43 | /* [3] En el constructor adquirimos el 44 | recurso: 45 | lockeamos el mutex 46 | */ 47 | Lock(std::mutex &m) : m(m) { 48 | m.lock(); 49 | } 50 | 51 | /* [4] En el destructor liberamos el 52 | recurso: 53 | deslockeamos el mutex 54 | */ 55 | ~Lock() { 56 | m.unlock(); 57 | } 58 | 59 | /* [5] No tiene sentido copiar locks, 60 | forzar a que no se pueda. 61 | y tampoco tiene mucho sentido moverlos 62 | (aunque es cuestionable) 63 | */ 64 | Lock(const Lock&) = delete; 65 | Lock& operator=(const Lock&) = delete; 66 | Lock(Lock&&) = delete; 67 | Lock& operator=(Lock&&) = delete; 68 | 69 | }; 70 | 71 | class Sum: public Thread { 72 | private: 73 | unsigned int *start; 74 | unsigned int *end; 75 | 76 | unsigned int &result; 77 | std::mutex &m; 78 | 79 | public: 80 | Sum(unsigned int *start, 81 | unsigned int *end, 82 | unsigned int &result, 83 | std::mutex &m) : 84 | start(start), end(end), 85 | result(result), m(m) {} 86 | 87 | virtual void run() override { 88 | unsigned int temporal_sum; 89 | for (int round = 0; round < ROUNDS; ++round) { 90 | temporal_sum = 0; 91 | for (unsigned int *p = start; p < end; ++p) { 92 | temporal_sum += *p; 93 | } 94 | } 95 | 96 | Lock l(m); // -+- 97 | result += temporal_sum; // | esta es 98 | // | la CS 99 | // | 100 | } // ---------------------------+- 101 | /* [6] el mutex es liberado aquí cuando 102 | la variable "l" es destruida por irse 103 | de scope. 104 | Liberación del mutex automática!! 105 | */ 106 | }; 107 | 108 | int main() { 109 | unsigned int nums[N] = { 132131, 1321, 31371, 110 | 30891, 891, 123891, 111 | 3171, 30891, 891, 112 | 123891 }; 113 | unsigned int result = 0; 114 | std::mutex m; 115 | std::vector threads; 116 | 117 | for (int i = 0; i < N/2; ++i) { 118 | threads.push_back(new Sum( 119 | &nums[i*2], 120 | &nums[(i+1)*2], 121 | result, 122 | m 123 | )); 124 | } 125 | 126 | for (int i = 0; i < N/2; ++i) { 127 | threads[i]->start(); 128 | } 129 | 130 | for (int i = 0; i < N/2; ++i) { 131 | threads[i]->join(); 132 | delete threads[i]; 133 | } 134 | 135 | std::cout << result; // 479340 136 | std::cout << "\n"; 137 | 138 | return 0; 139 | } 140 | /* [7] 141 | 142 | Challenge: lanza una excepción en el método Sum::run. 143 | En un experimento lánzala *antes* de tomar el lock, en otra *después* 144 | de tomar el lock. 145 | 146 | Obviamente que la suma total ya no sera correcta pero, el programa 147 | se te colgó o no? 148 | 149 | Hace el mismo experimento pero con el código en 05_sumatoria_with_mutex.cpp: 150 | proba q pasa si se lanza una excepción *antes* de tomar el lock, 151 | *luego* de tomar el lock (pero *antes* de liberarlo) y que pasa si se lanza 152 | *luego* de liberarlo. 153 | 154 | En alguno de esos experimentos el programa se te colgara por que habrá threads 155 | que querrán tomar el lock y no podrán por que otro ya lo tomo y se olvido 156 | de liberarlo. 157 | 158 | Eso es un **deadlock** 159 | 160 | Ahora enteras por que se hace tanto **énfasis** en usar RAII: 161 | liberar la memoria **no** es lo único que hay que liberar. 162 | 163 | Has llegado al final del ejercicio, continua 164 | con el siguiente. 165 | */ 166 | -------------------------------------------------------------------------------- /05_sumatoria_with_mutex.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | [1] 3 | Para evitar que múltiples hilos accedan a un 4 | recurso compartido (variable/objeto) se usa un 5 | mecanismo de coordinación llamado Mutex 6 | (Mutual Exclusion) 7 | 8 | El ejemplo debería imprimir por pantalla el 9 | número 479340 siempre. 10 | 11 | Para verificar que efectivamente no hay una 12 | race condition, correr esto: 13 | for i in {0..10000} 14 | do 15 | ./05_sumatoria_with_mutex.exe 16 | done | uniq 17 | 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #define N 10 27 | #define ROUNDS 1000000 28 | 29 | // Misma clase Thread que en 03_is_prime_parallel_by_inheritance.cpp 30 | #include "libs/thread.h" 31 | 32 | class Sum: public Thread { 33 | private: 34 | unsigned int *start; 35 | unsigned int *end; 36 | 37 | unsigned int &result; 38 | 39 | 40 | /* [2] Referencia a un mutex: 41 | no hay que ni copiarlo ni moverlo. 42 | 43 | *** Importante *** 44 | Si un grupo de hilos va a compartir una 45 | variable/objeto (y por ende la posibilidad 46 | de una race condition), los hilos deben 47 | coordinar entre ellos el acceso a dicha 48 | variable. 49 | Para ello deben compartir el mismo 50 | objeto mutex 51 | */ 52 | std::mutex &m; 53 | 54 | public: 55 | Sum(unsigned int *start, 56 | unsigned int *end, 57 | unsigned int &result, 58 | std::mutex &m) : 59 | start(start), end(end), 60 | result(result), m(m) {} 61 | 62 | virtual void run() override { 63 | unsigned int temporal_sum; 64 | for (int round = 0; round < ROUNDS; ++round) { 65 | temporal_sum = 0; 66 | for (unsigned int *p = start; p < end; ++p) { 67 | temporal_sum += *p; 68 | } 69 | } 70 | 71 | 72 | /* [3] Tomamos (adquirimos) el mutex. 73 | Cualquier otro hilo (incluido el 74 | nuestro) que quiera tomar este mutex 75 | se bloqueara hasta que nosotros 76 | llamemos a unlock y lo liberemos. 77 | */ 78 | m.lock(); // --+- 79 | // | solo un 80 | result += temporal_sum; // | hilo a 81 | // | la vez: 82 | // | CS 83 | /* [4] Liberamos el mutex para 84 | que otros hilos lo puedan 85 | tomar y entrar a la region 86 | critica (CS). // | 87 | */ // | 88 | m.unlock(); // --+- 89 | } 90 | }; 91 | 92 | int main() { 93 | unsigned int nums[N] = { 132131, 1321, 31371, 94 | 30891, 891, 123891, 95 | 3171, 30891, 891, 96 | 123891 }; 97 | unsigned int result = 0; 98 | 99 | /* [5] Un único mutex; No un mutex por hilo 100 | 101 | [6] Hay otras variantes de mutexes como 102 | recursive_mutex y timed_mutex que pueden 103 | resultar "tentadoramente más fáciles y 104 | convenientes" pero que pueden en realidad 105 | enmascarar un mal diseño detrás de escena 106 | 107 | No usarlas a menos que no haya otra 108 | alternativa. 109 | */ 110 | std::mutex m; 111 | 112 | std::vector threads; 113 | 114 | for (int i = 0; i < N/2; ++i) { 115 | threads.push_back(new Sum( 116 | &nums[i*2], 117 | &nums[(i+1)*2], 118 | result, 119 | m 120 | )); 121 | } 122 | 123 | for (int i = 0; i < N/2; ++i) { 124 | threads[i]->start(); 125 | } 126 | 127 | for (int i = 0; i < N/2; ++i) { 128 | threads[i]->join(); 129 | delete threads[i]; 130 | } 131 | 132 | std::cout << result; // 479340 133 | std::cout << "\n"; 134 | 135 | return 0; 136 | } 137 | /* [7] 138 | 139 | Extra challenges: 140 | 141 | - En [3] proba en mover el `m.lock()` al **principio** del método run. 142 | Deberías seguir sin una RC **pero** vas a ver q todo funciona más lento. 143 | 144 | Probar en medir los tiempos con `time`. Fíjate el tiempo "real". 145 | (nota: si no ves mucha diferencia podes probar en aumentar el valor 146 | de ROUNDS para que se note) 147 | 148 | Cuanto más grande sea la zona cubierta por un lock y cuantos 149 | más threads **compitan por adquirir el lock**, 150 | más se van a trabar los threads y menos concurrente va a ser 151 | el procesamiento (cada vez se parecerá a un sistema secuencial). 152 | 153 | En estos casos se dice que hay **contention** sobre el lock 154 | o sobre el recurso compartido. 155 | 156 | - En vez de crear un único mutex en [5] y quedarse 157 | con una referencia en [2], proba en tener un mutex **propio** 158 | en cada functor (en [2]). 159 | 160 | Te sigue funcionando o volvieron las RCs? 161 | 162 | Has llegado al final del ejercicio, continua 163 | con el siguiente. 164 | */ 165 | -------------------------------------------------------------------------------- /07_sumatoria_with_monitor.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | [1] Ejemplo de encapsulamiento de un objeto 3 | compartido y su mutex en un único objeto. 4 | 5 | Este objeto protegido se lo conoce como 6 | **monitor** (si, lo se, no es un nombre copado, 7 | de alguna forma quiere decir que el objeto 8 | monitorea los acceso al objeto compartido). 9 | 10 | El ejemplo debería imprimir por pantalla el 11 | número 479340. 12 | 13 | Para verificar que efectivamente no hay una 14 | race condition, correr esto: 15 | for i in {0..1000} 16 | do 17 | ./07_sumatoria_with_monitor.exe 18 | done | uniq 19 | 20 | */ 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #define N 10 29 | 30 | // Misma clase Thread que en 03_is_prime_parallel_by_inheritance.cpp 31 | #include "libs/thread.h" 32 | 33 | class ResultProtected { // aka monitor 34 | private: 35 | 36 | /* [2] el monitor u objeto protegido tiene 37 | su mutex y tiene al objeto compartido 38 | que hay que proteger 39 | */ 40 | std::mutex m; 41 | unsigned int result; 42 | 43 | public: 44 | ResultProtected(unsigned int v): result(v) {} 45 | 46 | /* [3] *** Importante *** 47 | Cada método "protegido" de un monitor 48 | debería ser una critical section 49 | 50 | Poner locks por todos lados ***NO** es 51 | una buena idea. Solo hara que las cosas 52 | se cuelguen y no funcionen 53 | 54 | En 08_monitor_interface_critical_section.cpp 55 | lo vamos a ver bien. 56 | */ 57 | void inc(unsigned int s) { 58 | 59 | /* 60 | [4] En el ejemplo anterior 06_sumatoria_with_locks_raii.cpp 61 | implementamos un objeto RAII llamado Lock. 62 | 63 | Acá vamos a usar el provisto por C++, el 64 | std::unique_lock. 65 | 66 | C++ provee otros mutexes como: 67 | 68 | - lock_guard: equivale a unique_lock y seria más seguro de usar 69 | por q tiene una API publica más reducida. 70 | Lo malo es q no lo podes usar con conditional_variables 71 | (que las veremos pronto) 72 | - scoped_lock: te permite hacer lock sobre más de 1 mutex 73 | a la vez. 74 | 99.9% de las veces q tengas q lockear más de 1 mutex es 75 | por q tenes un serio problema en el diseño. 76 | 77 | En este caso podríamos usar lock_guard perfectamente 78 | pero preferí usar unique_lock por q es el mismo que usare 79 | luego en los ejemplos de conditional_variables. 80 | */ 81 | std::unique_lock lck(m); 82 | result += s; 83 | } 84 | 85 | unsigned int get_val() { 86 | std::unique_lock lck(m); 87 | return result; 88 | } 89 | 90 | }; 91 | 92 | class Sum : public Thread { 93 | private: 94 | unsigned int *start; 95 | unsigned int *end; 96 | 97 | /* [5] Una referencia al monitor: 98 | el objeto compartido y su mutex 99 | 100 | En general los objetos activos (hilos) 101 | no deberían tener referencias a mutexes 102 | ni manejarlos sino tener referencias a 103 | los monitores y que estos coordinen el 104 | acceso y protejan al recurso compartido 105 | */ 106 | ResultProtected &result; 107 | 108 | public: 109 | Sum(unsigned int *start, 110 | unsigned int *end, 111 | ResultProtected &result) : 112 | start(start), end(end), 113 | result(result) {} 114 | 115 | virtual void run() override { 116 | unsigned int temporal_sum = 0; 117 | for (unsigned int *p = start; p < end; ++p) { 118 | temporal_sum += *p; 119 | } 120 | 121 | /* [6] No nos encargamos de proteger 122 | el recurso compartido (unsigned int 123 | result) sino que el objeto protegido 124 | (monitor) de tipo ResultProtected sera 125 | el responsable de protegerlo. 126 | 127 | Encapsulamos toda la critical section 128 | CS en un único método del monitor. 129 | */ 130 | result.inc(temporal_sum); 131 | } 132 | }; 133 | 134 | int main() { 135 | unsigned int nums[N] = { 132131, 1321, 31371, 136 | 30891, 891, 123891, 137 | 3171, 30891, 891, 138 | 123891 }; 139 | ResultProtected result(0); 140 | 141 | std::vector threads; 142 | 143 | for (int i = 0; i < N/2; ++i) { 144 | threads.push_back(new Sum( 145 | &nums[i*2], 146 | &nums[(i+1)*2], 147 | result 148 | )); 149 | } 150 | 151 | for (int i = 0; i < N/2; ++i) { 152 | threads[i]->start(); 153 | } 154 | 155 | for (int i = 0; i < N/2; ++i) { 156 | threads[i]->join(); 157 | delete threads[i]; 158 | } 159 | 160 | std::cout << result.get_val(); // 479340 161 | std::cout << "\n"; 162 | 163 | return 0; 164 | } 165 | /* [7] 166 | 167 | Medita sobre [4] y [6]. La parte realmente complicada 168 | de trabajar con threads, mutexes y monitores es la descubrir 169 | las critical sections reales. 170 | 171 | En 08_monitor_interface_critical_section.cpp vamos 172 | a ver esto con otro ejemplo. 173 | */ 174 | 175 | -------------------------------------------------------------------------------- /04_sumatoria_with_race_conditions.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | [1] Ejemplo de una race condition: accesos de 3 | lecto/escritura a una misma variable u objeto 4 | por múltiples hilos que terminan dejando a la 5 | variable/objeto en un estado inconsistente 6 | 7 | Este ejemplo calcula una suma y el resultado final 8 | debería ser 479340. 9 | 10 | Como siempre se suman los mismos números el 11 | resultado final 479340 debería ser siempre el 12 | mismo pero debido a la race condition, 13 | el resultado puede variar. 14 | 15 | Para tratar de ver el bug (puede ser difícil de 16 | triggerearlo), correr esto en la consola: 17 | for i in {0..10000} 18 | do 19 | ./04_sumatoria_with_race_conditions.exe; 20 | done | uniq 21 | 22 | Ese código bash corre el program 10000 veces y se 23 | queda con los valores únicos (podrás frenarlo antes con Ctrl-C) 24 | 25 | Si no hubiera RC siempre deberías ver el mismo valor (479340) 26 | una única vez pero veras q no. 27 | 28 | Nota: triggerear la RC es básicamente por azar, probar varias 29 | veces! 30 | 31 | */ 32 | 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | #define N 10 39 | #define ROUNDS 1 40 | 41 | // Misma clase Thread que en 03_is_prime_parallel_by_inheritance.cpp 42 | #include "libs/thread.h" 43 | 44 | 45 | class Sum: public Thread { 46 | private: 47 | unsigned int *start; 48 | unsigned int *end; 49 | 50 | unsigned int &result; 51 | 52 | public: 53 | Sum(unsigned int *start, 54 | unsigned int *end, 55 | unsigned int &result) : 56 | start(start), end(end), result(result) {} 57 | 58 | virtual void run() override { 59 | /* [2] 60 | * Sumo un subconjunto de números: 61 | * 62 | * /-- start /-- end 63 | * V V 64 | * - - --+--+--+--+--+--+--+--+--- - - 65 | * : ::|nn|mm|nn|nn|nn|nn|mm|:: : 66 | * - - --+--+--+--+--+--+--+--+--- - - 67 | * */ 68 | unsigned int temporal_sum; 69 | for (int round = 0; round < ROUNDS; ++round) { 70 | /* 71 | * Nota: los sumo muchas veces (ROUNDS veces) solo 72 | * para poder correr el thread mucho y poder 73 | * mostrar fácilmente *race conditions*, *contention* 74 | * y otras yerbas. 75 | * */ 76 | temporal_sum = 0; 77 | for (unsigned int *p = start; 78 | p < end; 79 | ++p) { 80 | temporal_sum += *p; 81 | } 82 | } 83 | 84 | /* [3] acá esta la race condition: 85 | múltiples instancias del functor 86 | Sum corriendo, cada uno, el método 87 | "run" en threads en paralelo 88 | 89 | Todos **escribiendo** a la variable 90 | **compartida** "result" con escrituras 91 | **no-atómicas**. 92 | 93 | Esta línea es la *** critical section *** 94 | que habría que proteger y evitar 95 | que múltiples hilos la accedan concurrentemente 96 | */ 97 | result += temporal_sum; 98 | } 99 | }; 100 | 101 | int main() { 102 | unsigned int nums[N] = { 132131, 1321, 31371, 103 | 30891, 891, 123891, 104 | 3171, 30891, 891, 105 | 123891 }; 106 | unsigned int result = 0; 107 | 108 | std::vector threads; 109 | 110 | for (int i = 0; i < N/2; ++i) { 111 | /* [4] Nótese como cada hilo tiene acceso a la 112 | **misma** variable "result" y que cada 113 | hilo **leera y modificara la misma 114 | variable** 115 | 116 | Esta variable es un **recurso compartido** 117 | */ 118 | threads.push_back(new Sum( 119 | &nums[i*2], 120 | &nums[(i+1)*2], 121 | result 122 | )); 123 | } 124 | 125 | for (int i = 0; i < N/2; ++i) { 126 | threads[i]->start(); 127 | } 128 | 129 | for (int i = 0; i < N/2; ++i) { 130 | threads[i]->join(); 131 | delete threads[i]; 132 | } 133 | 134 | std::cout << result; // 479340 ?? 135 | std::cout << "\n"; 136 | 137 | return 0; 138 | } 139 | /* [5] 140 | 141 | Compila el código con g++ y el flag -fsanitize=thread (thread sanitize) 142 | 143 | g++ -fsanitize=thread -std=c++11 -ggdb -pedantic -Wall -o \ 144 | 04_sumatoria_with_race_conditions_tsan.exe \ 145 | 04_sumatoria_with_race_conditions.cpp \ 146 | -pthread 147 | 148 | Ahora correrlo con la variable de entorno TSAN_OPTIONS='halt_on_error=1' 149 | 150 | for i in {0..10000} 151 | do 152 | TSAN_OPTIONS='halt_on_error=1' ./04_sumatoria_with_race_conditions_tsan.exe; 153 | done | uniq 154 | 155 | TSAN instrumenta tu binario para detectar race conditions. Hace a tu programa 156 | *muy* lento y solo detecta la RC cuando esta se produce (o sea q tenes 157 | q probar el código *muchas* veces). 158 | 159 | No te asustes si no entendes el error de TSAN, lo importante es que lo podes 160 | usar como heurística para saber si hay RCs o no y con algo de suerte podrás 161 | ver hasta el donde esta la RC. 162 | 163 | Ante una RC *siempre* busca que objetos son compartidos, y los métodos 164 | pues son la entrada a las RCs. 165 | 166 | En los siguientes ejercicios vamos a profundizar sobre esto. 167 | 168 | Has llegado al final del ejercicio, continua 169 | con el siguiente. 170 | */ 171 | -------------------------------------------------------------------------------- /08_monitor_interface_critical_section.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | [1] 3 | 4 | Un objeto compartido + mutex no alcanzan: 5 | uno tiene que diseñar los métodos públicos del objeto 6 | como las critical sections y protegerlas. 7 | 8 | Solo así tendrás un *monitor* 9 | 10 | Este ejemplo debería imprimir por pantalla el 11 | número 1 ya que si bien hay varios números 12 | primos, solo queremos si hay (1) primos o no (0) 13 | 14 | Para verificar que efectivamente no hay una 15 | race condition, correr esto: 16 | for i in {0..1000} 17 | do 18 | ./08_monitor_interface_critical_section.exe 19 | done | uniq 20 | 21 | Funcionó?? 22 | 23 | ~~~ 24 | 25 | El bug esta en [2] y [3]. 26 | El fix lo veras en [4] y [5] 27 | 28 | */ 29 | 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | #define N 10 37 | 38 | // Misma clase Thread que en 03_is_prime_parallel_by_inheritance.cpp 39 | #include "libs/thread.h" 40 | 41 | class ResultProtected { // aka monitor 42 | private: 43 | std::mutex m; 44 | unsigned int result; 45 | 46 | public: 47 | ResultProtected(unsigned int v): result(v) {} 48 | 49 | /* [3] *** Importante *** 50 | Cada método "protegido" de un monitor 51 | debería ser una critical section 52 | 53 | Poner locks por todos lados ***NO** es 54 | una buena idea. Solo hara que las cosas 55 | se cuelguen y no funcionen 56 | 57 | Esto ya te lo dije en 07_sumatoria_with_monitor.cpp 58 | y aquí vas a ver un caso explicito. 59 | 60 | En este caso, inc() **no** es la critical 61 | section real así que a pesar de protegerla 62 | con un lock vamos a tener una race condition igual. 63 | */ 64 | void inc(unsigned int s) { 65 | std::unique_lock lck(m); 66 | result += s; 67 | } 68 | 69 | unsigned int get_val() { 70 | std::unique_lock lck(m); 71 | return result; 72 | } 73 | 74 | /* [4] Nuestras critical sections son 75 | get_val y inc_if_you_are_zero 76 | 77 | Descomentar la siguiente implementación 78 | y *borrar* el método inc(): 79 | 80 | Jamas implementen un método protegido 81 | que no represente a una critical section 82 | Alguien desprevenido podría llegar usar 83 | a inc() pensado que es segura cuando no 84 | lo es -> esto es *importante* 85 | 86 | Si queres descubrir las critical sections 87 | siempre pensá "que cosas quiere hacer como 88 | un todo". 89 | 90 | En nuestro caso queremos incrementar 91 | solo si el contador esta en 0. 92 | Queremos "checkear e incrementar" de una 93 | forma atómica. 94 | */ 95 | /* 96 | void inc_if_you_are_zero(unsigned int s) { 97 | std::unique_lock lck(m); 98 | if (result == 0) { 99 | result += s; 100 | } 101 | } 102 | */ 103 | 104 | }; 105 | 106 | class AreAnyPrime : public Thread { 107 | private: 108 | unsigned int n; 109 | ResultProtected &result; 110 | 111 | public: 112 | AreAnyPrime(unsigned int n, 113 | ResultProtected &result) : 114 | n(n), 115 | result(result) {} 116 | 117 | virtual void run() override { 118 | /* Si ya encontramos un número primo, 119 | * salimos */ 120 | if (result.get_val() >= 1) 121 | return; 122 | 123 | /* Y si no, vemos si nuestro número es 124 | * primo o no. */ 125 | for (unsigned int i = 2; i < n; ++i) { 126 | if (n % i == 0) { 127 | return; 128 | } 129 | } 130 | 131 | /* Y si el número es primo, incrementamos 132 | * el contador.... */ 133 | 134 | /* [2] Es este inc() correcto? Es nuestra 135 | critical section? 136 | 137 | La respuesta es no: queremos 138 | incrementar solo si el contador es 139 | 0. 140 | 141 | Si preguntamos con get_val y luego 142 | incrementamos con inc(), no estamos 143 | haciendo una única operación atómica 144 | sino 2 y esto abre la posibilidad a 145 | una data race condition. 146 | */ 147 | result.inc(1); 148 | 149 | /* [5] Borrar la línea "result.inc(1)" 150 | y reemplazarla por la llamada a 151 | inc_if_you_are_zero 152 | */ 153 | /* result.inc_if_you_are_zero(1); */ 154 | } 155 | }; 156 | 157 | 158 | int main() { 159 | unsigned int nums[N] = { 132131, 132130891, 31371, 160 | 132130891, 891, 123891, 161 | 132130891, 132130891, 162 | 132130891 }; 163 | ResultProtected result(0); 164 | 165 | std::vector threads; 166 | 167 | for (int i = 0; i < N; ++i) { 168 | threads.push_back(new AreAnyPrime( 169 | nums[i], 170 | result 171 | )); 172 | } 173 | 174 | for (int i = 0; i < N; ++i) { 175 | threads[i]->start(); 176 | } 177 | 178 | for (int i = 0; i < N; ++i) { 179 | threads[i]->join(); 180 | delete threads[i]; 181 | } 182 | 183 | std::cout << result.get_val(); // 1 184 | std::cout << "\n"; 185 | 186 | return 0; 187 | } 188 | /* [6] 189 | 190 | Recompilar y volver a probar para verificar que 191 | la race condition fue removida 192 | 193 | Conclusion: 194 | 195 | Métodos protegidos MÁS una buena interfaz del 196 | monitor diseñada para resolver el problema 197 | son los que nos evitan las race condition. 198 | 199 | Se deben encontrar primero las regiones criticas 200 | y para cada region critica se debe implementar 201 | un método en el monitor protegido con un mutex. 202 | 203 | Recuerda que una critical section es un código 204 | que vos queres q se ejecute "atómicamente". 205 | 206 | Has llegado al final del ejercicio, y al final 207 | de este tutorial. 208 | 209 | */ 210 | 211 | -------------------------------------------------------------------------------- /libs/queue.h: -------------------------------------------------------------------------------- 1 | #ifndef QUEUE_H_ 2 | #define QUEUE_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | struct ClosedQueue : public std::runtime_error { 12 | ClosedQueue() : std::runtime_error("The queue is closed") {} 13 | }; 14 | 15 | /* 16 | * Multiproducer/Multiconsumer Blocking Queue (MPMC) 17 | * 18 | * Queue is a generic MPMC queue with blocking operations 19 | * push() and pop(). 20 | * 21 | * Two additional methods, try_push() and try_pop() allow 22 | * non-blocking operations. 23 | * 24 | * On a closed queue, any method will raise ClosedQueue. 25 | * 26 | * */ 27 | template > 28 | class Queue { 29 | private: 30 | std::queue q; 31 | const unsigned int max_size; 32 | 33 | bool closed; 34 | 35 | std::mutex mtx; 36 | std::condition_variable is_not_full; 37 | std::condition_variable is_not_empty; 38 | 39 | public: 40 | Queue() : max_size(UINT_MAX-1), closed(false) {} 41 | explicit Queue(const unsigned int max_size) : max_size(max_size), closed(false) {} 42 | 43 | 44 | bool try_push(T const& val) { 45 | std::unique_lock lck(mtx); 46 | 47 | if (closed) { 48 | throw ClosedQueue(); 49 | } 50 | 51 | if (q.size() == this->max_size) { 52 | return false; 53 | } 54 | 55 | if (q.empty()) { 56 | is_not_empty.notify_all(); 57 | } 58 | 59 | q.push(val); 60 | return true; 61 | } 62 | 63 | bool try_pop(T& val) { 64 | std::unique_lock lck(mtx); 65 | 66 | if (q.empty()) { 67 | if (closed) { 68 | throw ClosedQueue(); 69 | } 70 | return false; 71 | } 72 | 73 | if (q.size() == this->max_size) { 74 | is_not_full.notify_all(); 75 | } 76 | 77 | val = q.front(); 78 | q.pop(); 79 | return true; 80 | } 81 | 82 | void push(T const& val) { 83 | std::unique_lock lck(mtx); 84 | 85 | if (closed) { 86 | throw ClosedQueue(); 87 | } 88 | 89 | while (q.size() == this->max_size) { 90 | is_not_full.wait(lck); 91 | } 92 | 93 | if (q.empty()) { 94 | is_not_empty.notify_all(); 95 | } 96 | 97 | q.push(val); 98 | } 99 | 100 | 101 | T pop() { 102 | std::unique_lock lck(mtx); 103 | 104 | while (q.empty()) { 105 | if (closed) { 106 | throw ClosedQueue(); 107 | } 108 | is_not_empty.wait(lck); 109 | } 110 | 111 | if (q.size() == this->max_size) { 112 | is_not_full.notify_all(); 113 | } 114 | 115 | T const val = q.front(); 116 | q.pop(); 117 | 118 | return val; 119 | } 120 | 121 | void close() { 122 | std::unique_lock lck(mtx); 123 | 124 | if (closed) { 125 | throw std::runtime_error("The queue is already closed."); 126 | } 127 | 128 | closed = true; 129 | is_not_empty.notify_all(); 130 | } 131 | 132 | private: 133 | Queue(const Queue&) = delete; 134 | Queue& operator=(const Queue&) = delete; 135 | 136 | }; 137 | 138 | template<> 139 | class Queue { 140 | private: 141 | std::queue q; 142 | const unsigned int max_size; 143 | 144 | bool closed; 145 | 146 | std::mutex mtx; 147 | std::condition_variable is_not_full; 148 | std::condition_variable is_not_empty; 149 | 150 | public: 151 | explicit Queue(const unsigned int max_size) : max_size(max_size), closed(false) {} 152 | 153 | 154 | bool try_push(void* const & val) { 155 | std::unique_lock lck(mtx); 156 | 157 | if (closed) { 158 | throw ClosedQueue(); 159 | } 160 | 161 | if (q.size() == this->max_size) { 162 | return false; 163 | } 164 | 165 | if (q.empty()) { 166 | is_not_empty.notify_all(); 167 | } 168 | 169 | q.push(val); 170 | return true; 171 | } 172 | 173 | bool try_pop(void*& val) { 174 | std::unique_lock lck(mtx); 175 | 176 | if (q.empty()) { 177 | if (closed) { 178 | throw ClosedQueue(); 179 | } 180 | return false; 181 | } 182 | 183 | if (q.size() == this->max_size) { 184 | is_not_full.notify_all(); 185 | } 186 | 187 | val = q.front(); 188 | q.pop(); 189 | return true; 190 | } 191 | 192 | void push(void* const& val) { 193 | std::unique_lock lck(mtx); 194 | 195 | if (closed) { 196 | throw ClosedQueue(); 197 | } 198 | 199 | while (q.size() == this->max_size) { 200 | is_not_full.wait(lck); 201 | } 202 | 203 | if (q.empty()) { 204 | is_not_empty.notify_all(); 205 | } 206 | 207 | q.push(val); 208 | } 209 | 210 | 211 | void* pop() { 212 | std::unique_lock lck(mtx); 213 | 214 | while (q.empty()) { 215 | if (closed) { 216 | throw ClosedQueue(); 217 | } 218 | is_not_empty.wait(lck); 219 | } 220 | 221 | if (q.size() == this->max_size) { 222 | is_not_full.notify_all(); 223 | } 224 | 225 | void* const val = q.front(); 226 | q.pop(); 227 | 228 | return val; 229 | } 230 | 231 | void close() { 232 | std::unique_lock lck(mtx); 233 | 234 | if (closed) { 235 | throw std::runtime_error("The queue is already closed."); 236 | } 237 | 238 | closed = true; 239 | is_not_empty.notify_all(); 240 | } 241 | 242 | private: 243 | Queue(const Queue&) = delete; 244 | Queue& operator=(const Queue&) = delete; 245 | 246 | }; 247 | 248 | 249 | template 250 | class Queue : private Queue { 251 | public: 252 | explicit Queue(const unsigned int max_size) : Queue(max_size) {} 253 | 254 | 255 | bool try_push(T* const& val) { 256 | return Queue::try_push(val); 257 | } 258 | 259 | bool try_pop(T*& val) { 260 | return Queue::try_pop((void*&)val); 261 | } 262 | 263 | void push(T* const& val) { 264 | return Queue::push(val); 265 | } 266 | 267 | 268 | T* pop() { 269 | return (T*) Queue::pop(); 270 | } 271 | 272 | void close() { 273 | return Queue::close(); 274 | } 275 | 276 | private: 277 | Queue(const Queue&) = delete; 278 | Queue& operator=(const Queue&) = delete; 279 | 280 | }; 281 | 282 | #endif 283 | -------------------------------------------------------------------------------- /10_blocking_queue_with_busy_wait_and_polling.cpp: -------------------------------------------------------------------------------- 1 | /* [1] 2 | Implementación de una queue protegida (thread safe) 3 | y bloqueante en su version busy-wait / polling 4 | (**ineficiente** en términos de CPU) 5 | 6 | No solo los métodos push y pop usan un mutex 7 | para evitar race conditions sino que el push 8 | dejara de poner elementos en la queue si esta esta llena 9 | y el pull no retornara hasta que no haya algo en la queue 10 | para retirar. 11 | 12 | El push y pop se **bloquean** cuando la queue esta llena / vacía 13 | respectivamente. 14 | 15 | Te suena a algo? Es igual que lo que sucede con los sockets 16 | donde el send y recv se bloquean si no se puede enviar más datos 17 | o no se recibió dato alguno. 18 | 19 | **Misma idea** 20 | 21 | En esta implementación se hara uso de busy-waits y polling 22 | algo que es muy ineficiente en términos de CPU pero en ciertas 23 | aplicaciones es la única solución. 24 | 25 | Mientras ejecutas el ejemplo, ejecuta 'top' en otra consola 26 | y observa el uso de la CPU. 27 | 28 | A cuanto se dispara al CPU de tu máquina? 29 | **/ 30 | 31 | #include 32 | #include 33 | 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | /* [2] Blocking Queue. 40 | 41 | Se implementan los métodos push() y pop() 42 | que ponen y retiran elementos de la queue. 43 | 44 | Si la queue esta llena, push() se bloquea; 45 | si la queue esta vacía pop() se bloquea. 46 | 47 | El mecanismo de bloqueo se hara con loops: busy-waits y 48 | polling. 49 | **/ 50 | class Queue { 51 | private: 52 | std::queue q; 53 | const unsigned int max_size; 54 | 55 | std::mutex mtx; 56 | 57 | public: 58 | Queue(const unsigned int max_size) : max_size(max_size) {} 59 | /* 60 | * [3] 61 | * 62 | * Un Blocking Queue puede también ofrecer try_push() y try_pop() 63 | * no-bloqueantes. 64 | * 65 | * Esto permite que un thread (digamos el consumidor) haga 66 | * try_pop() y no se bloque si esta vacía y al mismo tiempo 67 | * otro thread (un productor) llame a push() y se bloquee 68 | * se la queue esta llena. 69 | */ 70 | bool try_push(const int& val) { 71 | std::unique_lock lck(mtx); 72 | if (q.size() == this->max_size) { 73 | return false; 74 | } 75 | 76 | q.push(val); 77 | return true; 78 | } 79 | 80 | bool try_pop(int& val) { 81 | std::unique_lock lck(mtx); 82 | if (q.empty()) { 83 | return false; 84 | } 85 | 86 | val = q.front(); 87 | q.pop(); 88 | return true; 89 | } 90 | 91 | void push(const int& val) { 92 | mtx.lock(); 93 | 94 | /* [4] 95 | Busy wait: esperamos que la queue este no-llena 96 | para poder guardar el elemento en la queue. 97 | 98 | La forma de esperar es con un loop que virtualmente 99 | no hace nada. 100 | 101 | Obviamente debemos deslockear y re-lockead el mutex 102 | para que otros hilos tengan la oportunidad de hacer 103 | un pull. 104 | 105 | Es muy ineficiente en términos de CPU pero en ciertas 106 | aplicaciones es la única solución. 107 | */ 108 | while (q.size() >= this->max_size) { 109 | mtx.unlock(); 110 | // Entre el unlock() y el lock() 111 | // otros hilos podrán tomar el mutex aquí 112 | 113 | /* 114 | * [6] 115 | Dormir 10 milisegundos es suficiente? 116 | Tal vez es demasiado y dormimos de más (ineficiente) 117 | Tal vez es poco y loopeamos de más (ineficiente) 118 | * */ 119 | // std::this_thread::sleep_for(std::chrono::milliseconds(10)); 120 | mtx.lock(); 121 | } 122 | 123 | q.push(val); 124 | mtx.unlock(); 125 | } 126 | 127 | 128 | int pop() { 129 | mtx.lock(); 130 | 131 | /* [5] 132 | 133 | Al igual que en [4], esperamos hasta que la queue este 134 | no-vacía 135 | 136 | En este caso en vez de hacer un busy-wait haremos una 137 | variante: 138 | 139 | La alternativa es poner un sleep entre el unlock y 140 | el lock. Esto se lo conoce como Polling. Reduce el uso 141 | de CPU pero sigue siendo ineficiente ya que no es fácil 142 | predecir cuanto tiempo se debe dormir (el parámetro del 143 | sleep) 144 | 145 | **/ 146 | while (q.empty()) { 147 | mtx.unlock(); 148 | /* 149 | * [7] 150 | Dormir 10 milisegundos es suficiente? 151 | Tal vez es demasiado y dormimos de más (ineficiente) 152 | Tal vez es poco y loopeamos de más (ineficiente) 153 | * */ 154 | // std::this_thread::sleep_for(std::chrono::milliseconds(10)); 155 | mtx.lock(); 156 | } 157 | 158 | const int val = q.front(); 159 | q.pop(); 160 | 161 | mtx.unlock(); 162 | return val; 163 | } 164 | 165 | private: 166 | Queue(const Queue&) = delete; 167 | Queue& operator=(const Queue&) = delete; 168 | }; 169 | 170 | 171 | namespace { 172 | const int MAX_NUM = 30; 173 | const int PROD_NUM = 20; 174 | const int CONS_NUM = 10; 175 | const int QUEUE_MAXSIZE = 10; 176 | } 177 | 178 | 179 | void sleep_a_little(std::default_random_engine& generator) { 180 | std::uniform_int_distribution get_random_int(100, 500); 181 | 182 | auto random_int = get_random_int(generator); 183 | auto milliseconds_to_sleep = std::chrono::milliseconds(random_int); 184 | std::this_thread::sleep_for(milliseconds_to_sleep); // sleep some "pseudo-random" time 185 | } 186 | 187 | /* [8] 188 | * 189 | * Ahora el productor llama a push() y si la queue esta llena 190 | * el bloqueo se hace dentro de push(). 191 | * 192 | * El productor no debe hacer ningún loop de reintento 193 | * (a diferencia de lo que pasaba con try_push()) 194 | * */ 195 | void productor_de_numeros(Queue& q) { 196 | std::default_random_engine generator; 197 | 198 | for (int i = 0; i < MAX_NUM; ++i) { 199 | sleep_a_little(generator); 200 | q.push(1); 201 | } 202 | } 203 | 204 | /* [9] 205 | * 206 | * Ahora el consumidor llama a pop() y si la queue esta vacía 207 | * el bloqueo se hace dentro de pop(). 208 | * 209 | * El consumidor no debe hacer ningún loop de reintento 210 | * (a diferencia de lo que pasaba con try_pop()) 211 | * */ 212 | void consumidor_de_numeros(Queue& q, int& resultado_parcial) { 213 | std::default_random_engine generator; 214 | 215 | int suma = 0; 216 | int n; 217 | do { 218 | n = q.pop(); 219 | suma += n; 220 | 221 | sleep_a_little(generator); 222 | } while (n != 0); 223 | 224 | resultado_parcial = suma; 225 | } 226 | 227 | 228 | // Este main es igual al de 09_non_blocking_queue.cpp 229 | int main(int argc, char *argv[]) { 230 | Queue q(QUEUE_MAXSIZE); 231 | 232 | std::vector productores(PROD_NUM); 233 | std::vector consumidores(CONS_NUM); 234 | std::vector resultados_parciales(CONS_NUM); 235 | 236 | std::cout << "Lanzando " << CONS_NUM << " consumidores de numeros\n"; 237 | for (int i = 0; i < CONS_NUM; ++i) { 238 | consumidores[i] = std::thread(&consumidor_de_numeros, std::ref(q), std::ref(resultados_parciales[i])); 239 | } 240 | std::cout << "Lanzando " << PROD_NUM << " productores de numeros\n"; 241 | for (int i = 0; i < PROD_NUM; ++i) { 242 | productores[i] = std::thread(&productor_de_numeros, std::ref(q)); 243 | } 244 | 245 | std::cout << "Esperando a que los " << PROD_NUM << " productores terminen\n\n"; 246 | for (int i = 0; i < PROD_NUM; ++i) { 247 | productores[i].join(); 248 | } 249 | 250 | std::cout << "Los consumidores deben estar bloqueados en el pop de la queue\n"; 251 | std::cout << "Enviando (push) " << CONS_NUM << " ceros para que cada consumidor lo saque de la queue y finalice.\n\n"; 252 | for (int i = 0; i < CONS_NUM; ++i) { 253 | q.push(0); 254 | } 255 | 256 | std::cout << "Esperando a que los " << CONS_NUM << " consumidores terminen\n\n"; 257 | int suma = 0; 258 | for (int i = 0; i < CONS_NUM; ++i) { 259 | consumidores[i].join(); 260 | suma += resultados_parciales[i]; 261 | } 262 | 263 | std::cout << "Se lanzaron " << PROD_NUM << " productores que cada uno creo " << MAX_NUM << " 'unos'\n"; 264 | std::cout << "Por lo tanto, la suma total deberia dar " << PROD_NUM * MAX_NUM << " y la suma efectivamente dio " << suma << "\n"; 265 | std::cout << ((PROD_NUM * MAX_NUM == suma)? "OK\n" : "FALLO\n"); 266 | return 0; 267 | } 268 | 269 | /* 270 | * [10] 271 | 272 | Viste como se dispara la CPU? 273 | 274 | Un busy-wait es un loop que corre tan rápido como puede para 275 | ver si una condición esta dada o no. 276 | 277 | Se usa en aplicaciones **muy** especificas por que como veras, 278 | *te quema la CPU!* 279 | 280 | Anda a [6] y [7] y descomenta el "sleep". Compila y volvé 281 | a correr el código. 282 | 283 | Proba valores de 10 milliseconds, de 1 millisecond 284 | y otro de 1000 milliseconds. 285 | 286 | Que te dice `top`? Ya no te quema la CPU? 287 | Y el programa en total, cuanto tarda en cada caso? 288 | (medilo con `time`) 289 | 290 | Los busy-waits con sleep "martillan" menos a la CPU 291 | (less hammering) pero introducen un delay/latencia 292 | que es difícil de tunear. 293 | 294 | El polling se usa solo cuando no tenes otra forma 295 | de coordinación (imagínate que estas esperando un mail 296 | muy importante y estas apretando F5 una y otra vez 297 | refrescando el inbox: no te queda otra que polling) 298 | 299 | En el siguiente ejemplo veremos **la** herramienta 300 | para evitar polling: las *conditional variables* 301 | */ 302 | 303 | -------------------------------------------------------------------------------- /03_is_prime_parallel_by_inheritance.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | [1] 3 | Ejemplo de como ejecutar una función/functor en 4 | un hilo separado en C++ 5 | 6 | Esta vez, en vez de usar composición usaremos 7 | **herencia**. 8 | 9 | Para ello crearemos una objeto Thread que 10 | ejecutara un método virtual en su propio hilo 11 | definido por las clases hijas que hereden de 12 | Thread 13 | 14 | Cuando el objeto functor encapsula dentro de él 15 | el concepto de hilo se dice que el objeto es un 16 | "objeto activo". 17 | 18 | 19 | Threads por herencia es la forma de usar threads 20 | en lenguajes como Java. 21 | 22 | Otros, como Python, son iguales a C++ y permiten 23 | las dos opciones (composición y herencia). 24 | 25 | Golang en cambio tiene go-rutinas que son 26 | "threads ligeros" manejados por el runtime de Golang. 27 | */ 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | #define N 10 36 | 37 | class Thread { 38 | private: 39 | std::thread thread; 40 | 41 | public: 42 | Thread () {} 43 | 44 | void start() { 45 | /* [2] Lanzamos el thread que correrá 46 | siempre la misma función (Thread::main) 47 | 48 | Como Thread::main es un **método** 49 | sin parámetros y std::thread espera 50 | a una **función** podemos ver a 51 | Thread::main como una función que 52 | recibe como primer argumento al 53 | objeto this (tal como en los TDA de C!) 54 | 55 | std::thread soporta correr una 56 | función con argumentos con la llamada: 57 | 58 | std::thread( funcion, arg1, arg2, ...) 59 | 60 | Por lo tanto 61 | std::thread( metodo, this ) 62 | 63 | es equivalente a correr el método 64 | sin argumentos en un thread. 65 | 66 | Como Thread::main llama a Thread::run 67 | y Thread::run es un método **polimórfico**, 68 | cada objeto ejecutara 69 | un código particular en el thread. 70 | 71 | Objetos distintos podrán correr en 72 | sus propios threads con esta única 73 | implementación de Thread mientras 74 | hereden de Thread y creen sus propias 75 | versiones del método run. 76 | */ 77 | thread = std::thread( 78 | &Thread::main, 79 | this 80 | ); 81 | } 82 | 83 | // [3] 84 | // 85 | // Este método es el que correrá en su propio thread. 86 | // 87 | // Obviamente el que hacer esta dentro de Thread::run 88 | // que es polimórfico 89 | // 90 | // La idea de Thread::main es que me permite poner 91 | // un try-catch y atrapar cualquier excepción que 92 | // se lance en Thread::run. 93 | // 94 | // Si una excepción se escapa de la función que esta 95 | // corriendo en un thread, el program termina con un abort 96 | // 97 | // Not nice. 98 | // 99 | // Quienes implementen Thread::run deberían atrapar 100 | // las excepciones ellos. El try-catch de Thread::main 101 | // es solo como último recurso. 102 | void main() { 103 | try { 104 | this->run(); 105 | } catch(const std::exception &err) { 106 | // Nota: por simplicidad estoy haciendo unos prints. 107 | // Código productivo debería *loggear* el error, no solo 108 | // prints. 109 | std::cerr << "Unexpected exception: " << err.what() << "\n"; 110 | } catch(...) { 111 | std::cerr << "Unexpected exception: \n"; 112 | } 113 | } 114 | 115 | void join() { 116 | thread.join(); 117 | } 118 | 119 | /* [4] Virtual puro para forzar una 120 | definición en las clases hijas. 121 | 122 | Sera responsabilidad de ellas implementar lo que quieran 123 | que corran en un thread aquí. 124 | */ 125 | virtual void run() = 0; 126 | 127 | 128 | /* [5] Destructor virtual: siempre hacerlo 129 | virtual si pensamos en usar herencia. 130 | */ 131 | virtual ~Thread() {} 132 | 133 | 134 | 135 | /* [6] No tiene sentido copiar hilos, así 136 | que forzamos a que no se puedan copiar. 137 | */ 138 | Thread(const Thread&) = delete; 139 | Thread& operator=(const Thread&) = delete; 140 | 141 | /* [7] Y aunque tiene sentido, vamos a ver 142 | que es un peligro permitir mover un thread 143 | así que también vamos a prohibir el move. 144 | 145 | Lo vas a entender cuando veas [10] 146 | */ 147 | Thread(Thread&& other) = delete; 148 | Thread& operator=(Thread&& other) = delete; 149 | 150 | }; 151 | 152 | 153 | /* [8] Un objeto q encapsula a un thread se lo conoce como un 154 | "objeto activo". 155 | 156 | Un objeto que tiene sus atributos y su lógica 157 | (que encapsula un algoritmo o una tarea) pero 158 | que vive en su propio hilo 159 | */ 160 | class IsPrime : public Thread { 161 | private: 162 | unsigned int n; 163 | bool &result; 164 | 165 | public: 166 | IsPrime(unsigned int n, bool &result) : 167 | n(n), 168 | result(result) {} 169 | 170 | /* [9] El contenido de este método sera el que se ejecute 171 | en el thread 172 | 173 | Nota: las keywords virtual y override en una clase hija 174 | no son necesarias pero **ayudan** a que quede explicita 175 | la intención: estamos **sobrescribiendo** un método virtual 176 | heredado. 177 | */ 178 | virtual void run() override { 179 | for (unsigned int i = 2; i < n; ++i) { 180 | if (n % i == 0) { 181 | result = false; 182 | return; 183 | } 184 | } 185 | 186 | result = true; 187 | } 188 | }; 189 | 190 | 191 | 192 | int main() { 193 | unsigned int nums[N] = { 0, 1, 2, 132130891, 194 | 132130891, 4, 13, 195 | 132130891, 132130891, 196 | 132130871 }; 197 | bool results[N]; 198 | 199 | std::vector threads; 200 | 201 | for (int i = 0; i < N; ++i) { 202 | /* [10] Acá es donde creamos nuestros objetos 203 | 204 | Por que usamos el heap? 205 | 206 | La vas a flipar: 207 | 208 | Cuando hagas Thread::start() vas a estar pasándole 209 | a std::thread un puntero this (un puntero al objeto IsPrime) 210 | 211 | No te olvides q esto no es más que una **dirección** 212 | de la memoria donde IsPrime esta "en ese momento" 213 | 214 | Si guardas el objeto IsPrime en el stack de main 215 | y luego lo moves a otro lado, tu objeto estará ahora 216 | en **otro lugar de la memoria** y esa **dirección** q le pasaste 217 | a std::thread ya **no** sera la dirección "actual" 218 | de IsPrime.. 219 | 220 | En otras palabras std::thread estará usando un puntero/dirección 221 | con contenido indefinido. 222 | 223 | Vos podrías ser cauteloso y **no** mover a IsPrime nunca 224 | pero... que pasa si guardas IsPrime en un container 225 | como std::vector y este te lo mueve? 226 | 227 | >> Game over << 228 | 229 | Esto se lo conoce como "pointer instability" y aunque hay 230 | algunos containers que garantizan estabilidad, es tricky. 231 | 232 | Tenes varias alternativas: 233 | 234 | - usas containers q provean pointer stability como std::list 235 | - usas containers pre-allocados y te aseguras q no sean 236 | redimensionados (como std::vector cuando le seteas capacity) 237 | - o usas el heap y asi tus objetos no seran movidos *aun* 238 | si el container se mueve/redimensiona 239 | 240 | La ultima opcion es la menos eficiente pero es **mucho** 241 | mas segura. 242 | 243 | Por eso usamos el heap: para que quede en un solo lugar 244 | y cualquier puntero a IsPrime se mantenga válido. 245 | * */ 246 | Thread *t = new IsPrime( 247 | nums[i], 248 | results[i]); 249 | threads.push_back(t); 250 | 251 | /* [11] y acá "activamos" a los "objetos activos" 252 | (lanzamos el thread) 253 | */ 254 | t->start(); 255 | } 256 | 257 | /* ************************************** */ 258 | /* Ahora: Todos los hilos están corriendo */ 259 | /* ************************************** */ 260 | 261 | /* [12] Esperamos a que cada hilo termine. 262 | Cada join bloqueara al hilo llamante (main) 263 | hasta que el hilo sobre el cual se le hace 264 | join (threads[i]) termine 265 | 266 | Ademas, por haber usado el heap debemos 267 | hacer el correspondiente delete 268 | */ 269 | for (int i = 0; i < N; ++i) { 270 | threads[i]->join(); 271 | delete threads[i]; 272 | } 273 | 274 | /* **************************************** */ 275 | /* Ahora: Todos los hilos terminaron y sus */ 276 | /* recursos limpiados con el join */ 277 | /* **************************************** */ 278 | 279 | for (int i = 0; i < N; ++i) { 280 | std::cout << results[i] << " "; 281 | } 282 | std::cout << "\n"; 283 | 284 | return 0; 285 | } 286 | 287 | /* [13] 288 | 289 | Has llegado al final del ejercicio, continua 290 | con el siguiente. 291 | */ 292 | 293 | -------------------------------------------------------------------------------- /09_non_blocking_queue.cpp: -------------------------------------------------------------------------------- 1 | /* [1] 2 | * 3 | Hasta ahora has visto como proteger un objeto compartido 4 | con mutex, construyéndole una capa de protección llamada monitor. 5 | 6 | Cuando tengas múltiple threads leyendo y escribiendo sobre 7 | un mismo objeto compartido sin un claro orden, construir 8 | un monitor es tu mejor opción. 9 | 10 | Sin embargo habrá ocasiones en que los threads no acceden 11 | "aleatoriamente" a un objeto. 12 | 13 | Habrá ocasiones en que un thread (digamos Alice) lee y escribe 14 | sobre el objeto compartido, **luego** deja de hacerlo y **luego** 15 | lo lee/escribe un segundo thread (digamos Bob). 16 | 17 | Aunque los threads Alice y Bob tienen acceso via un puntero/referencia 18 | al mismo objeto (y por eso esta compartido), 19 | no esta "realmente" siendo compartido *a la vez*. 20 | 21 | En estos caso podemos optar por darle el **ownership** del 22 | objeto a Alice, que el thread haga lo q necesite y cuando 23 | ya no necesite de él, **se lo pase** al thread Bob. 24 | 25 | En este escenario ese objeto ya **no** estaría compartido 26 | ya que en un momento es accesible *únicamente* por Alice 27 | y al momento siguiente *únicamente* por Bob. 28 | 29 | El único instante donde habría un problema es en el **pasaje**. 30 | 31 | Necesitamos un mecanismo thread-safe que nos permita 32 | **pasar** objetos de un thread a otro. 33 | 34 | Necesitamos un **thread-safe queue**. 35 | 36 | En este ejemplo veremos una thread-safe queue **no-bloqueante**. 37 | 38 | A los threads como Alice que pasan los objetos (los pushean 39 | en la queue) los llamaremos "productores"; a los threads 40 | como Bob que reciben los objetos (los que hacen pop()) 41 | los llamaremos "consumidores". 42 | 43 | Conserva esas dos palabritas cerca de tu corazón, aparecen 44 | en todas las literaturas. 45 | 46 | ~~~ 47 | 48 | Mientras ejecutas el ejemplo, ejecuta 'top' en otra consola 49 | y observa el uso de la CPU. 50 | 51 | Se prende fuego no? (ya veremos como fixear eso) 52 | 53 | **/ 54 | 55 | #include 56 | #include 57 | #include 58 | 59 | #include 60 | #include 61 | #include 62 | #include 63 | 64 | /* [2] NonBlocking Queue 65 | 66 | Se implementan los métodos try_push() y try_pop() 67 | que ponen y retiran elementos de la queue. 68 | 69 | Ambos métodos protegen al recurso (Queue es un monitor) 70 | y por lo tanto es thread safe. 71 | 72 | Los métodos son "try_" por que pueden fallar: 73 | - try_push puede no poner un elemento si la queue esta llena 74 | - try_pop puede no poner un elemento si la queue esta vacía. 75 | 76 | En caso de falla, los métodos *no* reintentan *ni esperan*: 77 | son *no bloqueantes*. 78 | 79 | Estos métodos se usan cuando el thread puede seguir trabajando 80 | y no quiere bloquearse si la queue esta llena/vacía. 81 | 82 | **/ 83 | class Queue { 84 | private: 85 | std::queue q; 86 | const unsigned int max_size; 87 | 88 | std::mutex mtx; 89 | 90 | public: 91 | Queue(const unsigned int max_size) : max_size(max_size) {} 92 | 93 | /* 94 | * [3] 95 | * Intentamos pushear un elemento. Si la queue esta llena 96 | * retornamos que fallamos. 97 | * 98 | * Una queue que impone un limite en la cantidad de elementos 99 | * se la llama BoundedQueue. Aquellas que no, se las llaman 100 | * UnboundedQueue. 101 | */ 102 | bool try_push(const int& val) { 103 | std::unique_lock lck(mtx); 104 | if (q.size() == this->max_size) { 105 | return false; 106 | } 107 | 108 | q.push(val); 109 | return true; 110 | } 111 | 112 | /* 113 | * [4] 114 | * 115 | Retornamos el valor pop'eado por referencia así dejamos 116 | el retorno para el booleano al igual que en el push() 117 | */ 118 | bool try_pop(int& val) { 119 | std::unique_lock lck(mtx); 120 | if (q.empty()) { 121 | return false; 122 | } 123 | 124 | val = q.front(); 125 | q.pop(); 126 | return true; 127 | } 128 | 129 | private: 130 | Queue(const Queue&) = delete; 131 | Queue& operator=(const Queue&) = delete; 132 | 133 | }; 134 | 135 | 136 | namespace { 137 | const int MAX_NUM = 30; 138 | const int PROD_NUM = 10; 139 | const int CONS_NUM = 10; 140 | const int QUEUE_MAXSIZE = 10; 141 | } 142 | 143 | 144 | // Esto esta solo para simular tiempos aleatorios de trabajo en 145 | // los productores y consumidores 146 | void sleep_a_little(std::default_random_engine& generator) { 147 | std::uniform_int_distribution get_random_int(100, 500); 148 | 149 | auto random_int = get_random_int(generator); 150 | auto milliseconds_to_sleep = std::chrono::milliseconds(random_int); 151 | std::this_thread::sleep_for(milliseconds_to_sleep); // sleep some "pseudo-random" time 152 | } 153 | 154 | 155 | 156 | /* [6] 157 | * Para probar la Queue vamos a tener muchos 158 | * "productores" de números 159 | * que serán pusheados en la queue. 160 | * 161 | * Notar que try_push() puede fallar así que el productor 162 | * es el responsable de reintentar. 163 | * */ 164 | void productor_de_numeros(Queue& q) { 165 | std::default_random_engine generator; 166 | 167 | bool ok = false; 168 | for (int i = 0; i < MAX_NUM; ++i) { 169 | ok = false; 170 | sleep_a_little(generator); 171 | 172 | while (not ok) 173 | ok = q.try_push(1); 174 | } 175 | } 176 | 177 | /* [7] 178 | * Por el otro lado vamos a tener muchos 179 | * consumidores que leen de la queue 180 | * hasta que lean el número 0 para luego finalizar. 181 | * 182 | * Notar que try_pop() puede fallar así que el consumidor 183 | * es el responsable de reintentar. 184 | * */ 185 | void consumidor_de_numeros(Queue& q, int& resultado_parcial) { 186 | std::default_random_engine generator; 187 | 188 | bool ok = false; 189 | int suma = 0; 190 | int n; 191 | do { 192 | ok = false; 193 | while (not ok) 194 | ok = q.try_pop(n); 195 | 196 | suma += n; 197 | 198 | sleep_a_little(generator); 199 | } while (n != 0); 200 | 201 | resultado_parcial = suma; 202 | } 203 | 204 | 205 | int main(int argc, char *argv[]) { 206 | Queue q(QUEUE_MAXSIZE); 207 | 208 | std::vector productores(PROD_NUM); 209 | std::vector consumidores(CONS_NUM); 210 | std::vector resultados_parciales(CONS_NUM); 211 | 212 | /* 213 | * [5] 214 | * Lanzamos los productores y consumidores, 215 | * cada uno en su hilo. 216 | * - Los productores irán poniendo un 1 217 | * en la queue cada cierto tiempo. 218 | * - Los consumidores irán sacando un 1 219 | * e irán sumándolos en resultados_parciales 220 | **/ 221 | 222 | std::cout << "Lanzando " << CONS_NUM << " consumidores de numeros\n"; 223 | for (int i = 0; i < CONS_NUM; ++i) { 224 | consumidores[i] = std::thread(&consumidor_de_numeros, std::ref(q), std::ref(resultados_parciales[i])); 225 | } 226 | std::cout << "Lanzando " << PROD_NUM << " productores de numeros\n"; 227 | for (int i = 0; i < PROD_NUM; ++i) { 228 | productores[i] = std::thread(&productor_de_numeros, std::ref(q)); 229 | } 230 | 231 | /* [8] 232 | * Esperamos a que todos los productores terminen 233 | * */ 234 | std::cout << "Esperando a que los " << PROD_NUM << " productores terminen\n\n"; 235 | for (int i = 0; i < PROD_NUM; ++i) { 236 | productores[i].join(); 237 | } 238 | 239 | /* 240 | * [9] 241 | * Los consumidores están bloqueados en la queue, 242 | * tratando de hacer un pop. 243 | * Como le decimos que ya no va a ver ningún 244 | * elemento más y que deben terminar? 245 | * 246 | * En este ejemplo usamos un valor 'dummy' que 247 | * cada consumidor entenderá que representa el fin de 248 | * la queue (una especie de EOF). 249 | * 250 | * Es responsabilidad de quien quiere cerrar la queue enviar 251 | * un dummy por cada consumidor. 252 | * 253 | * Es responsabilidad de cada consumidor que al sacar un dummy 254 | * deje de sacar elementos y finalize. 255 | * 256 | * Así, si pusheamos N dummys, se cerraran N consumidores. 257 | * 258 | * NOTA: No es la única solución y de hecho no es 259 | * necesariamente la más elegante, pero funciona. 260 | * En un ejercicio siguiente te mostrare una alternativa. 261 | **/ 262 | std::cout << "Los consumidores deben estar bloqueados en el pop de la queue\n"; 263 | std::cout << "Enviando (push) " << CONS_NUM << " ceros para que cada consumidor lo saque de la queue y finalice.\n\n"; 264 | for (int i = 0; i < CONS_NUM; ++i) { 265 | while (!q.try_push(0)) {} 266 | } 267 | 268 | /* [10] 269 | * Esperamos a que todos los consumidores terminen 270 | * */ 271 | std::cout << "Esperando a que los " << CONS_NUM << " consumidores terminen\n\n"; 272 | int suma = 0; 273 | for (int i = 0; i < CONS_NUM; ++i) { 274 | consumidores[i].join(); 275 | suma += resultados_parciales[i]; 276 | } 277 | 278 | std::cout << "Se lanzaron " << PROD_NUM << " productores que cada uno creo " << MAX_NUM << " 'unos'\n"; 279 | std::cout << "Por lo tanto, la suma total deberia dar " << PROD_NUM * MAX_NUM << " y la suma efectivamente dio " << suma << "\n"; 280 | std::cout << ((PROD_NUM * MAX_NUM == suma)? "OK\n" : "FALLO\n"); 281 | return 0; 282 | } 283 | 284 | /* [11] 285 | * Challenge: 286 | * 287 | * Agrega 2 nuevos métodos thread safe y no-bloqueantes: 288 | * 289 | * - int push_some(const int* values, const int cnt) 290 | * - int pop_some(int* values, int& cnt) 291 | * 292 | * push_some() pushea de forma atómica hasta cnt elementos en la queue. 293 | * Menos elementos pueden ser pusheados si se llega al limite. 294 | * Retorna la cantidad de elementos que fueron efectivamente pusheados. 295 | * 296 | * pop_some() retira de forma atómica hasta cnt elementos de la queue. 297 | * Menos elementos pueden ser retirados si la queue queda vacía. 298 | * Retorna la cantidad de elementos que fueron efectivamente retirados. 299 | * 300 | * Notas una semejanza entre push_some()/pop_some() de la queue 301 | * y el recvsome()/sendsome() del socket? 302 | * 303 | * */ 304 | 305 | -------------------------------------------------------------------------------- /tests/queue.cpp: -------------------------------------------------------------------------------- 1 | #include "../libs/queue.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | /* 8 | * A small test for Queue to ensure that it works 9 | * under different types of T 10 | * 11 | * It is not an exhaustive test. 12 | * */ 13 | 14 | namespace { 15 | const int QUEUE_MAXSIZE = 10; 16 | } 17 | 18 | void raise_if_false(bool ok) { 19 | if (!ok) 20 | throw std::runtime_error("assertion failed"); 21 | } 22 | 23 | void test_non_blocking_queue__int() { 24 | Queue q(QUEUE_MAXSIZE); 25 | int val; 26 | bool ok; 27 | 28 | // We expect to be able to push N elements for a N limit 29 | // queue without trouble 30 | for (int i = 0; i < QUEUE_MAXSIZE; ++i) { 31 | ok = q.try_push(i); 32 | raise_if_false(ok); 33 | } 34 | 35 | // The N+1 element however, should fail 36 | ok = q.try_push(999); 37 | raise_if_false(!ok); 38 | 39 | // A pop should work just fine, retrieving 40 | // the first pushed element. 41 | ok = q.try_pop(val); 42 | raise_if_false(ok); 43 | raise_if_false(val == 0); 44 | 45 | // Now that we made room we can push one more element 46 | ok = q.try_push(999); 47 | raise_if_false(ok); 48 | 49 | // We expect to pop all the elements of the queue in 50 | // a FIFO order (in the loop we pop N-1 elements) 51 | for (int i = 1; i < QUEUE_MAXSIZE; ++i) { 52 | ok = q.try_pop(val); 53 | raise_if_false(ok); 54 | raise_if_false(val == i); 55 | } 56 | 57 | // Pop the last pushed value 58 | ok = q.try_pop(val); 59 | raise_if_false(ok); 60 | raise_if_false(val == 999); 61 | 62 | // Push some values... 63 | q.push(42); 64 | q.push(57); 65 | 66 | // ...close the queue 67 | q.close(); 68 | 69 | // and check that we cannot push anything else 70 | try { 71 | q.try_push(47); 72 | raise_if_false(false); 73 | } catch (const ClosedQueue&) { 74 | raise_if_false(true); 75 | } 76 | 77 | // but we can pop until the queue gets empty 78 | val = q.pop(); 79 | raise_if_false(val == 42); 80 | 81 | val = q.pop(); 82 | raise_if_false(val == 57); 83 | try { 84 | q.try_pop(val); 85 | raise_if_false(false); 86 | } catch (const ClosedQueue&) { 87 | raise_if_false(true); 88 | } 89 | 90 | std::cout << "[OK] test_non_blocking_queue__int\n"; 91 | } 92 | 93 | void test_non_blocking_queue__complex() { 94 | Queue> q(QUEUE_MAXSIZE); 95 | std::complex val; 96 | bool ok; 97 | 98 | // We expect to be able to push N elements for a N limit 99 | // queue without trouble 100 | for (int i = 0; i < QUEUE_MAXSIZE; ++i) { 101 | ok = q.try_push(i); 102 | raise_if_false(ok); 103 | } 104 | 105 | // The N+1 element however, should fail 106 | ok = q.try_push(999); 107 | raise_if_false(!ok); 108 | 109 | // A pop should work just fine, retrieving 110 | // the first pushed element. 111 | ok = q.try_pop(val); 112 | raise_if_false(ok); 113 | raise_if_false(val == 0); 114 | 115 | // Now that we made room we can push one more element 116 | ok = q.try_push(999); 117 | raise_if_false(ok); 118 | 119 | // We expect to pop all the elements of the queue in 120 | // a FIFO order (in the loop we pop N-1 elements) 121 | for (int i = 1; i < QUEUE_MAXSIZE; ++i) { 122 | ok = q.try_pop(val); 123 | raise_if_false(ok); 124 | raise_if_false(val == i); 125 | } 126 | 127 | // Pop the last pushed value 128 | ok = q.try_pop(val); 129 | raise_if_false(ok); 130 | raise_if_false(val == 999); 131 | 132 | // Push some values... 133 | q.push(42); 134 | q.push(57); 135 | 136 | // ...close the queue 137 | q.close(); 138 | 139 | // and check that we cannot push anything else 140 | try { 141 | q.try_push(47); 142 | raise_if_false(false); 143 | } catch (const ClosedQueue&) { 144 | raise_if_false(true); 145 | } 146 | 147 | // but we can pop until the queue gets empty 148 | val = q.pop(); 149 | raise_if_false(val == 42); 150 | 151 | val = q.pop(); 152 | raise_if_false(val == 57); 153 | try { 154 | q.try_pop(val); 155 | raise_if_false(false); 156 | } catch (const ClosedQueue&) { 157 | raise_if_false(true); 158 | } 159 | 160 | std::cout << "[OK] test_non_blocking_queue__complex\n"; 161 | } 162 | 163 | struct Value { 164 | int i; 165 | Value(int i=0) : i(i) {} 166 | operator int() { return i; } 167 | }; 168 | 169 | void test_non_blocking_queue__value() { 170 | Queue q(QUEUE_MAXSIZE); 171 | Value val; 172 | bool ok; 173 | 174 | // We expect to be able to push N elements for a N limit 175 | // queue without trouble 176 | for (int i = 0; i < QUEUE_MAXSIZE; ++i) { 177 | ok = q.try_push(i); 178 | raise_if_false(ok); 179 | } 180 | 181 | // The N+1 element however, should fail 182 | ok = q.try_push(999); 183 | raise_if_false(!ok); 184 | 185 | // A pop should work just fine, retrieving 186 | // the first pushed element. 187 | ok = q.try_pop(val); 188 | raise_if_false(ok); 189 | raise_if_false(val == 0); 190 | 191 | // Now that we made room we can push one more element 192 | ok = q.try_push(999); 193 | raise_if_false(ok); 194 | 195 | // We expect to pop all the elements of the queue in 196 | // a FIFO order (in the loop we pop N-1 elements) 197 | for (int i = 1; i < QUEUE_MAXSIZE; ++i) { 198 | ok = q.try_pop(val); 199 | raise_if_false(ok); 200 | raise_if_false(val == i); 201 | } 202 | 203 | // Pop the last pushed value 204 | ok = q.try_pop(val); 205 | raise_if_false(ok); 206 | raise_if_false(val == 999); 207 | 208 | // Push some values... 209 | q.push(42); 210 | q.push(57); 211 | 212 | // ...close the queue 213 | q.close(); 214 | 215 | // and check that we cannot push anything else 216 | try { 217 | q.try_push(47); 218 | raise_if_false(false); 219 | } catch (const ClosedQueue&) { 220 | raise_if_false(true); 221 | } 222 | 223 | // but we can pop until the queue gets empty 224 | val = q.pop(); 225 | raise_if_false(val == 42); 226 | 227 | val = q.pop(); 228 | raise_if_false(val == 57); 229 | try { 230 | q.try_pop(val); 231 | raise_if_false(false); 232 | } catch (const ClosedQueue&) { 233 | raise_if_false(true); 234 | } 235 | 236 | std::cout << "[OK] test_non_blocking_queue__value\n"; 237 | } 238 | 239 | void test_non_blocking_queue__ptr_void() { 240 | Queue q(QUEUE_MAXSIZE); 241 | void* val; 242 | bool ok; 243 | 244 | // We expect to be able to push N elements for a N limit 245 | // queue without trouble 246 | for (int i = 0; i < QUEUE_MAXSIZE; ++i) { 247 | val = new Value(i); 248 | ok = q.try_push(val); 249 | raise_if_false(ok); 250 | } 251 | 252 | // The N+1 element however, should fail 253 | val = new Value(999); 254 | ok = q.try_push(val); 255 | raise_if_false(!ok); 256 | delete (Value*)val; 257 | 258 | // A pop should work just fine, retrieving 259 | // the first pushed element. 260 | ok = q.try_pop(val); 261 | raise_if_false(ok); 262 | raise_if_false(*(Value*)val == 0); 263 | delete (Value*)val; 264 | 265 | // Now that we made room we can push one more element 266 | val = new Value(999); 267 | ok = q.try_push(val); 268 | raise_if_false(ok); 269 | 270 | // We expect to pop all the elements of the queue in 271 | // a FIFO order (in the loop we pop N-1 elements) 272 | for (int i = 1; i < QUEUE_MAXSIZE; ++i) { 273 | ok = q.try_pop(val); 274 | raise_if_false(ok); 275 | raise_if_false(*(Value*)val == i); 276 | delete (Value*)val; 277 | } 278 | 279 | // Pop the last pushed value 280 | ok = q.try_pop(val); 281 | raise_if_false(ok); 282 | raise_if_false(*(Value*)val == 999); 283 | delete (Value*)val; 284 | 285 | // Push some values... 286 | q.push(new Value(42)); 287 | q.push(new Value(57)); 288 | 289 | // ...close the queue 290 | q.close(); 291 | 292 | // and check that we cannot push anything else 293 | val = new Value(47); 294 | try { 295 | q.try_push(val); 296 | raise_if_false(false); 297 | } catch (const ClosedQueue&) { 298 | raise_if_false(true); 299 | } 300 | delete (Value*)val; 301 | 302 | // but we can pop until the queue gets empty 303 | val = q.pop(); 304 | raise_if_false(*(Value*)val == 42); 305 | delete (Value*)val; 306 | 307 | val = q.pop(); 308 | raise_if_false(*(Value*)val == 57); 309 | delete (Value*)val; 310 | 311 | try { 312 | q.try_pop(val); 313 | raise_if_false(false); 314 | } catch (const ClosedQueue&) { 315 | raise_if_false(true); 316 | } 317 | 318 | std::cout << "[OK] test_non_blocking_queue__ptr_void\n"; 319 | } 320 | 321 | void test_non_blocking_queue__ptr_value() { 322 | Queue q(QUEUE_MAXSIZE); 323 | Value *val; 324 | bool ok; 325 | 326 | // We expect to be able to push N elements for a N limit 327 | // queue without trouble 328 | for (int i = 0; i < QUEUE_MAXSIZE; ++i) { 329 | val = new Value(i); 330 | ok = q.try_push(val); 331 | raise_if_false(ok); 332 | } 333 | 334 | // The N+1 element however, should fail 335 | val = new Value(999); 336 | ok = q.try_push(val); 337 | raise_if_false(!ok); 338 | delete val; 339 | 340 | // A pop should work just fine, retrieving 341 | // the first pushed element. 342 | ok = q.try_pop(val); 343 | raise_if_false(ok); 344 | raise_if_false(*val == 0); 345 | delete val; 346 | 347 | // Now that we made room we can push one more element 348 | val = new Value(999); 349 | ok = q.try_push(val); 350 | raise_if_false(ok); 351 | 352 | // We expect to pop all the elements of the queue in 353 | // a FIFO order (in the loop we pop N-1 elements) 354 | for (int i = 1; i < QUEUE_MAXSIZE; ++i) { 355 | ok = q.try_pop(val); 356 | raise_if_false(ok); 357 | raise_if_false(*val == i); 358 | delete val; 359 | } 360 | 361 | // Pop the last pushed value 362 | ok = q.try_pop(val); 363 | raise_if_false(ok); 364 | raise_if_false(*val == 999); 365 | delete val; 366 | 367 | // Push some values... 368 | q.push(new Value(42)); 369 | q.push(new Value(57)); 370 | 371 | // ...close the queue 372 | q.close(); 373 | 374 | // and check that we cannot push anything else 375 | val = new Value(47); 376 | try { 377 | q.try_push(val); 378 | raise_if_false(false); 379 | } catch (const ClosedQueue&) { 380 | raise_if_false(true); 381 | } 382 | delete val; 383 | 384 | // but we can pop until the queue gets empty 385 | val = q.pop(); 386 | raise_if_false(*val == 42); 387 | delete val; 388 | 389 | val = q.pop(); 390 | raise_if_false(*val == 57); 391 | delete val; 392 | 393 | try { 394 | q.try_pop(val); 395 | raise_if_false(false); 396 | } catch (const ClosedQueue&) { 397 | raise_if_false(true); 398 | } 399 | 400 | std::cout << "[OK] test_non_blocking_queue__ptr_value\n"; 401 | } 402 | 403 | int main() try { 404 | test_non_blocking_queue__int(); 405 | test_non_blocking_queue__complex(); 406 | test_non_blocking_queue__value(); 407 | test_non_blocking_queue__ptr_void(); 408 | test_non_blocking_queue__ptr_value(); 409 | return 0; 410 | } catch (const std::exception& err) { 411 | std::cout << "Exception: " << err.what() << "\n"; 412 | return 1; 413 | } catch (...) { 414 | std::cout << "Unknown exception\n"; 415 | return 2; 416 | } 417 | -------------------------------------------------------------------------------- /12_how_to_close_a_queue.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | struct ClosedQueue : public std::runtime_error { 13 | ClosedQueue() : std::runtime_error("The queue is closed") {} 14 | }; 15 | 16 | class Queue { 17 | private: 18 | std::queue q; 19 | const unsigned int max_size; 20 | 21 | bool closed; 22 | 23 | std::mutex mtx; 24 | std::condition_variable is_not_full; 25 | std::condition_variable is_not_empty; 26 | 27 | public: 28 | Queue(const unsigned int max_size) : max_size(max_size), closed(false) {} 29 | 30 | 31 | bool try_push(const int& val) { 32 | std::unique_lock lck(mtx); 33 | 34 | /* 35 | * Si la queue esta cerrada no se aceptan más elementos 36 | * y try_push() y push() deben fallar. 37 | * 38 | * En esta implementación se lanza una excepción si la queue 39 | * esta cerrada; 40 | * en otras implementaciones se retorna via algún mecanismo 41 | * un bool o tienen un método is_closed() 42 | * 43 | * Python por ejemplo implementa ambos cosa pero realmente 44 | * is_closed() te puede llevar a una race condition. 45 | * 46 | * Por ejemplo, podrías pensar que esto es correcto: 47 | * 48 | * while (not blocking_queue.is_closed()) { 49 | * blocking_queue.try_push(...); 50 | * } 51 | * 52 | * Pero ese código **no** evitara q hagas un push() (o un pop()) 53 | * sobre una queue que esta cerrada. 54 | * 55 | * Pudiste ver la RC? 56 | * */ 57 | if (closed) { 58 | throw ClosedQueue(); 59 | } 60 | 61 | if (q.size() == this->max_size) { 62 | return false; 63 | } 64 | 65 | if (q.empty()) { 66 | is_not_empty.notify_all(); 67 | } 68 | 69 | q.push(val); 70 | return true; 71 | } 72 | 73 | bool try_pop(int& val) { 74 | std::unique_lock lck(mtx); 75 | 76 | /* 77 | * Tentador pero **no**!! 78 | * 79 | * Tanto try_pop() como pop() deben fallar si la queue 80 | * esta cerrada **y** esta vacía. 81 | * 82 | * Si la queue esta cerrada eso **no** implica que no 83 | * queden elementos **aun** en la queue y para 84 | * la perspectiva de cualquier thread consumidor 85 | * **aun** hay trabajo por hacer. 86 | * */ 87 | //if (closed) { 88 | // throw ClosedQueue(); 89 | //} 90 | 91 | if (q.empty()) { 92 | if (closed) { 93 | throw ClosedQueue(); 94 | } 95 | return false; 96 | } 97 | 98 | if (q.size() == this->max_size) { 99 | is_not_full.notify_all(); 100 | } 101 | 102 | val = q.front(); 103 | q.pop(); 104 | return true; 105 | } 106 | 107 | void push(const int& val) { 108 | std::unique_lock lck(mtx); 109 | 110 | if (closed) { 111 | throw ClosedQueue(); 112 | } 113 | 114 | while (q.size() == this->max_size) { 115 | is_not_full.wait(lck); 116 | } 117 | 118 | if (q.empty()) { 119 | is_not_empty.notify_all(); 120 | } 121 | 122 | q.push(val); 123 | } 124 | 125 | 126 | int pop() { 127 | std::unique_lock lck(mtx); 128 | 129 | while (q.empty()) { 130 | if (closed) { 131 | throw ClosedQueue(); 132 | } 133 | is_not_empty.wait(lck); 134 | } 135 | 136 | if (q.size() == this->max_size) { 137 | is_not_full.notify_all(); 138 | } 139 | 140 | const int val = q.front(); 141 | q.pop(); 142 | 143 | return val; 144 | } 145 | 146 | void close() { 147 | std::unique_lock lck(mtx); 148 | 149 | /* 150 | * Cerrar dos veces una queue no es ningún problema 151 | * realmente (al final es cambiar un booleano). 152 | * 153 | * Pero **lógicamente esta mal**: o hay algo mal programado 154 | * o hay algún bug que hizo q se llame a close() más de 155 | * una vez. 156 | * 157 | * Es como querer cerrar un file o hacer un free dos veces. 158 | * 159 | * Consejo: si sabes que hay un estado invalido, checkearlo 160 | * y fallar rápido. Hara que descubras bugs mucho más rápido. 161 | * */ 162 | if (closed) { 163 | throw std::runtime_error("The queue is already closed."); 164 | } 165 | closed = true; 166 | 167 | /* Probablemente estoy mintiendo aquí ya q no se si la queue 168 | * esta vacio o no realmente **pero** en el caso de que lo 169 | * este puede que algún thread este bloqueado en el pop(). 170 | * 171 | * Si es así este notify_all() lo va a despertar y ese thread 172 | * tendrá la oportunidad de ver q la queue esta cerrada y 173 | * q no debe continuar con el pop() 174 | * 175 | * */ 176 | is_not_empty.notify_all(); 177 | } 178 | 179 | private: 180 | Queue(const Queue&) = delete; 181 | Queue& operator=(const Queue&) = delete; 182 | 183 | }; 184 | 185 | 186 | namespace { 187 | const int MAX_NUM = 30; 188 | const int PROD_NUM = 10; 189 | const int CONS_NUM = 10; 190 | const int QUEUE_MAXSIZE = 10; 191 | } 192 | 193 | 194 | void sleep_a_little(std::default_random_engine& generator) { 195 | std::uniform_int_distribution get_random_int(100, 500); 196 | 197 | auto random_int = get_random_int(generator); 198 | auto milliseconds_to_sleep = std::chrono::milliseconds(random_int); 199 | std::this_thread::sleep_for(milliseconds_to_sleep); // sleep some "pseudo-random" time 200 | } 201 | 202 | void productor_de_numeros(Queue& q) { 203 | std::default_random_engine generator; 204 | 205 | for (int i = 0; i < MAX_NUM; ++i) { 206 | sleep_a_little(generator); 207 | /* 208 | * Debería poner un try-catch en caso q la queue este cerrada? 209 | * 210 | * En el caso de los productores (quienes hagan push) 211 | * "no" debería ser necesario. 212 | * 213 | * Si la queue esta cerrada y se hace un push, se va a 214 | * lanzar una excepción. Eso no cambia. 215 | * 216 | * Pero en un buen diseño las queues deberían cerrarse 217 | * **solo** si los productores terminaron. 218 | * 219 | * Cuando hay una sola queue para N productores 220 | * (como es este caso) el cierre de la queue la debe hacer 221 | * alguien de afuera (en este ejemplo lo hace el main()) 222 | * 223 | * Cuando la queue es una por cada productor, es el mismo 224 | * productor cuando termina quien cierra **su** queue. 225 | * 226 | * Por eso, 99.99% de las veces un productor debería 227 | * asumir que la queue (o "su" queue) esta abierta 228 | * siempre y sino, esta OK q se lance una excepción 229 | * por que realmente es algo *excepcional*. 230 | * */ 231 | q.push(1); 232 | } 233 | } 234 | 235 | void consumidor_de_numeros(Queue& q, int& resultado_parcial) { 236 | std::default_random_engine generator; 237 | 238 | int suma = 0; 239 | int n; 240 | do { 241 | // Al contrario de un productor, el consumidor no tiene 242 | // ni idea de cuando habrá o no más elementos. 243 | // 244 | // Recordad que una queue vacía no significa que no 245 | // habrá "nunca más" nada ahí. 246 | // 247 | // Solo al atrapar la excepción podemos saber que la 248 | // queue realmente *nunca más* tendrá elementos por 249 | // que la queue esta **cerrada y vacía**. 250 | try { 251 | n = q.pop(); 252 | } catch (const ClosedQueue&) { 253 | break; 254 | } 255 | suma += n; 256 | 257 | sleep_a_little(generator); 258 | } while (true); 259 | 260 | resultado_parcial = suma; 261 | } 262 | 263 | // Este main es igual q en 09_non_blocking_queue.cpp excepto 264 | // que ahora en vez de pushear valores dummy para hacer que los 265 | // consumidores finalicen, hacemos un close de la queue explicito. 266 | int main(int argc, char *argv[]) { 267 | Queue q(QUEUE_MAXSIZE); 268 | 269 | std::vector productores(PROD_NUM); 270 | std::vector consumidores(CONS_NUM); 271 | std::vector resultados_parciales(CONS_NUM); 272 | 273 | std::cout << "Lanzando " << CONS_NUM << " consumidores de numeros\n"; 274 | for (int i = 0; i < CONS_NUM; ++i) { 275 | consumidores[i] = std::thread(&consumidor_de_numeros, std::ref(q), std::ref(resultados_parciales[i])); 276 | } 277 | std::cout << "Lanzando " << PROD_NUM << " productores de numeros\n"; 278 | for (int i = 0; i < PROD_NUM; ++i) { 279 | productores[i] = std::thread(&productor_de_numeros, std::ref(q)); 280 | } 281 | 282 | std::cout << "Esperando a que los " << PROD_NUM << " productores terminen\n\n"; 283 | for (int i = 0; i < PROD_NUM; ++i) { 284 | productores[i].join(); 285 | } 286 | 287 | std::cout << "Los consumidores deben estar bloqueados en el pop de la queue\n"; 288 | std::cout << "Cerrando la queue\n\n"; 289 | q.close(); 290 | 291 | std::cout << "Esperando a que los " << CONS_NUM << " consumidores terminen\n\n"; 292 | int suma = 0; 293 | for (int i = 0; i < CONS_NUM; ++i) { 294 | consumidores[i].join(); 295 | suma += resultados_parciales[i]; 296 | } 297 | 298 | std::cout << "Se lanzaron " << PROD_NUM << " productores que cada uno creo " << MAX_NUM << " 'unos'\n"; 299 | std::cout << "Por lo tanto, la suma total deberia dar " << PROD_NUM * MAX_NUM << " y la suma efectivamente dio " << suma << "\n"; 300 | std::cout << ((PROD_NUM * MAX_NUM == suma)? "OK\n" : "FALLO\n"); 301 | return 0; 302 | } 303 | 304 | /* 305 | * Challenge: 306 | * 307 | * Cuando una queue es cerrada, los consumidores pueden (y deben) 308 | * seguir sacando elementos hasta que la queue esta vacía. 309 | * 310 | * Así uno puede asegurarse q todos los elementos que fueron pusheados 311 | * en algún momento van a ser procesados eventualmente. 312 | * 313 | * 99.99% es lo que necesitas pero.... Habrá situaciones excepcionales 314 | * en las que vas a querer "frenar" todo lo más rápido posible. 315 | * 316 | * Entonces, cerrar la queue no te va a alcanzar, vas a querer 317 | * cerrar la queue y *vaciarla* forzadamente cosa que los consumidores 318 | * terminen lo antes posible (a costa de dejar elementos sin procesar). 319 | * 320 | * Tu misión: modificar close() para que reciba un parámetro opcional 321 | * llamado drain, cuyo default es false. 322 | * 323 | * Si drain es true, close() debe cerrar la queue y luego vaciarla 324 | * completamente. 325 | * 326 | * 327 | * (Challenge)^2: 328 | * 329 | * En ciertas ocasiones hay N productores q pushean datos a una queue y 330 | * no esta claro cuando la queue hay q cerrarla. 331 | * 332 | * No hay ningún otro thread q detecte el fin de los productores y pueda 333 | * llamar a close() (como lo esta haciendo main() en este ejercicio). 334 | * 335 | * En ese caso se podría modificar la queue para que tenga un contador 336 | * - la queue lo inicializa con la cantidad de productores N 337 | * - cada llamada a close() decrementa en 1 ese contador y si es cero hace realmente 338 | * el close. 339 | * 340 | * Así, N productores llaman a close() y solo el último realmente cerrara la queue. 341 | * 342 | * Notar q este esquema es solo valido cuando N es fijo, fijate 343 | * si ves cual seria el issue si permitís q N sea dinámico. 344 | * */ 345 | 346 | -------------------------------------------------------------------------------- /11_blocking_queue_with_conditional_variables.cpp: -------------------------------------------------------------------------------- 1 | /* [1] 2 | Implementación de una queue protegida (thread safe) 3 | y bloqueante en su version con conditional variables. 4 | 5 | Mientras ejecutas el ejemplo, ejecuta 'top' en otra consola 6 | y observa el uso de la CPU. 7 | 8 | Si no le pifie en nada deberías que la CPU no se 9 | te prende fuego! 10 | 11 | **/ 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | /* [2] Blocking Queue 23 | 24 | Una queue bloqueante debe implementar: 25 | - un pull (remover de la queue) que se bloquee si la 26 | queue esta vacía. 27 | - (opcionalmente) un push (poner en la queue) que 28 | se bloquee si la queue esta llena. 29 | (aka BoundedQueue) 30 | 31 | El mecanismo de bloqueo se hara con *condition variables*. 32 | 33 | Salvando algunos detalles de implementación, lenguajes 34 | como Python y Ruby proveen blocking queues como esta. 35 | 36 | En Golang, hay lo que se llaman "channels" y que son, 37 | en esencia, equivalentes a una blocking queue. 38 | **/ 39 | class Queue { 40 | private: 41 | std::queue q; 42 | const unsigned int max_size; 43 | 44 | // [3] Observa: 45 | // - hay 1 mutex por que la queue *es* un recurso compartido 46 | // - hay 2 conditional variables por q el push() va a esperar 47 | // q la queue este no-llena y el pop() va a esperar 48 | // q la queue este no-vacía. 49 | // O sea hay 2 conditional variables por que hay **2 condiciones** 50 | std::mutex mtx; 51 | std::condition_variable is_not_full; 52 | std::condition_variable is_not_empty; 53 | 54 | public: 55 | Queue(const unsigned int max_size) : max_size(max_size) {} 56 | 57 | 58 | // [4] 59 | // 60 | // Tanto try_push() como try_pop() siguen siendo no-bloqueantes 61 | // como en el ejercicio 09_non_blocking_queue.cpp y 62 | // 10_blocking_queue_with_busy_wait_and_polling.cpp 63 | // 64 | // Pero ahora sabemos que habrá threads bloqueados en push() 65 | // y pop() a la espera de que la queue este no-llena y no-vacía 66 | // respectivamente. 67 | // 68 | // Entonces tenemos la obligación de **notificarles** 69 | // 70 | // El try_push() y push() deberán notificar cuando la queue 71 | // deje de estar no-vacía (por que pushearon un elemento) 72 | // 73 | // El try_pop() y pop() deberán notificar cuando la queue 74 | // deje de estar no-llena (por que retiraron un elemento) 75 | bool try_push(const int& val) { 76 | std::unique_lock lck(mtx); 77 | if (q.size() == this->max_size) { 78 | return false; 79 | } 80 | 81 | if (q.empty()) { 82 | /* [5] 83 | La queue esta vacía por lo que este 84 | push hara que la queue tenga un 85 | elemento y por lo tanto deje de 86 | estar vacía. 87 | 88 | Como puede haber hilos esperando a que 89 | la queue no este vacía, despertamos a todos 90 | ellos enviándoles una señal con el método 91 | notify_all(). 92 | 93 | Esto es: todo hilo que este "esperando" (wait) 94 | la *condición* "is_not_empty" se va a despertar. 95 | 96 | Como este hilo **aun** tiene adquirido 97 | el mutex (lock), los otros hilos que 98 | se despierten no ejecutaran nada hasta 99 | que liberemos nosotros el mutex lo que 100 | nos garantiza que no habrá race 101 | conditions. 102 | 103 | Las conditional variables tienen 2 métodos 104 | para señalizar: 105 | 106 | - notify_all 107 | - notify_one 108 | 109 | El primero le avisa a todos los threads, el segundo 110 | solo a uno. 111 | 112 | Es tentador llamar a notify_one por performance 113 | pero es super tricky no caer en un deadlock 114 | y más aun el OS no suele implementar correctamente 115 | la semántica "one". 116 | 117 | Por lo que recomendamos **siempre** usar notify_all() 118 | 119 | Ver también [7] que es la otra parte de la 120 | ecuación donde se hace el `wait` en el pop() 121 | 122 | Vas a ver este notify_all() también en push() 123 | **/ 124 | is_not_empty.notify_all(); 125 | } 126 | 127 | q.push(val); 128 | return true; 129 | } 130 | 131 | bool try_pop(int& val) { 132 | std::unique_lock lck(mtx); 133 | if (q.empty()) { 134 | return false; 135 | } 136 | 137 | if (q.size() == this->max_size) { 138 | /* 139 | [6] 140 | 141 | Al igual que en [5], tanto el try_pop() como el pop() 142 | seguro que hara que la queue pase a estar no-llena, 143 | así que tienen la obligación de notificarles 144 | a quienes estén esperando la condición (que sera push()) 145 | **/ 146 | is_not_full.notify_all(); 147 | } 148 | 149 | val = q.front(); 150 | q.pop(); 151 | return true; 152 | } 153 | 154 | // Al igual que en 10_blocking_queue_with_busy_wait_and_polling.cpp 155 | // push() y pop() son bloqueantes pero en vez de una busy wait 156 | // usaremos conditional variables. 157 | void push(const int& val) { 158 | std::unique_lock lck(mtx); 159 | 160 | 161 | /* [7] 162 | Si la queue esta llena, no podemos 163 | hacer un push. 164 | 165 | En vez de retornar con un 166 | código de error esperamos 167 | a que la queue deje de estar llena 168 | con el método wait(). 169 | 170 | Literalmente este hilo deja de 171 | ejecutarse a la 172 | **espera de recibir una señal**. 173 | 174 | **A la espera de que se de una condición** 175 | (de ahí el nombre "conditional variable") 176 | 177 | Señal que debería llegarnos cuando 178 | se cumpla la condición y la 179 | queue no este llena (alguien hizo un pop() o try_pop()). 180 | 181 | Sin embargo como pueden haber 182 | otros hilos **también** haciendo push, 183 | es posible que para cuando este hilo en 184 | particular se despierte la queue vuelva 185 | a estar llena. 186 | 187 | *Algún otro push() nos gano de mano!!* 188 | 189 | Por eso tenemos un **loop** 190 | y mientras este llena seguiremos 191 | haciendo waits. 192 | 193 | Aunque estemos haciendo un loop fijate que esto 194 | es mucho más eficiente que hacer un mero polling 195 | como en 10_blocking_queue_with_busy_wait_and_polling.cpp 196 | ya que al debloquearnos tenemos muchas chances de 197 | que podamos realizar el push(). 198 | 199 | Detalle oscuro: 200 | 201 | Dependiendo de la implementación 202 | que haga el sistema operativo de las 203 | conditional variables, algunas implementaciones 204 | pueden generar "señales espurias" en las que 205 | un hilo que esta esperando (wait) se despierte 206 | sin que otro hilo haya hecho una señal (notify) 207 | real. 208 | 209 | Esta es **otra razón** para tener el **loop**. 210 | 211 | Y que hace exactamente un wait() ? 212 | 213 | Lo primero que hace es liberar el mutex que 214 | **debe** estar previamente tomado y luego 215 | pone a dormir el thread. 216 | 217 | Al liberar el mutex le va a permitir a otros 218 | threads meterse en el pop/push. 219 | 220 | Cuando la señal llegue `wait()` va a retomar 221 | el mutex: 222 | 223 | - si lo puede retomar (lock()), wait() retorna 224 | - sino, se queda bloqueado hasta q lo pueda tomar 225 | (como pasa con un mutex tradicional) 226 | 227 | Resume: 228 | - wait debe siempre llamarse con un lock ya tomado 229 | - debe haber siempre un loop para recheckear en caso 230 | de un "despertar espurio" 231 | 232 | **/ 233 | while (q.size() == this->max_size) { 234 | is_not_full.wait(lck); 235 | } 236 | 237 | if (q.empty()) { 238 | // lo mismo que esta en try_push() 239 | // le notificamos a quienes estén esperando 240 | // "is_not_empty" 241 | is_not_empty.notify_all(); 242 | } 243 | 244 | q.push(val); 245 | } 246 | 247 | 248 | int pop() { 249 | std::unique_lock lck(mtx); 250 | 251 | // [8] 252 | // 253 | // Al igual que push() espera a que la queue este no-llena, 254 | // el pop() espera a que este no-vacía. 255 | // 256 | // Por las mismas razones hacemos un **loop** y un wait() 257 | // sobre la conditional variable "is_not_empty" 258 | while (q.empty()) { 259 | is_not_empty.wait(lck); 260 | } 261 | 262 | if (q.size() == this->max_size) { 263 | // Igual que en try_pop(), le notificamos a quienes 264 | // estén bloqueados esperando en "is_not_full" 265 | // que la queue ya no esta llena. 266 | is_not_full.notify_all(); 267 | } 268 | 269 | const int val = q.front(); 270 | q.pop(); 271 | 272 | return val; 273 | } 274 | 275 | private: 276 | Queue(const Queue&) = delete; 277 | Queue& operator=(const Queue&) = delete; 278 | 279 | }; 280 | 281 | 282 | namespace { 283 | const int MAX_NUM = 30; 284 | const int PROD_NUM = 10; 285 | const int CONS_NUM = 10; 286 | const int QUEUE_MAXSIZE = 10; 287 | } 288 | 289 | 290 | void sleep_a_little(std::default_random_engine& generator) { 291 | std::uniform_int_distribution get_random_int(100, 500); 292 | 293 | auto random_int = get_random_int(generator); 294 | auto milliseconds_to_sleep = std::chrono::milliseconds(random_int); 295 | std::this_thread::sleep_for(milliseconds_to_sleep); // sleep some "pseudo-random" time 296 | } 297 | 298 | void productor_de_numeros(Queue& q) { 299 | std::default_random_engine generator; 300 | 301 | for (int i = 0; i < MAX_NUM; ++i) { 302 | sleep_a_little(generator); 303 | q.push(1); 304 | } 305 | } 306 | 307 | void consumidor_de_numeros(Queue& q, int& resultado_parcial) { 308 | std::default_random_engine generator; 309 | 310 | int suma = 0; 311 | int n; 312 | do { 313 | n = q.pop(); 314 | suma += n; 315 | 316 | sleep_a_little(generator); 317 | } while (n != 0); 318 | 319 | resultado_parcial = suma; 320 | } 321 | 322 | // Este main es igual q en 09_non_blocking_queue.cpp 323 | // No hay nada nuevo aquí 324 | int main(int argc, char *argv[]) { 325 | Queue q(QUEUE_MAXSIZE); 326 | 327 | std::vector productores(PROD_NUM); 328 | std::vector consumidores(CONS_NUM); 329 | std::vector resultados_parciales(CONS_NUM); 330 | 331 | std::cout << "Lanzando " << CONS_NUM << " consumidores de numeros\n"; 332 | for (int i = 0; i < CONS_NUM; ++i) { 333 | consumidores[i] = std::thread(&consumidor_de_numeros, std::ref(q), std::ref(resultados_parciales[i])); 334 | } 335 | std::cout << "Lanzando " << PROD_NUM << " productores de numeros\n"; 336 | for (int i = 0; i < PROD_NUM; ++i) { 337 | productores[i] = std::thread(&productor_de_numeros, std::ref(q)); 338 | } 339 | 340 | std::cout << "Esperando a que los " << PROD_NUM << " productores terminen\n\n"; 341 | for (int i = 0; i < PROD_NUM; ++i) { 342 | productores[i].join(); 343 | } 344 | 345 | std::cout << "Los consumidores deben estar bloqueados en el pop de la queue\n"; 346 | std::cout << "Enviando (push) " << CONS_NUM << " ceros para que cada consumidor lo saque de la queue y finalice.\n\n"; 347 | for (int i = 0; i < CONS_NUM; ++i) { 348 | q.push(0); 349 | } 350 | 351 | std::cout << "Esperando a que los " << CONS_NUM << " consumidores terminen\n\n"; 352 | int suma = 0; 353 | for (int i = 0; i < CONS_NUM; ++i) { 354 | consumidores[i].join(); 355 | suma += resultados_parciales[i]; 356 | } 357 | 358 | std::cout << "Se lanzaron " << PROD_NUM << " productores que cada uno creo " << MAX_NUM << " 'unos'\n"; 359 | std::cout << "Por lo tanto, la suma total deberia dar " << PROD_NUM * MAX_NUM << " y la suma efectivamente dio " << suma << "\n"; 360 | std::cout << ((PROD_NUM * MAX_NUM == suma)? "OK\n" : "FALLO\n"); 361 | return 0; 362 | } 363 | 364 | /* [9] 365 | * Challenge: modifica esta Queue para q si el max_size es 0 *no* bloquee 366 | * al hacer un push() y que el try_push() nunca falle. 367 | * 368 | * También deberías deshabilitar si max_size es 0 los notify_all en pop() 369 | * y try_pop(). 370 | * 371 | * Dicha implementación seria una UnboundedQueue (queue sin limites). 372 | * 373 | * Muchas implementación (recuerdo la de Python) ofrecen una única 374 | * Queue que según el parámetro max_size se comportan como una bounded 375 | * o unbounded queue. 376 | * 377 | * Donde se usan las UnboundedQueue? 378 | * 379 | * En ciertas aplicaciones en las q se desea q la aplicación 380 | * no se bloque a costa de consumir toda la memoria ram de la computadora. 381 | * 382 | * Por supuesto q una "queue sin limites" es solo una ilusión: eventualmente 383 | * si no se hacen pops la queue se llena y revienta la memoria ram. 384 | * 385 | * Que lindo es despertase a las 4AM por una alerta en tu celular 386 | * con un OutOfMemory Error :D 387 | * */ 388 | 389 | --------------------------------------------------------------------------------