ವಿಷಯಕ್ಕೆ ತೆರಳಿ

ಮಲ್ಟಿಥ್ರೆಡಿಂಗ್ (Multithreading)

ಮಲ್ಟಿಥ್ರೆಡಿಂಗ್ ಎನ್ನುವುದು ಒಂದು ಪ್ರೊಸೆಸ್‌ನೊಳಗೆ ಹಲವು ಥ್ರೆಡ್‌ಗಳನ್ನು (ಚಿಕ್ಕ ಕಾರ್ಯಗಳು) ಏಕಕಾಲದಲ್ಲಿ ಚಲಾಯಿಸುವ ಒಂದು ತಂತ್ರ. ಒಂದೇ ಪ್ರೊಸೆಸ್‌ನ ಮೆಮೊರಿ ಸ್ಪೇಸ್ ಅನ್ನು ಎಲ್ಲಾ ಥ್ರೆಡ್‌ಗಳು ಹಂಚಿಕೊಳ್ಳುತ್ತವೆ. ಇದು I/O-ಬೌಂಡ್ (ಇನ್‌ಪುಟ್/ಔಟ್‌ಪುಟ್ ಬೌಂಡ್) ಕಾರ್ಯಗಳಿಗೆ ಅತ್ಯಂತ ಉಪಯುಕ್ತವಾಗಿದೆ.

I/O-ಬೌಂಡ್ ಕಾರ್ಯಗಳು: ನೆಟ್‌ವರ್ಕ್ ವಿನಂತಿಗಳು, ಫೈಲ್ ಓದುವುದು/ಬರೆಯುವುದು, ಡೇಟಾಬೇಸ್ ಸಂಪರ್ಕ ಮುಂತಾದ ಕಾರ್ಯಗಳು. ಈ ಸಂದರ್ಭಗಳಲ್ಲಿ, ಪ್ರೊಸೆಸರ್ ಹೆಚ್ಚಾಗಿ ಕಾಯುತ್ತಿರುತ್ತದೆ. ಆ ಕಾಯುವ ಸಮಯದಲ್ಲಿ ಬೇರೆ ಥ್ರೆಡ್‌ಗಳನ್ನು ಚಲಾಯಿಸಬಹುದು.

ಪೈಥಾನ್‌ನಲ್ಲಿ ಮಲ್ಟಿಥ್ರೆಡಿಂಗ್

ಪೈಥಾನ್‌ನಲ್ಲಿ ಮಲ್ಟಿಥ್ರೆಡಿಂಗ್ ಅನ್ನು threading ಮಾಡ್ಯೂಲ್ ಬಳಸಿ ಕಾರ್ಯಗತಗೊಳಿಸಲಾಗುತ್ತದೆ.

ಉದಾಹರಣೆ: ಎರಡು ವಿಭಿನ್ನ ಕಾರ್ಯಗಳನ್ನು ಎರಡು ಥ್ರೆಡ್‌ಗಳಲ್ಲಿ ಚಲಾಯಿಸೋಣ.

import threading
import time

def print_numbers():
    """ಸಂಖ್ಯೆಗಳನ್ನು ಪ್ರಿಂಟ್ ಮಾಡುವ ಫಂಕ್ಷನ್"""
    print(f"ಥ್ರೆಡ್ {threading.current_thread().name}: ಪ್ರಾರಂಭವಾಯಿತು")
    for i in range(1, 6):
        print(i)
        time.sleep(1)
    print(f"ಥ್ರೆಡ್ {threading.current_thread().name}: ಮುಗಿಯಿತು")

def print_letters():
    """ಅಕ್ಷರಗಳನ್ನು ಪ್ರಿಂಟ್ ಮಾಡುವ ಫಂಕ್ಷನ್"""
    print(f"ಥ್ರೆಡ್ {threading.current_thread().name}: ಪ್ರಾರಂಭವಾಯಿತು")
    for letter in ['A', 'B', 'C', 'D', 'E']:
        print(letter)
        time.sleep(1.5)
    print(f"ಥ್ರೆಡ್ {threading.current_thread().name}: ಮುಗಿಯಿತು")

# ಥ್ರೆಡ್‌ಗಳನ್ನು ರಚಿಸುವುದು
t1 = threading.Thread(target=print_numbers, name="ಸಂಖ್ಯೆ-ಥ್ರೆಡ್")
t2 = threading.Thread(target=print_letters, name="ಅಕ್ಷರ-ಥ್ರೆಡ್")

# ಥ್ರೆಡ್‌ಗಳನ್ನು ಪ್ರಾರಂಭಿಸುವುದು
t1.start()
t2.start()

# ಮುಖ್ಯ ಥ್ರೆಡ್, t1 ಮತ್ತು t2 ಮುಗಿಯುವವರೆಗೆ ಕಾಯುತ್ತದೆ
t1.join()
t2.join()

print("ಎಲ್ಲಾ ಥ್ರೆಡ್‌ಗಳು ಮುಗಿದಿವೆ.")
ಔಟ್‌ಪುಟ್ (ಅಂದಾಜು):
ಥ್ರೆಡ್ ಸಂಖ್ಯೆ-ಥ್ರೆಡ್: ಪ್ರಾರಂಭವಾಯಿತು
1
ಥ್ರೆಡ್ ಅಕ್ಷರ-ಥ್ರೆಡ್: ಪ್ರಾರಂಭವಾಯಿತು
A
2
B
3
C
4
D
5
ಥ್ರೆಡ್ ಸಂಖ್ಯೆ-ಥ್ರೆಡ್: ಮುಗಿಯಿತು
E
ಥ್ರೆಡ್ ಅಕ್ಷರ-ಥ್ರೆಡ್: ಮುಗಿಯಿತು
ಎಲ್ಲಾ ಥ್ರೆಡ್‌ಗಳು ಮುಗಿದಿವೆ.
ಇಲ��ಲಿ, ಎರಡೂ ಫಂಕ್ಷನ್‌ಗಳು ಏಕಕಾಲದಲ್ಲಿ ಚಲಿಸುತ್ತವೆ. join() ಮೆಥಡ್, ಮುಖ್ಯ ಪ್ರೋಗ್ರಾಮ್ ಮುಂದುವರಿಯುವ ಮೊದಲು ಆಯಾ ಥ್ರೆಡ್‌ಗಳು ತಮ್ಮ ಕಾರ್ಯವನ್ನು ಪೂರ್ಣಗೊಳಿಸುವವರೆಗೆ ಕಾಯುವಂತೆ ಮಾಡುತ್ತದೆ.

ಬಳಕೆಯ ಸಂದರ್ಭಗಳು (Use Cases)

  1. ವೆಬ್ ಸ್ಕ್ರೇಪಿಂಗ್: ಹಲವು ವೆಬ್‌ಪುಟಗಳಿಂದ ಏಕಕಾಲದಲ್ಲಿ ಡೇಟಾವನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು.
  2. GUI ಅಪ್ಲಿಕೇಶನ್‌ಗಳು: ಬಳಕೆದಾರರ ಇಂಟರ್ಫೇಸ್ (UI) ಅನ್ನು ರೆಸ್ಪಾನ್ಸಿವ್ ಆಗಿಡಲು. ಒಂದು ಥ್ರೆಡ್ ಹಿನ್ನೆಲೆಯಲ್ಲಿ ದೀರ್ಘಾವಧಿಯ ಕಾರ್ಯವನ್ನು ನಿರ್ವಹಿಸುತ್ತಿದ್ದರೆ, UI ಥ್ರೆಡ್ ಫ್ರೀ ಆಗಿರುತ್ತದೆ.
  3. ನೆಟ್‌ವರ್ಕಿಂಗ್: ಹಲವು ಕ್ಲೈಂಟ್‌ಗಳಿಂದ ಬರುವ ವಿನಂತಿಗಳನ್ನು ಏಕಕಾಲದಲ್ಲಿ ನಿರ್ವಹಿಸಲು.
  4. ಫೈಲ್ ಪ್ರೊಸೆಸಿಂಗ್: ಹಲವು ಫೈಲ್‌ಗಳನ್ನು ಏಕಕಾಲದಲ್ಲಿ ಓದಲು ಅಥವಾ ಬರೆಯಲು.

ಮಿತಿಗಳು ಮತ್ತು ಸಮಸ್ಯೆಗಳು

1. ಗ್ಲೋಬಲ್ ಇಂಟರ್‌ಪ್ರಿಟರ್ ಲಾಕ್ (Global Interpreter Lock - GIL)

ಪೈಥಾನ್‌ನ CPython ಇಂಪ್ಲಿಮೆಂಟೇಶನ್‌ನಲ್ಲಿ GIL ಎಂಬ ಒಂದು ಮಿತಿ ಇದೆ. GIL, ಒಂದು ಸಮಯದಲ್ಲಿ ಕೇವಲ ಒಂದೇ ಥ್ರೆಡ್ ಪೈಥಾನ್ ಬೈಟ್‌ಕೋಡ್ ಅನ್ನು ಎಕ್ಸಿಕ್ಯೂಟ್ ಮಾಡಲು ಅನುಮತಿಸುತ್ತದೆ.

  • ಪರಿಣಾಮ: CPU-ಬೌಂಡ್ (ಗಣಿತದ ಲೆಕ್ಕಾಚಾರಗಳಂತಹ) ಕಾರ್ಯಗಳಿಗೆ ಮಲ್ಟಿಥ್ರೆಡಿಂಗ್‌ನಿಂದ ಯಾವುದೇ ವೇಗದ ಸುಧಾರಣೆ ಆಗುವುದಿಲ್ಲ. ಏಕೆಂದರೆ, ಥ್ರೆಡ್‌ಗಳು ನಿಜವಾಗಿಯೂ ಸಮಾನಾಂತರವಾಗಿ (parallel) ಚಲಿಸುವುದಿಲ್ಲ, ಬದಲಾಗಿ ಒಂದರ ನಂತರ ಒಂದರಂತೆ (concurrent) ಚಲಿಸುತ್ತವೆ.
  • ಪರಿಹಾರ: CPU-ಬೌಂಡ್ ಕಾರ್ಯಗಳಿಗೆ ಮಲ್ಟಿಪ್ರೊಸೆಸಿಂಗ್ (multiprocessing) ಬಳಸುವುದು ಉತ್ತಮ.

2. ರೇಸ್ ಕಂಡೀಷನ್ (Race Condition)

ಹಲವು ಥ್ರೆಡ್‌ಗಳು ಒಂದೇ ಸಮಯದಲ್ಲಿ ಹಂಚಿಕೆಯಾದ ಡೇಟಾವನ್ನು (shared data) ಬದಲಾಯಿಸಲು ಪ್ರಯತ್ನಿಸಿದಾಗ ರೇಸ್ ಕಂಡೀಷನ್ ಉಂಟಾಗುತ್ತದೆ. ಇದು ಅನಿರೀಕ್ಷಿತ ಮತ್ತು ತಪ್ಪಾದ ಫಲಿತಾಂಶಗಳಿಗೆ ಕಾರಣವಾಗಬಹುದು.

ಉದಾಹರಣೆ:

import threading

shared_variable = 0

def increment():
    global shared_variable
    for _ in range(1000000):
        shared_variable += 1

t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)

t1.start()
t2.start()

t1.join()
t2.join()

print(f"ಅಂತಿಮ ಮೌಲ್ಯ: {shared_variable}")
ನಿರೀಕ್ಷಿತ ಔಟ್‌ಪುಟ್: 2000000 ನಿಜವಾದ ಔಟ್‌ಪುಟ್ (ಹೆಚ್ಚಾಗಿ): 2000000 ಕ್ಕಿಂತ ಕಡಿಮೆ ಇರುವ ಒಂದು ಸಂಖ್ಯೆ.

ಕಾರಣ: shared_variable += 1 ಕಾರ್ಯಾಚರಣೆಯು ಅಟಾಮಿಕ್ (atomic) ಅಲ್ಲ. ಇದು ಮೂರು ಹಂತಗಳಲ್ಲಿ ನಡೆಯುತ್ತದೆ: 1. shared_variable ನ ಮೌಲ್ಯವನ್ನು ಓದುವುದು. 2. ಮೌಲ್ಯಕ್ಕೆ 1 ಅನ್ನು ಸೇರಿಸುವುದು. 3. ಹೊಸ ಮೌಲ್ಯವನ್ನು shared_variable ಗೆ ಬರೆಯುವುದು.

ಎರಡು ಥ್ರೆಡ್‌ಗಳು ಈ ಹಂತಗಳ ಮಧ್ಯೆ ಒಂದನ್ನೊಂದು ತಡೆಯಬಹುದು, ಇದರಿಂದಾಗಿ ಕೆಲವು ಇನ್‌ಕ್ರಿಮೆಂಟ್‌ಗಳು ಕಳೆದುಹೋಗುತ್ತವೆ.

3. ರೇಸ್ ಕಂಡೀಷನ್‌ಗೆ ಪರಿಹಾರ: ಲಾಕ್ಸ್ (Locks)

threading.Lock ಬಳಸಿ ರೇಸ್ ಕಂಡೀಷನ್ ಅನ್ನು ತಡೆಯಬಹುದು. ಒಂದು ಥ್ರೆಡ್ ಲಾಕ್ ಅನ್ನು ಪಡೆದಾಗ, ಬೇರೆ ಥ್ರೆಡ್‌ಗಳು ಆ ಲಾಕ್ ಬಿಡುಗಡೆಯಾಗುವವರೆಗೆ ಕಾಯಬೇಕು.

ಪರಿಹರಿಸಿದ ಉದಾಹರಣೆ:

import threading

shared_variable = 0
lock = threading.Lock()

def increment():
    global shared_variable
    for _ in range(1000000):
        lock.acquire()  # ಲಾಕ್ ಪಡೆಯುವುದು
        shared_variable += 1
        lock.release()  # ಲಾಕ್ ಬಿಡುಗಡೆ ಮಾಡುವುದು

# ... (ಉಳಿದ ಕೋಡ್ ಹಿಂದಿನಂತೆಯೇ)
ಈಗ, shared_variable += 1 ಕಾರ್ಯಾಚರಣೆಯು ಸುರಕ್ಷಿತವಾಗಿರುತ್ತದೆ ಮತ್ತು ಅಂತಿಮ ಮೌಲ್ಯ ಸರಿಯಾಗಿ 2000000 ಆಗಿರುತ್ತದೆ. with lock: ಸ್ಟೇಟ್‌ಮೆಂಟ್ ಬಳಸುವುದು ಇನ್ನೂ ಉತ್ತಮ ಅಭ್ಯಾಸ, ಏಕೆಂದರೆ ಅದು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಲಾಕ್ ಅನ್ನು ಬಿಡುಗಡೆ ಮಾಡುತ್ತದೆ.

def increment():
    global shared_variable
    for _ in range(1000000):
        with lock:
            shared_variable += 1

ಸಾರಾಂಶದಲ್ಲಿ, ಮಲ್ಟಿಥ್ರೆಡಿಂಗ್ I/O-ಬೌಂಡ್ ಕಾರ್ಯಗಳಿಗೆ ಅತ್ಯುತ್ತಮವಾಗಿದೆ, ಆದರೆ GIL ನಿಂದಾಗಿ CPU-ಬೌಂಡ್ ಕಾರ್ಯಗಳಿಗೆ ಸೂಕ್ತವಲ್ಲ. ಹಂಚಿಕೆಯಾದ ಡೇಟಾವನ್ನು ಬಳಸುವಾಗ ರೇಸ್ ಕಂ��ೀಷನ್‌ಗಳ ಬಗ್ಗೆ ಜಾಗರೂಕರಾಗಿರಬೇಕು ಮತ್ತು ಲಾಕ್‌ಗಳನ್ನು ಬಳಸಿ ಅವುಗಳನ್ನು ತಡೆಯಬೇಕು.