Đa luồng trong Python với ví dụ: Tìm hiểu GIL trong Python

⚡ Tóm tắt thông minh

Đa luồng trong Python Phương pháp này chạy nhiều luồng bên trong một tiến trình duy nhất, do đó chúng chia sẻ bộ nhớ và hoạt động đồng thời. Mô-đun luồng tạo và quản lý các luồng này, trong khi Khóa Trình thông dịch Toàn cục giới hạn khả năng song song thực sự, khiến kỹ thuật này phù hợp nhất cho các tác vụ phụ thuộc vào đầu vào/đầu ra.

  • 🔘 Luồng so với tiến trình: Luồng (thread) là một đơn vị thực thi bên trong một tiến trình, và các luồng chia sẻ dữ liệu của tiến trình trong khi chạy riêng lẻ.
  • ☑️ Hai mô-đun: Mô-đun _thread lỗi thời cung cấp các lệnh gọi cấp thấp, trong khi mô-đun threading cấp cao hơn là tiêu chuẩn hiện đại để xây dựng các luồng.
  • Tạo chủ đề: Mở rộng lớp Thread, ghi đè phương thức __init__ và run(), sau đó gọi start() để khởi chạy và join() để chờ hoàn thành.
  • 🧪 Tránh các điều kiện thi đấu: Một khóa chỉ cho phép một luồng truy cập vào vùng quan trọng tại một thời điểm, ngăn ngừa tình trạng tắc nghẽn và dữ liệu dùng chung bị hỏng.
  • 🛠️ Hiểu về GIL: Khóa trình thông dịch toàn cục cho phép một luồng chạy. Python Mỗi lần chỉ xử lý một bytecode, vì vậy các tác vụ đòi hỏi nhiều CPU cần đến xử lý đa luồng.
  • 🤖 Khối lượng công việc AI: Đa luồng giúp tăng tốc các bước AI bị giới hạn bởi đầu vào/đầu ra, chẳng hạn như tải dữ liệu, trong khi NumPy và PyTorGiải phóng GIL để thực hiện tính toán song song.

Đa luồng trong Python

Python ngôn ngữ lập trình cho phép bạn sử dụng đa xử lý hoặc đa luồng. Trong hướng dẫn này, bạn sẽ học cách viết các ứng dụng đa luồng trong Python.

Chủ đề là gì?

Luồng (thread) là một đơn vị thực thi trong lập trình song song. Đa luồng là một kỹ thuật cho phép CPU thực thi nhiều tác vụ của một tiến trình cùng một lúc. Các luồng này có thể thực thi độc lập trong khi vẫn chia sẻ tài nguyên của tiến trình.

Một quy trình là gì?

Về cơ bản, tiến trình là chương trình đang được thực thi. Khi bạn khởi chạy một ứng dụng trên máy tính (như trình duyệt hoặc trình soạn thảo văn bản), hệ điều hành sẽ tạo ra một tiến trình. quá trình.

Đa luồng trong Python?

Đa luồng trong Python Lập trình đa luồng là một kỹ thuật nổi tiếng, trong đó nhiều luồng trong một tiến trình chia sẻ không gian dữ liệu với luồng chính, giúp việc chia sẻ thông tin và giao tiếp giữa các luồng trở nên dễ dàng và hiệu quả. Luồng nhẹ hơn tiến trình. Nhiều luồng có thể thực thi độc lập trong khi vẫn chia sẻ tài nguyên của tiến trình. Mục đích của đa luồng là để chạy nhiều tác vụ và chức năng cùng một lúc.

Đa xử lý là gì?

Đa xử lý cho phép bạn chạy nhiều tiến trình không liên quan cùng lúc. Các tiến trình này không chia sẻ tài nguyên của chúng và giao tiếp thông qua IPC.

Python Đa luồng vs Đa xử lý

Để hiểu về tiến trình và luồng, hãy xem xét tình huống này: Một tệp .exe trên máy tính của bạn là một chương trình. Khi bạn mở nó, hệ điều hành sẽ tải nó vào bộ nhớ và CPU sẽ thực thi nó. Phiên bản của chương trình đang chạy được gọi là tiến trình.

Mỗi quy trình đều có hai thành phần cơ bản:

  • Code
  • Dữ liệu

Bây giờ, một tiến trình có thể chứa một hoặc nhiều phần con được gọi là chủ đề. Điều này phụ thuộc vào kiến ​​trúc hệ điều hành. Bạn có thể coi luồng (thread) như một phần của tiến trình có thể được hệ điều hành thực thi riêng biệt.

Nói cách khác, đó là một chuỗi các lệnh có thể được hệ điều hành thực thi độc lập. Các luồng trong cùng một tiến trình chia sẻ dữ liệu của tiến trình đó và được thiết kế để hoạt động cùng nhau nhằm tạo điều kiện thuận lợi cho tính song song.

Tại sao nên sử dụng Đa luồng?

Đa luồng cho phép bạn chia nhỏ ứng dụng thành nhiều tác vụ phụ và chạy các tác vụ này cùng lúc. Nếu bạn sử dụng đa luồng đúng cách, tốc độ, hiệu suất và khả năng hiển thị của ứng dụng đều có thể được cải thiện.

Python Đa luồng

Python Hỗ trợ các cấu trúc cho cả xử lý đa tiến trình và đa luồng. Trong hướng dẫn này, bạn sẽ tập trung chủ yếu vào việc triển khai đa luồng ứng dụng với PythonCó hai mô-đun chính có thể được sử dụng để xử lý các luồng trong đó. Python:

  1. Chủ đề mô-đun, và
  2. luồng mô-đun

Tuy nhiên, trong PythonNgoài ra còn có một khái niệm gọi là khóa trình thông dịch toàn cục (GIL). Tuy nhiên, nó không mang lại nhiều lợi ích về hiệu năng và thậm chí có thể còn không hiệu quả. giảm hiệu suất của một số ứng dụng đa luồng. Bạn sẽ tìm hiểu tất cả về nó trong các phần sắp tới của hướng dẫn này.

Các mô-đun Thread và Threading

Hai mô-đun mà bạn sẽ tìm hiểu trong hướng dẫn này là mô-đun chủ đềmô-đun luồng.

Tuy nhiên, mô-đun luồng từ lâu đã không còn được dùng nữa. Bắt đầu với Python 3, nó đã được coi là lỗi thời và chỉ có thể truy cập được dưới dạng _chủ đề để tương thích ngược.

Bạn nên sử dụng cấp độ cao hơn luồng Đây là mô-đun dành cho các ứng dụng mà bạn dự định triển khai. Mô-đun luồng chỉ được đề cập ở đây nhằm mục đích giáo dục.

Mô-đun chủ đề

Cú pháp để tạo một chủ đề mới bằng mô-đun này như sau:

thread.start_new_thread(function_name, arguments)

Được rồi, bây giờ bạn đã nắm được lý thuyết cơ bản để bắt đầu viết mã. Vì vậy, hãy mở IDLE hoặc một cuốn sổ tay và nhập nội dung sau:

import time
import _thread

def thread_test(name, wait):
   i = 0
   while i <= 3:
      time.sleep(wait)
      print("Running %s\n" %name)
      i = i + 1

   print("%s has finished execution" %name)

if __name__ == "__main__":
    
    _thread.start_new_thread(thread_test, ("First Thread", 1))
    _thread.start_new_thread(thread_test, ("Second Thread", 2))
    _thread.start_new_thread(thread_test, ("Third Thread", 3))

Lưu file và nhấn F5 để chạy chương trình. Nếu mọi thứ đã được thực hiện chính xác thì đây là kết quả mà bạn sẽ thấy:

Mô-đun chủ đề

Bạn sẽ tìm hiểu thêm về điều kiện thi đấu và cách xử lý chúng trong các phần tiếp theo.

Mô-đun chủ đề

GIẢI THÍCH MÃ

  1. Các câu lệnh này nhập mô-đun thời gian và luồng, được sử dụng để xử lý việc thực thi và trì hoãn. Python chủ đề.
  2. Ở đây, bạn đã xác định một hàm gọi là thread_test, cái này sẽ được gọi bởi bắt đầu_new_thread Phương thức này chạy một vòng lặp while trong bốn lần và in ra tên của luồng đã gọi nó. Sau khi vòng lặp hoàn tất, nó in ra một thông báo cho biết luồng đã kết thúc quá trình thực thi.
  3. Đây là phần chính của chương trình của bạn. Ở đây, bạn chỉ cần gọi bắt đầu_new_thread phương pháp với thread_test Truyền một hàm làm đối số. Thao tác này sẽ tạo một luồng mới cho hàm bạn truyền làm đối số và bắt đầu thực thi nó. Lưu ý rằng bạn có thể thay thế (thread_test) bằng bất kỳ hàm nào khác mà bạn muốn chạy dưới dạng một luồng.

Mô-đun luồng

Mô-đun này là sự triển khai cấp cao của việc phân luồng trong Python và tiêu chuẩn thực tế để quản lý các ứng dụng đa luồng. Nó cung cấp một loạt các tính năng khi so sánh với mô-đun luồng.

Cấu trúc của module Threading

Cấu trúc của module Threading

Dưới đây là danh sách một số chức năng hữu ích được xác định trong mô-đun này:

Tên chức năng Mô tả Chi tiết
hoạt độngCount() Trả về số lượng Sợi chỉ Những vật thể vẫn còn sống.
currentThread() Trả về đối tượng hiện tại của lớp Thread.
liệt kê () Liệt kê tất cả các đối tượng Thread đang hoạt động.
isDaemon() Trả về true nếu luồng là daemon.
isAlive () Trả về true nếu thread vẫn còn tồn tại.
Các phương thức lớp chủ đề
khởi đầu() Bắt đầu hoạt động của một chủ đề. Nó chỉ được gọi một lần cho mỗi luồng vì nó sẽ gây ra lỗi thời gian chạy nếu được gọi nhiều lần.
chạy() Phương thức này biểu thị hoạt động của một luồng và có thể bị ghi đè bởi một lớp mở rộng lớp Thread.
tham gia() Nó chặn việc thực thi mã khác cho đến khi luồng mà phương thức join() được gọi trên đó bị chấm dứt.

Cốt truyện: Lớp chủ đề

Trước khi bắt đầu lập trình đa luồng bằng cách sử dụng mô-đun threading, điều quan trọng là phải hiểu lớp Thread. Lớp Thread là lớp chính định nghĩa khuôn mẫu và các thao tác của một luồng trong lập trình. Python.

Cách phổ biến nhất để tạo một ứng dụng đa luồng Python Ứng dụng này nhằm mục đích khai báo một lớp kế thừa từ lớp Thread và ghi đè phương thức run() của lớp đó.

Tóm lại, lớp Thread biểu thị một chuỗi mã chạy trong một Chủ đề kiểm soát.

Vì vậy, khi viết một ứng dụng đa luồng, bạn sẽ thực hiện như sau:

  1. Định nghĩa một lớp kế thừa từ lớp Thread.
  2. Ghi đè __trong đó__ constructor
  3. Ghi đè chạy() phương pháp

Khi một đối tượng luồng đã được tạo, khởi đầu() phương pháp này có thể được sử dụng để bắt đầu thực hiện hoạt động này, và tham gia() phương pháp này có thể được sử dụng để chặn tất cả các mã khác cho đến khi hoạt động hiện tại kết thúc.

Bây giờ, chúng ta hãy thử sử dụng mô-đun luồng để triển khai ví dụ trước đó của bạn. Một lần nữa, hãy khởi động lại chương trình của bạn. IDLE và nhập nội dung sau:

import time
import threading

class threadtester (threading.Thread):
    def __init__(self, id, name, i):
       threading.Thread.__init__(self)
       self.id = id
       self.name = name
       self.i = i
       
    def run(self):
       thread_test(self.name, self.i, 5)
       print ("%s has finished execution " %self.name)

def thread_test(name, wait, i):

    while i:
       time.sleep(wait)
       print ("Running %s \n" %name)
       i = i - 1

if __name__=="__main__":
    thread1 = threadtester(1, "First Thread", 1)
    thread2 = threadtester(2, "Second Thread", 2)
    thread3 = threadtester(3, "Third Thread", 3)

    thread1.start()
    thread2.start()
    thread3.start()

    thread1.join()
    thread2.join()
    thread3.join()

Đây sẽ là đầu ra khi bạn thực thi đoạn mã trên:

Cốt truyện: Lớp chủ đề

GIẢI THÍCH MÃ

Cốt truyện: Lớp chủ đề

  1. Phần này tương tự như ví dụ trước. Tại đây, bạn nhập mô-đun thời gian và luồng, được sử dụng để xử lý việc thực thi và độ trễ của... Python chủ đề.
  2. Trong phần này, bạn đang tạo một lớp có tên là threadtester, lớp này kế thừa hoặc mở rộng Sợi chỉ lớp của mô-đun luồng. Đây là một trong những cách phổ biến nhất để tạo chủ đề trong Python. Tuy nhiên, bạn chỉ nên ghi đè hàm tạo và chạy() phương pháp trong ứng dụng của bạn. Như bạn có thể thấy trong mẫu mã ở trên, __trong đó__ phương thức (hàm tạo) đã bị ghi đè. Tương tự, bạn cũng đã ghi đè chạy() phương pháp. Nó chứa mã mà bạn muốn thực thi bên trong một luồng. Trong ví dụ này, bạn đã gọi hàm thread_test().
  3. Đây là phương thức thread_test(), phương thức này nhận giá trị là i Biến i được truyền vào làm đối số, giảm giá trị của nó đi 1 ở mỗi lần lặp và lặp lại phần còn lại của mã cho đến khi i bằng 0. Ở mỗi lần lặp, nó in ra tên của luồng đang thực thi và tạm dừng trong wait giây (thời gian này cũng được truyền vào làm đối số).
  4. thread1 = threadtester(1, “First Thread”, 1) Ở đây, chúng ta đang tạo một luồng và truyền ba tham số mà chúng ta đã khai báo trong __init__. Tham số đầu tiên là id của luồng, tham số thứ hai là tên của luồng và tham số thứ ba là bộ đếm, xác định số lần vòng lặp while sẽ chạy.
  5. Phương thức `thread2.start()` được sử dụng để bắt đầu thực thi một luồng. Bên trong, hàm `start()` gọi phương thức `run()` của lớp bạn đang xử lý.
  6. thread3.join() Phương thức join() chặn việc thực thi mã khác và đợi cho đến khi luồng mà nó được gọi kết thúc.

Như bạn đã biết, các luồng trong cùng một tiến trình có quyền truy cập vào bộ nhớ và dữ liệu của tiến trình đó. Do đó, nếu nhiều hơn một luồng cố gắng thay đổi hoặc truy cập dữ liệu cùng lúc, lỗi có thể xảy ra.

Trong phần tiếp theo, bạn sẽ thấy các loại sự cố khác nhau có thể xảy ra khi các luồng truy cập dữ liệu và vùng quan trọng mà không kiểm tra các giao dịch truy cập hiện có.

Bế tắc và điều kiện chạy đua

Trước khi tìm hiểu về tắc nghẽn và điều kiện tranh chấp, việc hiểu một vài định nghĩa cơ bản liên quan đến lập trình song song sẽ rất hữu ích:

  • Phần quan trọng: Đó là một đoạn mã truy cập hoặc sửa đổi các biến dùng chung và phải được thực hiện như một giao dịch nguyên tử.
  • Chuyển đổi ngữ cảnh: Đó là quá trình mà CPU thực hiện để lưu trữ trạng thái của một luồng trước khi chuyển từ tác vụ này sang tác vụ khác, để sau đó có thể tiếp tục từ cùng một điểm.

Bế tắc

Bế tắc là vấn đề đáng sợ nhất mà các nhà phát triển phải đối mặt khi viết các ứng dụng đồng thời/đa luồng trong PythonCách tốt nhất để hiểu về tình trạng tắc nghẽn là sử dụng ví dụ bài toán kinh điển trong khoa học máy tính, được gọi là... Nhà Hàng PhiloVấn đề sophers.

Câu hỏi đặt ra cho các triết gia ăn uống như sau:

Năm nhà triết học đang ngồi quanh một chiếc bàn tròn với năm đĩa mì Ý (một loại mì ống) và năm chiếc nĩa, như thể hiện trong sơ đồ.

Nhà Hàng Philovấn đề sophers

Nhà Hàng Philovấn đề sophers

Vào bất kỳ thời điểm nào, một triết gia phải đang ăn hoặc đang suy nghĩ.

Hơn nữa, một triết gia phải lấy hai chiếc nĩa gần mình (tức là nĩa trái và nĩa phải) trước khi có thể ăn mì spaghetti. Vấn đề bế tắc xảy ra khi cả năm triết gia cùng cầm nĩa phải của mình lên cùng một lúc.

Vì mỗi triết gia đều có một chiếc nĩa, nên họ sẽ đợi những người khác đặt nĩa xuống. Kết quả là không ai trong số họ có thể ăn được mì spaghetti.

Tương tự như vậy, trong một hệ thống đồng thời, bế tắc xảy ra khi các luồng hoặc quy trình khác nhau (philosophers) cố gắng có được các tài nguyên hệ thống được chia sẻ (fork) cùng một lúc. Kết quả là, không có quy trình nào có cơ hội thực thi vì chúng đang chờ một tài nguyên khác do một quy trình khác nắm giữ.

Điều kiện cuộc đua

Tình trạng tranh chấp tài nguyên (race condition) là một trạng thái không mong muốn của chương trình xảy ra khi hệ thống thực hiện hai hoặc nhiều thao tác đồng thời. Ví dụ, hãy xem xét vòng lặp for đơn giản này:

i=0; # a global variable
for x in range(100):
    print(i)
    i+=1;

Nếu bạn tạo n Với số lượng luồng chạy đoạn mã này cùng một lúc, bạn không thể xác định giá trị của i (được chia sẻ bởi các luồng) khi chương trình kết thúc thực thi. Điều này là do trong môi trường đa luồng thực tế, các luồng có thể chồng chéo nhau, và giá trị của i được một luồng truy xuất và sửa đổi có thể thay đổi trong quá trình một luồng khác truy cập vào nó.

Đây là hai loại vấn đề chính có thể xảy ra trong môi trường đa luồng hoặc phân tán. Python ứng dụng. Trong phần tiếp theo, bạn sẽ học cách khắc phục vấn đề này bằng cách đồng bộ hóa các luồng.

Syncchủ đề đồng bộ hóa

Để giải quyết các tình trạng chạy đua, bế tắc và các vấn đề dựa trên luồng khác, mô-đun luồng cung cấp Khóa object. Ý tưởng là khi một luồng muốn truy cập vào một tài nguyên cụ thể, nó sẽ có được một khóa cho tài nguyên đó. Khi một luồng khóa một tài nguyên cụ thể, không luồng nào khác có thể truy cập vào tài nguyên đó cho đến khi khóa được giải phóng. Kết quả là, các thay đổi đối với tài nguyên sẽ là nguyên tử và các điều kiện chạy đua sẽ được ngăn chặn.

Khóa là một nguyên thủy đồng bộ hóa cấp thấp được triển khai bởi _chủ đề mô-đun. Tại bất kỳ thời điểm nào, một khóa có thể ở một trong hai trạng thái: khóa or đã mở khóa. Nó hỗ trợ hai phương pháp:

  1. giành được(): Khi trạng thái khóa được mở, việc gọi phương thức acquire() sẽ thay đổi trạng thái thành locked và trả về. Tuy nhiên, nếu trạng thái đang là locked, lệnh gọi acquire() sẽ bị chặn cho đến khi phương thức release() được gọi bởi một luồng khác.
  2. giải phóng(): Phương thức phát hành () được sử dụng để đặt trạng thái thành đã mở khóa, tức là để giải phóng khóa. Nó có thể được gọi bởi bất kỳ luồng nào, không nhất thiết phải là luồng đã lấy được khóa.

Đây là một ví dụ về cách sử dụng khóa trong ứng dụng của bạn. Khởi động ứng dụng của bạn! IDLE và nhập nội dung sau:

import threading
lock = threading.Lock()

def first_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the first funcion')
        lock.release()

def second_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the second funcion')
        lock.release()

if __name__=="__main__":
    thread_one = threading.Thread(target=first_function)
    thread_two = threading.Thread(target=second_function)

    thread_one.start()
    thread_two.start()

    thread_one.join()
    thread_two.join()

Bây giờ, nhấn F5. Bạn sẽ thấy một kết quả như thế này:

Syncchủ đề đồng hồ hóa

GIẢI THÍCH MÃ

Syncchủ đề đồng hồ hóa

  1. Ở đây, bạn chỉ cần tạo một khóa mới bằng cách gọi luồng.Lock() chức năng của nhà máy. Trong nội bộ, Lock() trả về một phiên bản của lớp Lock cụ thể hiệu quả nhất được nền tảng duy trì.
  2. Trong câu lệnh đầu tiên, bạn lấy được khóa bằng cách gọi phương thức thu được(). Khi khóa đã được cấp, bạn in “đã lấy được khóa” đến bảng điều khiển. Khi tất cả mã mà bạn muốn luồng chạy đã thực thi xong, bạn giải phóng khóa bằng cách gọi phương thức Release().

Về lý thuyết thì ổn, nhưng làm sao bạn biết chắc chắn rằng khóa đã hoạt động? Nếu bạn nhìn vào kết quả đầu ra, bạn sẽ thấy rằng mỗi câu lệnh print chỉ in ra đúng một dòng tại một thời điểm. Hãy nhớ lại ví dụ trước đó, kết quả đầu ra từ hàm print bị xáo trộn vì nhiều luồng truy cập phương thức print() cùng một lúc. Ở đây, hàm print chỉ được gọi sau khi khóa đã được giành quyền. Vì vậy, kết quả đầu ra được hiển thị từng dòng một.

Ngoài ổ khóa, Python Ngoài ra, hệ thống còn hỗ trợ một số cơ chế khác để xử lý đồng bộ hóa luồng, như được liệt kê bên dưới:

  1. RKhóa
  2. Semaphores
  3. Điều kiện
  4. Sự kiện và
  5. Rào cản

Khóa phiên dịch toàn cầu (và cách xử lý nó)

Trước khi đi vào chi tiết của PythonGIL thân mến, chúng ta hãy cùng định nghĩa một vài thuật ngữ sẽ hữu ích trong việc hiểu phần tiếp theo:

  1. Mã phụ thuộc vào CPU: thuật ngữ này đề cập đến bất kỳ đoạn mã nào sẽ được CPU thực thi trực tiếp.
  2. Mã phụ thuộc vào I/O: đây có thể là bất kỳ đoạn mã nào truy cập hệ thống tập tin thông qua hệ điều hành.
  3. CPython: đó là tài liệu tham khảo thực hiện of Python và có thể được mô tả như trình thông dịch được viết bằng C và Python (ngôn ngữ lập trình).

GIL trong đó là gì Python?

Khóa thông dịch viên toàn cầu (GIL) in Python GIL (Global Interpreter Lock) là một loại khóa tiến trình hoặc mutex được sử dụng khi làm việc với các tiến trình. Nó đảm bảo rằng chỉ một luồng có thể truy cập vào một tài nguyên cụ thể tại một thời điểm, và nó cũng ngăn chặn việc sử dụng đồng thời các đối tượng và mã bytecode. Điều này mang lại lợi ích cho các chương trình đơn luồng bằng cách tăng hiệu suất. GIL trong... Python rất đơn giản và dễ thực hiện.

Khóa có thể được sử dụng để đảm bảo rằng chỉ có một luồng có quyền truy cập vào một tài nguyên cụ thể tại một thời điểm nhất định.

Một trong những tính năng của Python là nó sử dụng khóa chung trên mỗi quy trình thông dịch, có nghĩa là mọi quy trình đều xử lý Python chính trình thông dịch như một nguồn tài nguyên.

Ví dụ, giả sử bạn đã viết một Python Chương trình này sử dụng hai luồng để thực hiện cả các thao tác CPU và 'I/O'. Khi bạn chạy chương trình này, đây là những gì sẽ xảy ra:

  1. Python Trình thông dịch tạo ra một tiến trình mới và sinh ra các luồng.
  2. Khi thread-1 bắt đầu chạy, trước tiên nó sẽ lấy GIL và khóa nó.
  3. Nếu thread-2 muốn thực thi ngay bây giờ, nó sẽ phải đợi GIL được giải phóng ngay cả khi bộ xử lý khác rảnh.
  4. Bây giờ, giả sử luồng-1 đang chờ thao tác I/O. Tại thời điểm này, nó sẽ giải phóng GIL và thread-2 sẽ thu được nó.
  5. Sau khi hoàn thành các thao tác I/O, nếu luồng-1 muốn thực thi ngay bây giờ, nó sẽ lại phải đợi GIL được giải phóng bởi luồng-2.

Do đó, chỉ có một luồng có thể truy cập trình thông dịch bất cứ lúc nào, nghĩa là sẽ chỉ có một luồng thực thi Python Mã tại một thời điểm nhất định.

Điều này ổn đối với bộ xử lý đơn nhân vì nó sẽ sử dụng phương pháp phân chia thời gian (xem phần đầu tiên của hướng dẫn này) để xử lý các luồng. Tuy nhiên, trong trường hợp bộ xử lý đa nhân, một hàm phụ thuộc vào CPU thực thi trên nhiều luồng sẽ ảnh hưởng đáng kể đến hiệu suất của chương trình vì nó sẽ không thực sự sử dụng tất cả các lõi có sẵn cùng một lúc.

Tại sao cần GIL?

CPython Trình thu gom rác sử dụng một kỹ thuật quản lý bộ nhớ hiệu quả được gọi là đếm tham chiếu. Cách thức hoạt động như sau: Mỗi đối tượng trong Python Một đối tượng có số lượng tham chiếu, số lượng này tăng lên khi nó được gán cho một tên biến mới hoặc được thêm vào một vùng chứa (như bộ dữ liệu, danh sách, v.v.). Tương tự, số lượng tham chiếu giảm đi khi tham chiếu nằm ngoài phạm vi hoặc khi câu lệnh `del` được gọi. Khi số lượng tham chiếu của một đối tượng đạt đến 0, nó sẽ được thu gom rác và bộ nhớ được cấp phát sẽ được giải phóng.

Nhưng vấn đề là biến số tham chiếu dễ bị ảnh hưởng bởi các điều kiện giống như bất kỳ biến toàn cục nào khác. Để giải quyết vấn đề này, các nhà phát triển Python Chúng tôi quyết định sử dụng khóa trình thông dịch toàn cục. Lựa chọn khác là thêm khóa vào từng đối tượng, điều này sẽ dẫn đến tình trạng bế tắc và tăng chi phí từ các lệnh acquire() và release().

Vì vậy, GIL là một hạn chế đáng kể cho đa luồng Python các chương trình chạy các tác vụ nặng về CPU (thực chất là các chương trình đơn luồng). Nếu bạn muốn sử dụng nhiều lõi CPU trong ứng dụng của mình, hãy sử dụng đa xử lý mô-đun thay thế.

Câu Hỏi Thường Gặp

Sử dụng đa luồng (threading) cho các tác vụ phụ thuộc vào I/O như gọi mạng, truy cập tập tin hoặc truy vấn cơ sở dữ liệu, nơi các luồng chờ đợi tài nguyên bên ngoài. Sử dụng đa xử lý (multiprocessing) cho các tác vụ phụ thuộc vào CPU như tính toán phức tạp, vì nó chạy trên nhiều lõi và bỏ qua GIL (Global Interpreter Lock).

Không phải theo mặc định. PEP 703 đã giới thiệu một bản dựng đa luồng tùy chọn trong Python 3.13 vô hiệu hóa GIL, và Python Phiên bản 3.14 tiếp tục duy trì tính năng này. Các bản dựng tiêu chuẩn vẫn tích hợp GIL, vì vậy hầu hết các chương trình vẫn hoạt động chính xác như trước.

Luồng nền (daemon thread) chạy ngầm và không ngăn chương trình thoát. Khi chỉ còn lại các luồng nền, Python Tắt chúng và thoát. Hãy thiết lập một tiến trình với `thread.daemon = True` trước khi gọi `start()`.

Lớp Thread không trả về trực tiếp kết quả của phương thức run(). Hãy lưu trữ giá trị đó vào đối tượng luồng hoặc hàng đợi dùng chung, hoặc sử dụng concurrent.futures.ThreadPoolExecutor, phương thức submit() của nó trả về một Future mà bạn có thể đọc bằng result().

ThreadPoolExecutor là một công cụ hỗ trợ cấp cao quản lý một nhóm các luồng làm việc cho bạn. Bạn gửi các tác vụ bằng submit() hoặc map(), và nó sẽ xử lý việc tạo, tái sử dụng và dọn dẹp luồng, trả về các đối tượng Future chứa kết quả của mỗi luồng.

Bạn có thể tạo ra hàng trăm hoặc hàng nghìn luồng, nhưng GIL chỉ cho phép một luồng thực thi. Python Mã bytecode tại bất kỳ thời điểm nào. Chúng vẫn chồng chéo nhau trong quá trình chờ I/O, vì vậy nhiều luồng đồng thời giúp các chương trình phụ thuộc vào I/O hiệu quả hơn nhiều so với các chương trình phụ thuộc vào CPU.

Đúng vậy, chủ yếu là cho các bước phụ thuộc vào I/O như tải dữ liệu, gọi API hoặc đọc tệp. Quá trình huấn luyện nặng phụ thuộc vào CPU, vì vậy các thư viện như NumPy và Py rất hữu ích.TorHãy giải phóng GIL trong mã C hoặc dựa vào xử lý đa tiến trình để đạt được song song thực sự.

Đúng vậy. GitHub Copilot tự động hoàn thành các mẫu phổ biến như lớp con của Thread, cách sử dụng Lock và thiết lập ThreadPoolExecutor từ tên chú thích hoặc tên hàm. Luôn luôn xem lại mã được tạo ra để kiểm tra các điều kiện tranh chấp và cách xử lý khóa chính xác trước khi dựa vào nó.

Tóm tắt bài viết này với: