Welcome to the grand stage of Python threading, where we unravel the secrets of concurrent programming. In this performance, we will cover the threading module, share data gracefully, dance with locks to prevent race conditions, explore daemon threads, and orchestrate a harmonious experience with queues for thread-safe data processing.
Act I: Creating and Running Threads
In the opening act, we introduce the Thread
class, the protagonist of our concurrent tale. Let's create and run multiple threads to unleash the power of parallelism.
from threading import Thread
def square_numbers():
for i in range(1000):
result = i * i
if __name__ == "__main__":
threads = []
num_threads = 10
for i in range(num_threads):
thread = Thread(target=square_numbers)
threads.append(thread)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
Here, we've orchestrated a symphony of ten threads, each performing the noble task of squaring numbers. The start()
method sets them in motion, and join()
ensures the main thread waits for their triumphant completion.
Act II: Data Sharing Between Threads
Threads coexisting in the same memory space beckon us to explore data sharing. In our next act, we stage a captivating scenario where two threads gracefully interact with a shared global variable.
from threading import Thread
import time
database_value = 0
def increase():
global database_value
local_copy = database_value
local_copy += 1
time.sleep(0.1)
database_value = local_copy
if __name__ == "__main__":
print('Start value: ', database_value)
t1 = Thread(target=increase)
t2 = Thread(target=increase)
t1.start()
t2.start()
t1.join()
t2.join()
print('End value:', database_value)
print('end main')
As our threads gracefully increase the database value, we encounter a twist—a race condition. Two threads vying for the same resource lead to unexpected results. Fear not, for our hero, the Lock
, steps in to bring order to this chaos.
Act III: The Lock: Guardian of Data Integrity
The plot thickens as we face the villainous race condition. In this act, the Lock
emerges as our hero, preventing threads from stepping on each other's toes.
from threading import Thread, Lock
import time
database_value = 0
def increase(lock):
global database_value
lock.acquire()
local_copy = database_value
local_copy += 1
time.sleep(0.1)
database_value = local_copy
lock.release()
if __name__ == "__main__":
lock = Lock()
print('Start value: ', database_value)
t1 = Thread(target=increase, args=(lock,))
t2 = Thread(target=increase, args=(lock,))
t1.start()
t2.start()
t1.join()
t2.join()
print('End value:', database_value)
print('end main')
The Lock
swoops in to save the day, ensuring that the critical code section is protected. Threads politely wait their turn, eliminating the race condition and producing the expected result.
Act IV: Daemon Threads: A Mysterious Presence
In this act, we unravel the mystery of daemon threads—background performers that gracefully exit when the main show concludes.
from threading import Thread, Lock, current_thread
from queue import Queue
def worker(q, lock):
while True:
value = q.get()
# do stuff...
with lock:
# prevent printing at the same time with this lock
print(f"in {current_thread().name} got {value}")
# For each get(), a subsequent call to task_done() tells the queue
# that the processing on this item is complete.
# If all tasks are done, q.join() can unblock
q.task_done()
if __name__ == '__main__':
q = Queue()
num_threads = 10
lock = Lock()
for i in range(num_threads):
t = Thread(name=f"Thread{i+1}", target=worker, args=(q, lock))
t.daemon = True # dies when the main thread dies
t.start()
# fill the queue with items
for x in range(20):
q.put(x)
q.join() # Blocks until all items in the queue have been gotten and processed.
print('main done')
Daemon threads cast a mysterious aura as they gracefully exit when the main performance concludes. They are the silent guardians, handling background tasks without stealing the limelight.
Grand Finale: Queues for Thread-Safe Harmony
In our grand finale, we introduce the Queue
—the conductor orchestrating thread-safe data exchanges. Queues shine in both multithreaded and multiprocessing environments, ensuring a seamless flow of data.
from queue import Queue
# create queue
q = Queue()
# add elements
q.put(1) # 1
q.put(2) # 2 1
q.put(3) # 3 2 1
# now q looks like this:
# back --> 3 2 1 --> front
# get and remove the first element
first = q.get() # --> 1
print(first)
# q looks like this:
# back --> 3 2 --> front
In the multithreading spectacle, the queue takes center stage, guiding threads through a synchronized dance of data exchange. Operations on the queue are thread-safe, ensuring a flawless performance.
Our journey through the realms of Python threading concludes, leaving you equipped to compose your own symphony of concurrent elegance. May your threads dance in harmony, and your data flow seamlessly in the grand theater of parallelism.