Khóa học
Nếu bạn mới bắt đầu với Python và muốn học thêm, hãy tham gia khóa Giới thiệu Khoa học Dữ liệu với Python của DataCamp.
Global interpreter lock (GIL) là một cơ chế áp dụng khóa toàn cục cho trình thông dịch. Nó được dùng trong các trình thông dịch ngôn ngữ lập trình để đồng bộ hóa và quản lý việc thực thi các luồng, sao cho chỉ một luồng gốc (được hệ điều hành lập lịch) có thể thực thi tại một thời điểm.
Trong bối cảnh có nhiều luồng, có thể xảy ra tình huống cả hai luồng cố gắng truy cập vùng nhớ cùng lúc, dẫn đến việc ghi đè dữ liệu trong bộ nhớ. Do đó cần có một cơ chế giúp ngăn chặn hiện tượng này.
Một số trình thông dịch phổ biến có GIL là CPython và Ruby MRI. Như nhiều bạn đã biết, Python là ngôn ngữ thông dịch, có các bản phân phối như CPython, Jython, IronPython. Trong số đó, chỉ CPython hỗ trợ GIL, và đây cũng là bản triển khai Python được dùng rộng rãi nhất. CPython được phát triển chủ yếu bằng C và Python để hỗ trợ, làm việc tốt với các ứng dụng có nhiều thành phần C phía dưới.
Ngay cả khi bộ xử lý của bạn có nhiều nhân, một global interpreter chỉ cho phép một luồng được thực thi tại một thời điểm. Lý do là khi một luồng bắt đầu chạy, nó sẽ giữ global interpreter lock. Khi chờ một thao tác I/O (đọc/ghi dữ liệu từ/ra đĩa) hoặc một tác vụ nặng CPU (nhân/chia vector, ma trận), nó sẽ nhả khóa để các luồng khác của tiến trình có thể chạy. Vì thế, bạn không thể chạy các luồng khác đồng thời.

Hãy dành chút thời gian để hiểu sơ đồ trên. Như bạn thấy, có hàm factorial và hai luồng 1 và 2, luồng 1 đang ở trạng thái khóa còn luồng 2 ở trạng thái chờ. Điều này có nghĩa chỉ một trong hai luồng có thể truy cập hàm. Giả sử hàm factorial mất 2 giây để hoàn thành. Trong trường hợp lý tưởng, cả hai luồng lẽ ra có thể kết thúc trong 2 giây. Tuy nhiên, trong Python, hai luồng sẽ chạy nối tiếp chứ không chạy song song.
Hai luồng 1 và 2 gọi hàm factorial có thể mất thời gian gấp đôi so với một luồng gọi hàm đó hai lần. Điều này cũng cho thấy bộ quản lý bộ nhớ do trình thông dịch điều khiển không an toàn luồng (thread-safe), tức là nhiều luồng không thể truy cập đồng thời cùng một dữ liệu dùng chung.
Vì vậy, GIL (Nguồn: Understanding the Python GIL):
-
Giới hạn hoạt động đa luồng.
-
Hạn chế thực thi song song.
-
GIL đảm bảo chỉ có một luồng chạy trong trình thông dịch tại một thời điểm.
-
Giúp đơn giản hóa nhiều chi tiết cấp thấp như quản lý bộ nhớ.
Với GIL, đạt được tính toán hợp tác hay đa nhiệm điều phối thay vì tính toán song song.
Như có thể thấy từ sơ đồ trên, ban đầu có ba luồng; luồng 1 đang chạy và đã giữ GIL, khi thực hiện một thao tác I/O như đọc, ghi, v.v., luồng 1 nhả GIL và sau đó GIL được luồng 2 giữ. Chu kỳ này cứ tiếp diễn, và GIL luân phiên chuyển giữa các luồng cho đến khi chúng hoàn thành việc thực thi chương trình. Hãy nhớ rằng nếu bất kỳ luồng nào không có khóa và chưa hoàn thành thực thi, chúng sẽ ở trạng thái chờ.
GIL trong Python2 so với Python3

Global Interpreter Lock trong Python 2.7 hoạt động khác so với Python3. Trong một tác vụ thuần nặng CPU, luồng sẽ tiếp tục chạy vì không có thao tác I/O, khi đó các luồng khác sẽ ở trạng thái nhàn rỗi hoặc chờ, điều này không mong muốn. Để giải quyết, Python2 có khái niệm ticks. Global interpreter lock thực hiện kiểm tra để giám sát trạng thái của luồng như đang chờ, đang I/O hay đang chạy. Tuy nhiên, GIL không giám sát liên tục mọi thời điểm, mà dùng khái niệm ticks và kiểm tra các luồng sau mỗi 100 tick.
Một tick là một lệnh byte-code, và khi hoàn thành 100 lệnh byte-code, GIL sẽ kiểm tra xem các luồng đang chạy hay đang chờ. Cần lưu ý tick không liên quan đến thời gian vì nó là lệnh byte-code. Mỗi tick có thể mất thời gian dài hoặc ngắn hơn so với tick khác.
Việc kiểm tra định kỳ sau mỗi 100 tick này là cần thiết, đặc biệt với các tác vụ nặng CPU vì chúng không có thao tác I/O. Lưu ý bạn có thể điều chỉnh bộ đếm tick bằng mô-đun tích hợp sys của Python.
Hãy bàn về một số nhược điểm của cách tiếp cận trên:
Nay, nếu bạn nhìn vào sơ đồ này, bạn có thể nhận thấy các luồng không chạy song song mà theo thứ tự nối tiếp, còn gọi là tính toán hợp tác do global interpreter lock chi phối. Đồng thời, các luồng phải chờ đợi lâu hơn. Ví dụ, luồng T3 phải chờ cả hai luồng T1 & T2 nhả GIL. Vì vậy, các luồng bị đói khóa và tài nguyên tính toán.
Một nhược điểm khác là có sự cạnh tranh giữa các luồng để giành GIL. Ban đầu, có thể do mức ưu tiên mà T1 nhận GIL trước, rồi đến T2, v.v. Tuy nhiên, khi T3 nhả GIL, nó sẽ gửi thông báo đến tất cả các luồng khác, khi đó mọi luồng sẽ tranh nhau để giành GIL. Dẫu vậy, bạn có thể phần nào khắc phục bằng cách đặt mức ưu tiên cho từng luồng.
Trong Python3, mỗi luồng được cấp thời lượng cố định, tức 5ms thời gian thực thi. Điều này ngăn việc các luồng bị đói tài nguyên vì thời gian chờ là như nhau cho mọi luồng. Các luồng sẽ chờ xem GIL có được Luồng 1 chủ động nhả do thao tác I/O hoặc sleep hay không. Nếu không, nó sẽ buộc nhả sau 5ms. Do đó sẽ không có hiện tượng đói thời gian CPU vì mỗi luồng nhận được lượng thời gian ngang nhau, loại bỏ thách thức trong việc giành GIL.
Tuy nhiên, cần lưu ý rằng triển khai GIL mới ưu tiên nhiều hơn cho các công việc nặng CPU so với các thao tác I/O. Trong trường hợp một luồng đã nhả GIL và có hai luồng đang chờ, GIL sẽ được luồng có nhu cầu chạy tác vụ nặng CPU giữ, thay vì luồng còn lại.
Kết luận
Chúc mừng bạn đã hoàn thành hướng dẫn.
Hướng dẫn này đề cập đến một chủ đề nâng cao, nhằm cung cấp cho bạn cái nhìn tổng quan về cách GIL hoạt động trong Python. Vì hướng dẫn không bao gồm phần mã hóa về GIL và luồng, một bài tập hay cho bạn là tự thử nghiệm bằng cách tạo nhiều luồng và một phương thức thực hiện tác vụ nặng I/O, rồi phân tích cách GIL được chuyển đổi giữa từng luồng.
Vui lòng đặt câu hỏi liên quan đến hướng dẫn này ở phần bình luận bên dưới.
Tài liệu tham khảo:
Nếu bạn mới bắt đầu với Python và muốn học thêm, hãy tham gia khóa Giới thiệu Khoa học Dữ liệu với Python của DataCamp.