Python Threads Sinhala | Concurrency Basics SC Guide | Python සමග Threads

හැඳින්වීම: වැඩ ඉක්මනින් කරමුද? (Introduction: Let's Do Things Faster!)
කොහොමද යාලුවනේ! අද අපි කතා කරන්න යන්නේ Software Engineering වල හරිම වැදගත්, ඒ වගේම ටිකක් සංකීර්ණ වෙන්න පුළුවන් මාතෘකාවක් ගැන – ඒ තමයි Concurrency, විශේෂයෙන්ම Python වල Threads ගැන. අපි හැමෝම කැමතියිනේ අපේ වැඩ ඉක්මනින් ඉවර කරගන්න. Computer program එකක් ගත්තම ඒකත් එහෙමයි. එක වැඩක් ඉවර වෙනකම් බලන් ඉන්නේ නැතුව, එකම වෙලාවෙ වැඩ කීපයක් කරගන්න පුළුවන් නම් කොච්චර හොඳද?
සරලවම කිව්වොත්, Concurrency කියන්නේ එකම වෙලාවෙ වැඩ කිහිපයක් කරනවා වගේ පේන එක. මේක අපිට අපේ application වල performance එක වැඩි කරගන්න, පරිශීලක අත්දැකීම (User Experience) හොඳ කරන්න වගේ ගොඩක් දේවල් වලට උදව් වෙනවා. ඒ වගේම අද දවසේ Web applications, Network services වගේ ගොඩක් දේවල් වලට මේ Concurrency කියන concept එක නැතුවම බෑ.
මේ Guide එකෙන් අපි, Python වල Threads කියන්නේ මොකක්ද, ඒවා කොහොමද වැඩ කරන්නේ, ඒවගේ වාසි සහ අවාසි මොනවද, වගේම Global Interpreter Lock (GIL) කියන්නේ මොකක්ද, Race Conditions වගේ ප්රායෝගික ගැටළු වලට මුහුණ දෙන්නේ කොහොමද කියලත් සරලව තේරුම් ගමු. එහෙනම් වැඩේට බහිමු!
Concurrency කියන්නේ මොකක්ද? Processes vs. Threads (What is Concurrency? Processes vs. Threads)
අපි වැඩ කරනවා කියන්නේ Program එකක් Run කරනවා කියන එකනේ. Program එකක් කියන්නේ Instructions set එකක්. සාමාන්යයෙන් CPU එක මේ Instructions එකින් එක Run කරනවා.
Concurrency කියන්නේ, වැඩ කිහිපයක් එකම වෙලාවෙ කරනවා වගේ පේන්න සලස්වන එක. ඒ කියන්නේ, එක වැඩක් ඉවර වෙනකම්ම බලන් ඉන්නේ නැතුව, ඒ වැඩේට අදාල කොටසක් කරලා, තව වැඩක කොටසක් කරලා, ආයෙත් මුල් වැඩේට එනවා වගේ දෙයක්. මේක Single core processor එකක වුණත් කරන්න පුළුවන්, CPU එක ඉක්මනට වැඩ මාරු කරමින් (Context Switching) වැඩ කරන හින්දා. Multi-core processor එකක නම් එකම වෙලාවෙ ඇත්තටම වැඩ කිහිපයක් Run කරන්න පුළුවන්.
Processes vs. Threads: නංගිලා මල්ලිලා vs. ඥාති සහෝදරයෝ වගේ!
Software Engineering වලදි Concurrency achieve කරන්න ප්රධාන ක්රම දෙකක් තියෙනවා: Processes සහ Threads. මේ දෙකම වැඩ සමාන්තරව (in parallel) හෝ සමගාමීව (concurrently) කරන්න උදව් වුණත්, ඒවා වැඩ කරන විදිහේ ලොකු වෙනස්කම් තියෙනවා.
1. Processes (ක්රියාවලි)
- හඳුනාගැනීම: Process එකක් කියන්නේ Computer program එකක ස්වාධීන instances. හිතන්න, ඔයා Google Chrome open කරලා තියෙනවා, Microsoft Word open කරලා තියෙනවා. මේ හැම එකක්ම වෙන වෙනම processes.
- Memory Sharing: Process එකකට තමන්ටම වෙන්වූ memory space එකක් තියෙනවා. ඒ නිසා එක process එකක් තවත් process එකක memory එකට කෙලින්ම access කරන්න බෑ. මේකෙන් security එක වැඩි වුණත්, process අතර data share කරනවා නම් ඒකට අමතර ක්රම (Inter-Process Communication - IPC) පාවිච්චි කරන්න වෙනවා.
- Resource Overhead: Processes කියන්නේ බර වැඩක් (heavyweight). ඒවා create කරන්න, destroy කරන්න, switch කරන්න වැඩි resources (memory, CPU time) යනවා.
2. Threads (නූල්)
- හඳුනාගැනීම: Thread එකක් කියන්නේ process එකක් ඇතුළේ තියෙන execution path එකක්. Process එකක් ඇතුළේ threads කිහිපයක් තියෙන්න පුළුවන්. හිතන්න ඔයාගේ Web browser එකේ (process) tabs කිහිපයක් (threads) open කරලා තියෙනවා වගේ.
- Memory Sharing: Threads එකම process එකේ කොටස් නිසා, ඒවා එකම memory space එක share කරනවා. මේක නිසා data share කරන එක හරිම ලේසියි. හැබැයි මේකේ අවාසිත් තියෙනවා, ඒ තමයි Race Conditions වගේ ගැටළු.
- Resource Overhead: Threads කියන්නේ සැහැල්ලුයි (lightweight). ඒවා create කරන්න, destroy කරන්න, switch කරන්න යන resources ප්රමාණය processes වලට වඩා ගොඩක් අඩුයි.
සරලව කිව්වොත්, Processes කියන්නේ වෙන වෙනම ගෙවල් වල ඉන්න ඥාති සහෝදරයෝ වගේ (එකිනෙකාට අයිති දේවල් වෙනමයි). Threads කියන්නේ එකම ගෙදර ඉන්න නංගිලා, මල්ලිලා වගේ (එකම කුස්සිය, එකම සාලය වගේ පොදු දේවල් පාවිච්චි කරනවා).
Python වල Threads: `threading` Module එක (Threads in Python: The `threading` Module)
Python වල Threads manage කරන්න අපිට `threading` කියන built-in module එක පාවිච්චි කරන්න පුළුවන්. මේකෙන් Threads create කරන්න, manage කරන්න අවශ්ය පහසුකම් සපයනවා.
සරල Thread එකක් හදමු (Let's Create a Simple Thread)
Thread එකක් හදන්න ප්රධාන ක්රම දෙකක් තියෙනවා:
- Thread එකට Run කරන්න ඕන function එක specify කරන එක.
threading.Thread
class එක extend කරන එක (මේක ටිකක් advanced, අපි මුල් ක්රමය බලමු).
අපි මුලින්ම සරලම උදාහරණයක් බලමු. මෙතන අපි `print_numbers` කියලා function එකක් හදලා, ඒක thread එකක් විදිහට Run කරමු.
import threading
import time
def print_numbers():
"""Numbers 1 to 5 print කරන thread function එක"""
for i in range(1, 6):
time.sleep(1) # තත්පර 1ක් pause කරනවා
print(f"Thread 1: {i}")
def print_letters():
"""Letters A to E print කරන main function එක"""
for char_code in range(ord('A'), ord('E') + 1):
time.sleep(0.8) # තත්පර 0.8ක් pause කරනවා
print(f"Main Thread: {chr(char_code)}")
print("ආරම්භයයි! (Starting!)")
# thread එකක් හදනවා
# target = thread එකට run කරන්න ඕන function එක
thread1 = threading.Thread(target=print_numbers)
# thread එක start කරනවා (මේකෙන් function එක background එකේ run වෙන්න පටන් ගන්නවා)
thread1.start()
# main program එකේ අනිත් වැඩ කරනවා (මෙතන print_letters function එක)
print_letters()
# thread එක ඉවර වෙනකම් main thread එක බලන් ඉන්නවා
# මේක නැත්නම් main thread එක ඉවර වුණ ගමන් program එක close වෙන්න පුළුවන්.
thread1.join()
print("ඉවරයි! (Finished!)")
මේ Code එක Run කලාම ඔයාට පෙනෙයි `Thread 1: 1`, `Main Thread: A`, `Thread 1: 2`, `Main Thread: B` වගේ output එකක් එනවා. ඒ කියන්නේ threads දෙකම එකවර වැඩ කරනවා වගේ පේනවා.
Arguments Pass කරන හැටි (How to Pass Arguments)
Thread එකට run කරන්න දෙන function එකට arguments pass කරන්න ඕන නම්, `args` parameter එකට tuple එකක් විදිහට දෙන්න පුළුවන්:
import threading
import time
def greet(name, delay):
"""නමක් දීලා pause කරලා print කරන thread function එක"""
time.sleep(delay)
print(f"Hello, {name}! (from thread)")
print("Greeting threads ආරම්භයයි! (Starting greeting threads!)")
thread_john = threading.Thread(target=greet, args=("John", 2))
thread_jane = threading.Thread(target=greet, args=("Jane", 1))
thread_john.start()
thread_jane.start()
thread_john.join()
thread_jane.join()
print("Greeting threads ඉවරයි! (Greeting threads finished!)")
මේකෙන් John කියන නම delay එකක් ඇතුවත්, Jane කියන නම ඊට වඩා අඩු delay එකක් ඇතුවත් print වෙනවා. `join()` calls නැත්නම් “Greeting threads ඉවරයි!” කියන message එක “Hello, John!” and “Hello, Jane!” වලට කලින් print වෙන්න පුළුවන්.
Global Interpreter Lock (GIL) – Python වල විශේෂත්වය (The Specialty of Python)
Python වල Concurrency ගැන කතා කරද්දි Global Interpreter Lock (GIL) ගැන කතා නොකර බෑ. මේක තමයි Python වල Threads වල වැඩ කරන විදිහට ලොකුම බලපෑමක් කරන්නේ.
GIL කියන්නේ මොකක්ද? (What is GIL?)
GIL කියන්නේ Python interpreter එකේ තියෙන mutex (හෝ lock) එකක්. මේකෙන් කරන්නේ එකම වෙලාවෙ එක Python thread එකකට විතරක් Python bytecode execute කරන්න ඉඩ දෙන එක. සරලව කිව්වොත්, CPU එකේ cores කීපයක් තිබුණත්, Python threads කීපයක් එකවර Run කරන්න බැරිවෙන්නේ GIL එක නිසා.
ඇයි GIL එක තියෙන්නේ? (Why Does GIL Exist?)
GIL එක තියෙන්නේ ප්රධාන වශයෙන් හේතු දෙකක් නිසා:
- Memory Management: Python වල object memory management එක simpler කරගන්න. Python වල reference counting කියන method එකෙන් objects වල memory manage කරනවා. GIL එක නැත්නම් එකම object එකට threads කිහිපයකින් එකවර access කරද්දි (Race condition) memory corruption වෙන්න පුළුවන්.
- C Extensions: C/C++ වලින් ලියපු Python extensions (numpy, pandas වගේ) වල thread-safety සහතික කරන්න. මේවා සාමාන්යයෙන් thread-safe විදිහට ලියලා නෑ. GIL එක නිසා Python threads C extension වලට යනකොටත් ඒ safety එක තියෙනවා.
GIL එකේ බලපෑම (Impact of GIL)
- CPU-bound Tasks: Python threads, CPU intensive operations (ගණනය කිරීම්, data processing) වලදි සැබෑ parallelism එකක් දෙන්නේ නෑ. එක thread එකක් run වෙනකොට අනිත් threads lock එක ගන්න බලන් ඉන්නවා. ඒ නිසා CPU-bound tasks වලදි threads පාවිච්චි කරන එකෙන් performance එක වැඩි වෙන්නේ නෑ, සමහරවිට අඩු වෙන්නත් පුළුවන් context switching overhead එක නිසා.
- I/O-bound Tasks: හැබැයි I/O-bound tasks (Network requests, file operations, database queries) වලදි GIL එක released වෙනවා. ඒ කියන්නේ, thread එකක් I/O operation එකක් කරන වෙලාවට GIL එක අනිත් thread එකකට ගන්න පුළුවන්. මේ නිසා I/O-bound tasks වලදි threads පාවිච්චි කරන එකෙන් performance එක සැලකිය යුතු ලෙස වැඩි කරගන්න පුළුවන්.
මතක තියාගන්න, GIL එක Python interpreter එකේ තියෙන දෙයක්, programming language එකේ නෙමෙයි. PyPy, Jython, IronPython වගේ සමහර Python implementations වල GIL එක නෑ, නැත්නම් වෙනස් විදිහට handle කරනවා.
ප්රායෝගික ගැටළු සහ විසඳුම්: Race Conditions (Practical Issues & Solutions: Race Conditions)
අපි කලින් කතා කලා threads එකම memory space එක share කරනවා කියලා. මේකෙන් පහසුකම් වගේම අභියෝගත් ඇති වෙනවා. ඒකෙන් ප්රධානම එකක් තමයි Race Condition.
Race Condition කියන්නේ මොකක්ද? (What is a Race Condition?)
Race Condition එකක් කියන්නේ threads කිහිපයක් එකම shared resource (variable, data structure) එකකට එකවර access කරලා, අන්තිම result එක ඒවා execution වෙන order එක අනුව වෙනස් වෙනවා නම්. හිතන්න බැංකුවේ එකම ගිණුමට දෙන්නෙක් එකම වෙලාවෙ සල්ලි දාන්න හදනවා වගේ. කවුරු කලින් update කරනවද කියන එක අනුව ගිණුමේ අන්තිම ශේෂය (balance) වෙනස් වෙන්න පුළුවන්.
උදාහරණයක් විදිහට, shared counter එකක් බලමු:
import threading
# මේක shared resource එක. Threads කිහිපයක් මේකට access කරනවා.
shared_counter = 0
def increment_counter():
global shared_counter
for _ in range(100000):
# මේ operation එක Atomic නෑ, ඒ කියන්නේ එක පියවරක් නෙමෙයි.
# 1. shared_counter ගාන කියවනවා.
# 2. ගාන එකකින් වැඩි කරනවා.
# 3. වැඩි කරපු ගාන ආයෙත් shared_counter ට assign කරනවා.
# මේ අතර මැද තව thread එකක් access කරන්න පුළුවන්.
shared_counter += 1
num_threads = 5
threads = []
print(f"ආරම්භක counter අගය: {shared_counter}")
for _ in range(num_threads):
thread = threading.Thread(target=increment_counter)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print(f"අවසාන counter අගය (බලාපොරොත්තු වන): {num_threads * 100000}")
print(f"ඇත්තටම ලැබුණු counter අගය: {shared_counter}")
මේක Run කලාම, “ඇත්තටම ලැබුණු counter අගය” කියන එක “බලාපොරොත්තු වන” අගයට වඩා අඩු වෙලා තියෙනවා ඔයාට පෙනෙයි. ඒ Race Condition එකක් නිසා.
විසඳුම: Locks (සින්තෝසය!)
Race Conditions වළක්වන්න Synchronization Primitives පාවිච්චි කරනවා. ඒකෙන් ප්රධානම එකක් තමයි Locks (aka Mutexes - Mutual Exclusions). Lock එකක් කියන්නේ shared resource එකකට එකවර එක thread එකකට විතරක් access කරන්න ඉඩ දෙන mechanism එකක්.
Lock එකක් පාවිච්චි කරනකොට, thread එකක් resource එකට access කරන්න කලින් Lock එක acquire කරනවා. වැඩේ ඉවර වුණාම Lock එක release කරනවා. මේක කරන අතරේ, වෙන thread එකකට ඒ Lock එක ගන්න බෑ, ඒක බලන් ඉන්න ඕන කලින් thread එක release කරනකම්.
import threading
shared_counter = 0
# Lock එකක් හදනවා
counter_lock = threading.Lock()
def increment_counter_safe():
global shared_counter
for _ in range(100000):
# Lock එක acquire කරනවා (වෙන thread එකකට දැන් shared_counter ට access කරන්න බෑ)
counter_lock.acquire()
try:
shared_counter += 1
finally:
# වැඩේ ඉවර වුණාම Lock එක release කරනවා. (exception එකක් ආවත් release වෙන්න ඕන)
counter_lock.release()
num_threads = 5
threads = []
print(f"ආරම්භක safe counter අගය: {shared_counter}")
for _ in range(num_threads):
thread = threading.Thread(target=increment_counter_safe)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print(f"අවසාන safe counter අගය (බලාපොරොත්තු වන): {num_threads * 100000}")
print(f"ඇත්තටම ලැබුණු safe counter අගය: {shared_counter}")
මේක Run කලාම ඔයාට පෙනෙයි “ඇත්තටම ලැබුණු safe counter අගය” කියන එක “බලාපොරොත්තු වන” අගයට සමානයි කියලා. මොකද Lock එක නිසා Race Condition එකක් ඇති වුණේ නෑ.
Python වල Locks පාවිච්චි කරන්න with
statement එකත් පාවිච්චි කරන්න පුළුවන්. ඒක Automatic Lock acquire/release කරන නිසා හොඳම practice එකක්. උදාහරණයක් විදිහට:
def increment_counter_safe_with_lock():
global shared_counter
for _ in range(100000):
with counter_lock: # Lock එක acquire කරලා වැඩේ ඉවර වුණාම release කරනවා
shared_counter += 1
threading
module එකේ RLock
(Reentrant Lock), Semaphore
, Event
, Condition
වගේ තවත් synchronization primitives තියෙනවා. ඒවා තවදුරටත් සංකීර්ණ scenarios වලට උදව් වෙනවා.
Threads භාවිතා කළ යුත්තේ කවදාද? Best Practices (When to Use Threads? Best Practices)
දැන් අපි Threads ගැන හොඳ අවබෝධයක් ගත්තා. එහෙනම් Threads පාවිච්චි කරන්න හොඳම අවස්ථා මොනවද, ඒවගේම පාවිච්චි නොකළ යුතු අවස්ථා මොනවද කියලා බලමු.
I/O-bound Tasks සඳහා Threads (Threads for I/O-bound Tasks)
Python Threads පාවිච්චි කරන්න හොඳම අවස්ථාව තමයි I/O-bound tasks. I/O-bound කියන්නේ Network request එකක් කරනවා, File එකක් read/write කරනවා, Database එකකින් data ගන්නවා වගේ operations. මේවා කරනකොට CPU එකට වැඩිපුර වැඩක් නෑ, ඒක I/O operation එක ඉවර වෙනකම් බලන් ඉන්නවා.
මේ වෙලාවේදී Python GIL එක release කරන නිසා, වෙන thread එකකට GIL එක අරගෙන තමන්ගේ Python code execute කරන්න පුළුවන්. ඒ නිසා, එක I/O operation එකක් වෙනකම් බලන් ඉන්න අතරේ, තව I/O operation එකක් කරන්න පුළුවන්.
උදාහරණ:
- Web scraping (වෙබ් අඩවි වලින් දත්ත එකතු කිරීම)
- Files download කිරීම
- Database queries run කිරීම
- Network communication (API calls)
import threading
import requests
import time
def download_image(url, filename):
print(f"Downloading {filename} from {url}...")
try:
response = requests.get(url, stream=True)
response.raise_for_status() # HTTP errors අල්ලගන්න
with open(filename, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Finished downloading {filename}")
except requests.exceptions.RequestException as e:
print(f"Error downloading {filename}: {e}")
image_urls = [
"https://picsum.photos/id/237/200/300",
"https://picsum.photos/id/238/200/300",
"https://picsum.photos/id/239/200/300",
"https://picsum.photos/id/240/200/300"
]
start_time = time.time()
threads = []
for i, url in enumerate(image_urls):
filename = f"image_{i+1}.jpg"
thread = threading.Thread(target=download_image, args=(url, filename))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end_time = time.time()
print(f"All downloads completed in {end_time - start_time:.2f} seconds")
මේ Code එක Run කලාම, images ටික එකින් එක download වෙනවා වෙනුවට, එකවර download වෙනවා වගේ පෙනෙයි. ඒක I/O-bound task එකක් නිසා GIL එක released වෙලා, threads එකවර වැඩ කරන හින්දා.
CPU-bound Tasks සඳහා Threads නුසුදුසුයි (Threads are NOT suitable for CPU-bound Tasks)
අපි කලින් කතා කරපු GIL එක නිසා, Python threads, CPU intensive operations (දත්ත ගණනය කිරීම්, image processing, complex algorithms) වලදි සැබෑ parallelism එකක් දෙන්නේ නෑ. එක thread එකකට විතරයි Python bytecode එකවර execute කරන්න පුළුවන්.
ඒ නිසා, ඔයාගේ program එක CPU එකේ උපරිම usage එක ගන්න ඕන කරන වැඩක් නම්, Python threads පාවිච්චි කරන එකෙන් performance එක වැඩි වෙන්නේ නෑ. සමහරවිට context switching වලට යන overhead එක නිසා performance එක අඩු වෙන්නත් පුළුවන්.
විසඳුම: `multiprocessing` module එක
CPU-bound tasks වලට Python වල `multiprocessing` module එක පාවිච්චි කරන්න ඕන. මේකෙන් Processes create කරනවා. Processes වලට තමන්ටම වෙන්වූ Python interpreter instance එකක් සහ memory space එකක් තියෙන නිසා GIL එක ගැටලුවක් වෙන්නේ නෑ. මේ නිසා Processes වලට cores කිහිපයක එකවර වැඩ කරන්න පුළුවන්, සැබෑ parallelism එකක් ලබා දෙමින්.
Best Practices:
- I/O-bound සඳහා Threads, CPU-bound සඳහා Processes: මේක තමයි රත්තරන් නීතිය.
- Shared State අඩු කරන්න: Threads අතර data share කරනවා නම්, හැකිතාක් දුරට Shared mutable state (වෙනස් කළ හැකි shared data) අඩු කරන්න උත්සාහ කරන්න.
- Synchronization Primitives නිවැරදිව පාවිච්චි කරන්න: Shared state තියෙනවා නම්, Race Conditions වළක්වන්න Locks, Semaphores වගේ synchronization primitives නිවැරදිව පාවිච්චි කරන්න.
- Deadlocks ගැන අවබෝධය: Locks පාවිච්චි කරනකොට Deadlocks (threads එකිනෙකාට අවශ්ය Locks release කරනකම් බලන් ඉඳලා program එක freeze වෙන එක) ඇතිවෙන්න පුළුවන්. මේවා වළක්වන්න Locks එකම order එකකට acquire කරන්න වගේ strategies තියෙනවා.
අවසන් වචනය (Conclusion)
ඉතින් යාලුවනේ, මේ Guide එකෙන් අපි Python වල Concurrency සහ Threads ගැන මූලික අවබෝධයක් ලබාගත්තා. Concurrency කියන්නේ මොකක්ද, Processes සහ Threads අතර වෙනස, Python වල `threading` module එක පාවිච්චි කරන හැටි, ඒවගේම හරිම වැදගත් Global Interpreter Lock (GIL) ගැන, Race Conditions වගේ ගැටළු සහ ඒවාට Locks පාවිච්චි කරලා විසඳුම් හොයාගන්න හැටි වගේ ගොඩක් දේවල් ඉගෙන ගත්තා. ඒ වගේම Threads පාවිච්චි කරන්න ඕන I/O-bound tasks වලට මිසක් CPU-bound tasks වලට නෙමෙයි කියන එකත් මතක තියාගන්න. CPU-bound tasks වලට `multiprocessing` තමයි හොඳම විසඳුම.
Threads කියන්නේ හරිම බලවත් Tools එකක්. ඒවා නිවැරදිව පාවිච්චි කරනවා නම් ඔයාගේ applications වල performance එක සැලකිය යුතු ලෙස වැඩි කරගන්න පුළුවන්. හැබැයි ඒවා සමහර වෙලාවට complex වෙන්නත් පුළුවන්, විශේෂයෙන්ම shared state manage කරනකොට. ඒ නිසා මේ Concepts හොඳට තේරුම් අරගෙන, ප්රායෝගිකව වැඩ කරලා බලන එක හරිම වැදගත්.
ඔයාගේ ඊළඟ Project එකේ I/O-bound tasks තියෙනවා නම්, Python Threads පාවිච්චි කරලා Performance එක වැඩි කරලා බලන්න. මොනවා හරි ප්රශ්න තියෙනවා නම්, ඔයාගේ අත්දැකීම් කොහොමද කියලා Comment Section එකේ කියන්න අමතක කරන්න එපා. අපි ඊළඟ Guide එකෙන් හම්බවෙමු!