Download as pdf or txt
Download as pdf or txt
You are on page 1of 12

1 # ==================== S01 DISCUSSION ====================

2
3 '''
4 print("hello")
5
6 Sequential computing
7 - traditional approach
8 - is the type of computing where one instruction is given at a particular and then
the next instruction has to wwaut for the first instruction to be execute
9 - linear sequence
10 Parallel computing
11 - a type of computing where many calculation or the execution of the process are
carried out parallelly or simulataneosly
12 - increase the speed and efficiency of computing
13
14 Parallel architectures
15
16 #linear sequence
17 start = 1 #start of the sequence
18 end = 5 #end of the sequence
19
20 for i in range(start, end - 1):
21 print(i)
22
23 '''
24
25 # multiprocessing
26 import multiprocessing
27
28 # define a function to be run by each process
29 def worker(start, end):
30 for i in range(start, end + 1):
31 print(i)
32
33 # worker(1, 5)
34
35 # define the main process
36 ''''
37 if __name__ == '__main__'
38 - block of code is used to ensure that the code inside it, it only run when the
script is run directly and not when it is imported as a module. This is necessary to
avoid errors when using multiprocessing.
39 '''
40
41 if __name__ == '__main__':
42 # define the number of process
43 num_processes = 4
44
45 # define the range of numbers to be processed
46 start = 1
47 end = 100
48
49 # calculate the range of numbers each process will handle
50 range_size = (end - start + 1) // num_processes
51 ranges = [(i * range_size + 1, (i+1) * range_size) for i in range(num_processes)]
52 if ranges[-1][1] < end:
53 ranges[-1] = (ranges[-1][0],end)
54
55 # create a pool of processes and start them
56 pool = multiprocessing.Pool(processes = num_processes)
57 results = [pool.apply_async(worker, r) for r in ranges]
58 # use 'apply_async' to start each process running the 'worker' function on its
assigned chunk range
59
60 '''
61 worker' function simply prints out each number in its assigned range, so that the
output will be the same as if the numbers were processed sequentially. However, by
dividing the work among multiple process, the program can take advantage of parallel
computing to process the numbers more quickly.
62 '''
63
64 # wait for all process to finish
65 for result in results:
66 result.get()
67
68 '''
69 Flynn's Taxonomy
70 - is a classification system for computer architecture, it was proposed by Michael J.
Flynn in 1966. computer
71 - it categorizes comp[uter architectures into 4 classes based on the number of
instructions streams(or threads) and data streams that can be processed simultaneously
by the processor.
72 - 4 classes:
73 1. SISD
74 - Single Instruction Single Data
75 - this is the simplest and most common type of computer architecture. It operate
on a single instruction stream and a single data stream at a time.
76 - example: tradition desktop and laptop
77 2. SIMD
78 - Single Instruction Multiple Data
79 - this architecture operates on a single instruction stream but multiple data
stream simultaneously.
80 - image and video processing
81 - example: GPUs graphic processing units
82 3. MISD -
83 - Multiple Instructions Single Data
84 - this architecture operates on multiple instruction streams but a single data
stream at a time
85 - MISD machines are rate and not widely used in practice
86 4. MIMD -
87 - Multiple Instructions Multiple Data
88 - this architecture operates on multiple instructions streams and multiple data
streams simultaneously.
89 - 2 Categories of MIMD:
90 1. Shared-memory
91 - all processors share a common memory
92 2. Distributed-memory
93 - each processor has its own local memory
94 - example: worstations, supercomputers
95 workstations
96 - Different Types of Parallel Computing
97 - Shared Memory
98 - involves single memory space that is accessible by all processor memory can
read from and write to the same memory location
99 - example: multi-core processor and symmetric multiprocessing systems (SMP)
100
101 -Distributed Memory
102 - multiple processor or nodes each with their own local memory processor
103 '''
104
105 # a program that is designed to simulate Flynn's taxonomy - which is a classification
system for computer architecture based on the number of isntructions and data stream
that processed simulataneously
106 # this is a python program that simulates the processing of multiple instrcutions on
multiple data items by creating a set of processors that execute each instruction on
data. Then the program counts the number of instructions executed and data items
accessed to determine the level of Flynn's taxonomy that the processors fall into
107
108 '''
109 # use to generate random numbers
110 import random
111
112 # define the number of instructions and data elements
113 NUM_INSTRUCTIONS = 100
114 NUM_DATA_ELEMENTS = 1000
115
116 #contains the set of instructions that the processors can execute
117
118 #contains the set of data elements that the processors will operate on, random.randint()
function is used to generate random integers between 0 and 100, range() function is used
to specify the number of data elements to generate
119 DATA_SET = [random.randint(0, 100) for i in range(NUM_DATA_ELEMENTS)]
120
121 # define the processor class
122
123 class Processor:
124 #__init__ this method initializes a processor object with an instruction set, data
set, an array of 10 registers and a program counter (pc) initialized to 0.
125
126 def __init__(self, instruction_set, data_set):
127 self.instruction_set = instruction_set
128 self.data_set = data_set
129 self.registers = [0] * 10
130 self.pc = 0
131
132 # execute() this method randomly selects instruction from the intruction, randomly
selects an operand from either the register or data set depending on the
instruction, performs the operation specified by the instruction on the operand and
updates registers and data set as necessary
133
134 def execute(self):
135 instruction = self.instruction_set[self.pc % len(self.instruction_set)]
136
137 operand1 = random.randint(0, len(self.registers) - 1)
138 operand2 = random.choice([random.randint(0, len(self.registers) - 1),
random.randint(0, len(self.data_set) - 1)])
139
140 if instruction == "ADD":
141 self.registers[operand1] += operand2
142 elif instruction == "SUB":
143 self.registers[operand1] -= operand2
144 elif instruction == "MUL":
145 self.registers[operand1] *= operand2
146 elif instruction == "DIV":
147 if operand2 == 0:
148 self.registers[operand1] = 0
149 else:
150 self.registers[operand1] //= operand2
151 elif instruction == "LOAD":
152 self.registers[operand1] = self.data_set[operand2]
153 elif instruction == "STORE":
154 self.data_set[operand2] = self.registers[operand1]
155 elif instruction == "JUMP":
156 self.pc = operand2
157 elif instruction == "BRANCH":
158 if self.registers[operand1] > 0:
159 self.pc = operand2
160 self.pc += 1
161
162 # Define the main function
163 def main():
164 processors = [Processor(INSTRUCTION_SET, DATA_SET) for i in range(10)]
165 for i in range(NUM_INSTRUCTIONS):
166 for processor in processors:
167 processor.execute()
168 instruction_count = NUM_INSTRUCTIONS * len(processors)
169 data_count = NUM_DATA_ELEMENTS * len(processors)
170 print("Number of instructions executed: {}".format(instruction_count))
171 print("Number of data elements accessed: {}".format(data_count))
172
173 # Call the main function
174 if __name__ == "__main__":
175 main()
176
177 '''
178
179 '''
180 mini-activity:
181 Create a python program that implements Flynn's taxonomy will execute a sequence of
instructions defined in instruction set to perform arithmetic operations and manipulate
data from a data set.
182
183 In this program, we define a Processor class that simulates a processor with an
instruction set and a data set. The execute() method randomly selects an instruction
from the instruction set and randomly selects operands from either the registers or the
data set depending on the instruction, performs the operation specified by the
instruction on the operands, and updates the registers and data set as necessary.
184
185 To solve the problem we use the Processor. In this case, the problem is to load the
value at index 2 of the data set into register 0, add the value at index 3 of the data
set to register 0, and store the result back into index 2 of the data set. If the value
in register 0 is positive, we should jump to the beginning of the instruction set and
repeat the process. If the value in register 0 is not positive, we should exit the loop
and print the final state of the data set.
186
187 Finally, we solve the problem by repeatedly calling the execute() method on our
Processor object until we reach the end of the instruction set. We then print the final
state of the data set to see the result of our computation.
188
189 The final output will look like something like this:
190 [0, 5, 7, 8]
191
192 Note: The exact values will depend on the random numbers generated during the execution
193
194 use this data set:
195 data_set = [3, 5, 7, 8]
196
197 '''
198
199 '''
200 # use to generate random numbers
201 import random
202
203 # define the number of instructions and data elements
204 NUM_INSTRUCTIONS = 100
205 NUM_DATA_ELEMENTS = 1000
206
207 #contains the set of instructions that the processors can execute
208
209 #contains the set of data elements that the processors will operate on, random.randint()
function is used to generate random integers between 0 and 100, range() function is used
to specify the number of data elements to generate
210 DATA_SET = [random.randint(0, 100) for i in range(NUM_DATA_ELEMENTS)]
211
212 # define the processor class
213
214 class Processor:
215 #__init__ this method initializes a processor object with an instruction set, data
set, an array of 10 registers and a program counter (pc) initialized to 0.
216
217 def __init__(self, instruction_set, data_set):
218 self.instruction_set = instruction_set
219 self.data_set = data_set
220 self.registers = [0] * 10
221 self.pc = 0
222
223 # execute() this method randomly selects instruction from the intruction, randomly
selects an operand from either the register or data set depending on the
instruction, performs the operation specified by the instruction on the operand and
updates registers and data set as necessary
224
225 def execute(self):
226 instruction = self.instruction_set[self.pc % len(self.instruction_set)]
227
228 operand1 = random.randint(0, len(self.registers) - 1)
229 operand2 = random.choice([random.randint(0, len(self.registers) - 1),
random.randint(0, len(self.data_set) - 1)])
230
231 if instruction == "ADD":
232 self.registers[operand1] += operand2
233 elif instruction == "SUB":
234 self.registers[operand1] -= operand2
235 elif instruction == "MUL":
236 self.registers[operand1] *= operand2
237 elif instruction == "DIV":
238 if operand2 == 0:
239 self.registers[operand1] = 0
240 else:
241 self.registers[operand1] //= operand2
242 elif instruction == "LOAD":
243 self.registers[operand1] = self.data_set[operand2]
244 elif instruction == "STORE":
245 self.data_set[operand2] = self.registers[operand1]
246 elif instruction == "JUMP":
247 self.pc = operand2
248 elif instruction == "BRANCH":
249 if self.registers[operand1] > 0:
250 self.pc = operand2
251 self.pc += 1
252
253 # Define the main function
254 def main():
255 processors = [Processor(INSTRUCTION_SET, DATA_SET) for i in range(10)]
256 for i in range(NUM_INSTRUCTIONS):
257 for processor in processors:
258 processor.execute()
259 instruction_count = NUM_INSTRUCTIONS * len(processors)
260 data_count = NUM_DATA_ELEMENTS * len(processors)
261 print("Number of instructions executed: {}".format(instruction_count))
262 print("Number of data elements accessed: {}".format(data_count))
263
264 # Call the main function
265 if __name__ == "__main__":
266 main()
267
268 '''
269
270 '''
271 mini-activity:
272 Create a python program that implements Flynn's taxonomy will execute a sequence of
instructions defined in instruction set to perform arithmetic operations and manipulate
data from a data set.
273
274 In this program, we define a Processor class that simulates a processor with an
instruction set and a data set. The execute() method randomly selects an instruction
from the instruction set and randomly selects operands from either the registers or the
data set depending on the instruction, performs the operation specified by the
instruction on the operands, and updates the registers and data set as necessary.
275
276 To solve the problem we use the Processor. In this case, the problem is to load the
value at index 2 of the data set into register 0, add the value at index 3 of the data
set to register 0, and store the result back into index 2 of the data set. If the value
in register 0 is positive, we should jump to the beginning of the instruction set and
repeat the process. If the value in register 0 is not positive, we should exit the loop
and print the final state of the data set.
277
278 Finally, we solve the problem by repeatedly calling the execute() method on our
Processor object until we reach the end of the instruction set. We then print the final
state of the data set to see the result of our computation.
279
280 The final output will look like something like this:
281 [0, 5, 7, 8]
282
283 Note: The exact values will depend on the random numbers generated during the execution
284
285 use this data set:
286 data_set = [3, 5, 7, 8]
287
288 '''
289
290
291 # ==================== S02 DISCUSSION ====================
292 '''
293 May 06, 2023
294
295 Threads, Processes and Mutual Exclusion
296
297 Process
298 - is a standalone program that runs in its own memory
299 -- instance of program
300 refers to execution of the program code in a computer syustem. When a program
executed, it create an instance of itself in the memory system
301 * the 'instance' contains the program code, data, and resources needed to
execute the program
302 Threads
303 - is a subset of process that shared the same memory
304 - the basic unit that the operating system manages and it allocates time on the
processor to actually execute them.
305 |----------- Process ------------|
306 __________________________________
307 | Thread 1 | Thread 2 | Thread 3 |
308
309
310 '''
311 # import sleep() and perf_counter() functions from time module
312 from time import sleep, perf_counter
313
314 def task():
315 print('Starting a task ---> ')
316 sleep(1)
317 print('<--- task is Done.')
318 sleep(1)
319
320 start_time = perf_counter()
321
322 task()
323 task()
324
325 end_time = perf_counter()
326 print(f'It took {end_time- start_time: 0.2f} second(s) to complete.')
327
328 '''
329 task <-- 1 second --> task <-- 1 second --> done
330 '''
331
332 # Multi-threaded
333
334 from time import sleep, perf_counter
335 from threading import Thread
336 # import the thread class from the threading module
337
338 # create a new thread by instantiating an instance of the Thread class
339
340 # new_thread = Thread(target = fn, args = args_tuple)
341
342 '''
343 Thread() accepts 2 parameter:
344 - target - specifies a function(fn) to run the new thread
345 - args - specifies the arguments of the function(fn)
346 The arguments in tuple
347
348 --- tuple
349 - is an ordered, immutable sequence of element of different data types
350 - similar to lists, but they are immutable, which that their contents cannot modified
351 '''
352
353 def task():
354 print('Starting a task ---> ')
355 sleep(1)
356 print('<--- task is Done.')
357
358 start_time = perf_counter()
359
360 # Create two new threads
361 t1 = Thread(target=task)
362 t2 = Thread(target=task)
363
364 # start the threads
365 t1.start()
366 t2.start()
367
368 # wait for the threads to complete - join() by this function, the main thread will wait
for the second thread to complete before it terminated
369 t1.join()
370 t2.join()
371
372 end_time = perf_counter()
373 print(f'It took {end_time- start_time: 0.2f} second(s) to complete.')
374
375 '''
376 # tuple - creating tuple
377 my_tuple = (1, 2, "three", 4.0)
378
379 print(my_tuple[2])
380
381 # iterate
382 for item in my_tuple:
383 print(item)
384 '''
385
386 '''
387 <--1 second -->
388 task|--task--|<-- 1 second -->--|done
389
390 [Section] Mutual Exclusion
391 - refers to the concept of ensuring that only one thread can access a shared resource at
a time
392 --- techniques
393 - lock
394 - synchronization primitive that used to enforce mutual exclusion by
allowing only one thread to acquire a lock at time
395 - semaphores
396 - similar to lock, but it allows multiples threads to access a shared
resources simultaneously, up to a certain limit
397 - mutexes
398 - mutual exclusion, a type of lock that used to ensure that only one thread
can access shared resource at a time
399 - locked
400 - unlocked
401
402 '''
403
404 # May 05, 2023 [Tuesday]
405
406 # threading.Lock class to create and use locks
407 # a thread of execution in a computer program
408 # main thread - one thread of execution
409
410 # race condition - a concurrency failure case when the two thread two thread run the
same code and access or update the same resources (data variables, stream) leaving the
resource in unknown or inconsistent state.
411
412 '''
413 unlocked
414 - the lock has not been acquired and can be acquired by the next thread that makes
an attempt
415
416 locked
417 - the lock has been acquired by one thread and any thread that makes attempt to
acquoired it must wait until it is released.
418
419 locks are created in the unlocked state
420
421 threading.Lock
422 - an instance of lock that can be created and then acquired by threads before
accessing a critical session and released after the critical session
423
424 - create a lock
425 lock = Lock()
426 - acquire the lock
427 lock.acquire(blocking = false)
428 lock.acquire(timeout = 10)
429 - release the lock
430 lock.release()
431 '''
432
433 from time import sleep
434 from random import random
435 from threading import Thread, Lock
436
437 # working function
438 def task(lock, identifier, value):
439 # acquire the lock
440 with lock:
441 print(f'>thread {identifier} got the lock, sleeping value {value}')
442 sleep(value)
443
444
445 # create a share lock
446 lock = Lock()
447
448 # start a few threads that attempt to execute the critical section
449 for i in range(10):
450 Thread(target=task, args=(lock, i, random())).start()
451 # function is used to generate the sleep time for each thread
452
453 # wait for all the threads to finish
454
455
456 # ==================== S03 DISCUSSION ====================
457 ''' locksLivenessDiscussion.txt
458 Lock and Liveness
459
460 - Reentrant Lock
461 - a reentrant lock also known as recursive lock or recursive mutex
462 - it allows a thread to acqure the same lock multiple times without causing a
deadlock. It maintain a count of the number if times if has been acquired and
ensures that the thread that acquired the lock must release it the same number
463
464 count() {
465 lock()
466 counter++
467 unlock()
468 }
469
470 def function(name):
471 with lock:
472 print(f'{name} acquired the lock.')
473 count(name)
474 def count(name):
475 with lock:
476 print(f'{name} acquired the lock again')
477
478 thread1 = threading.Thread(target=function, args=('Thread 1'))
479 thread2 = threading.Thread(target=function, args=('Thread 2'))
480
481 Output:
482 Thread 1 acquired the lock.
483 Thread 1 acquired the lock again.
484 Thread 2 acquired the lock.
485 Thread 2 acquired the lock again.
486
487
488 - Try Lock
489 - a try lock allows a thread to attempt acquiring a lock and it return immediately
with a boolean value indivating whether the lock is acquired or not. This is useful
when we want to acquire a lock it its available and perform an alternative action if
it's not
490
491 database record - updating user 1 - ensure only one thread at a time
492 user 2
493 user 3
494
495 lock = threading.Lock()
496 database_record = {
497 "id": 101,
498 "name": "Juan Dela Cruz",
499 "age": 16
500 }
501
502 def update_database)record(new_name):
503 if lock.acquire(blocking = False):
504 try:
505 print("Updating database record. . .")
506 database_record["name"] = new_name
507 print("Database record updated: ", database_record)
508 finally:
509 lock.release()
510 else:
511 print("Unable to acquire the lock. Performing alternative action.")
512
513 thread1 = threading.Thread(target=function, args=('Perla Bautista'))
514 thread2 = threading.Thread(target=function, args=('Tomas Morato'))
515
516 # the try mechanism allows thread to check if the lock is available without waiting and
decide on an appropriate course of action
517
518 Output
519 Updating database . . .
520 Database record updated:{"id": 101, "name": "Juan Dela Cruz", "age":16}
521 Unable to acquire the lock. Performing alternative action.
522
523 - Read-write lock
524
525 Mini-Activity: Concurrent Counter
526 Create a python program that will implement a concurrent cunter that allows multiple
threads to increment and decretement its value safely using a reentrant lock
527
528 Instructions:
529 - Create a variable called 'counter' and initialize 0
530 - Create a reentrant lock using the function
531 '''
532
533
534 # ==================== S04 DISCUSSION ====================
535 '''
536 DeadLock
537 - Deadlock is a situation that occurs in concurrent programming when two or more
processes or threads are unable to proceed because each is waiting for the other to
release resource or take some action.
538
539 Dining Philosophers Problem
540 baby boy 1
541 baby boy 2
542 ----> center table <---- 5 fork ----> 2 fork for each Philospher
543 time thinking and eating shortlisted 5 forks
544 Gymuel
545 Abaya
546 Odoño
547
548 [1] fork 1 ---> default
549 [2] fork 2
550 [3] fork 3
551 [4] fork 4
552 [5] fork 5
553
554 1. each philosopher should sit and think until get hungry
555 2. when a philosopher gets hungry, they need to pick up the fork on their left and the
fork on their right to start eating
556 3. However, the philosopher are polite and don't want to grab a fork if their
neighboring philosopher is already using it.
557 4. So, the philosopher need to wait until both forks they require are available
558 5. if two philosopher simultaneously get hungry and try to grab their respective forks
at the same time, they might end up in a deadlock
559
560 - semaphores or implementing rules to ensure fairness in resource allocation
561 '''
562
563 import threading
564 import time
565
566
567 class Philosopher(threading.Thread):
568 running = True
569 num_philosophers = 5
570 # mutex - lock is used to ensure that only one attempt to acquire the forks at a
time, preventing conflicts and maintaining exclusivity
571 forks = [threading.Lock() for _ in range(num_philosophers)]
572 # added 1
573 mutex = threading.Lock()
574
575 def __init__(self, index):
576 threading.Thread.__init__(self)
577 self.index = index
578
579 def think(self):
580 print(f"Philosopher {self.index} is thinking.")
581
582 def eat(self):
583 fork1 = self.index
584 fork2 = (self.index + 1) % self.num_philosophers
585
586 self.mutex.acquire()
587
588 # acquire the lock on both forks
589 self.forks[fork1].acquire()
590 self.forks[fork2].acquire()
591
592 print(f"Philosopher {self.index} is eating {fork1} and {fork2}.")
593
594 self.forks[fork1].release()
595 self.forks[fork2].release()
596
597 self.mutex.release()
598
599 def stop(self):
600 self.running = False
601
602 def run(self):
603 while self.running:
604 self.think()
605 self.eat()
606
607
608 if __name__ == '__main__':
609 philosophers = []
610 for i in range(Philosopher.num_philosophers):
611 philosopher = Philosopher(i)
612 philosophers.append(philosopher)
613 philosopher.start()
614
615 # run the simulation for a certain time(ex 10 seconds)
616 time_to_run = 5
617 # time.sleep(time_to_run)
618
619 threading.Timer(time_to_run, lambda: [p.stop() for p in philosophers]).start()
620
621 # for philosopher in philosophers:
622 # philosopher.stop()
623
624 for philosopher in philosophers:
625 philosopher.join()
626
627 '''
628 Starvation
629 - occurs when a process or thread is unable to make progress or access a resource it
needs to being continuously bypassed or delayed by other process or threads.
630 - in the context on concurrent programming
631 -starvation occurs when a lower-priority process/thread is continuously
preempted or blocked by the higher priority
632
633 LiveLock
634 - is a concurrency failure case where a thread or process is not block but it is unable
to make progress because of the actions of another thread
635 - it involves two or more threads that share concurrency primitives such as mutual
exclusion(mutex) locks
636
637 Livelock - threads cannot make progress and are not blocked
638 Deadlock - threads cannot make progress and are blocked
639
640 - unique case of starvation
641 '''
642
643 # a task executes a loop and each iteration in the loop is a critical section that
contains blocking call - mutex lock
644
645 from threading import Thread
646 from threading import Lock
647 from time import sleep
648
649 # task
650 def task(lock, identifier):
651
652 # acquire
653 # with lock:
654 # execute the lock
655 for i in range(5):
656 sleep(0.001).
657 # common fix for starvation is to add blocking call
658 # lock.acquire()
659 print(f"Thread {identifier} working")
660 sleep(1)
661 # lock.release()
662
663 lock = Lock()
664 threads = [Thread(target=task, args=(lock, i)) for i in range(2)]
665
666 for thread in threads:
667 thread.start()
668
669 for thread in threads:
670 thread.join()
671
672 # acquire() and release()
673

You might also like