Hướng dẫn understanding the python gil - hiểu con trăn gil

Khóa thông dịch viên toàn cầu Python hoặc Gil, theo các từ đơn giản, là một mutex (hoặc khóa) chỉ cho phép một luồng giữ quyền kiểm soát của trình thông dịch Python.

Điều này có nghĩa là chỉ có một luồng có thể ở trong trạng thái thực thi tại bất kỳ thời điểm nào. Tác động của GIL không thể nhìn thấy đối với các nhà phát triển thực hiện các chương trình đơn luồng, nhưng nó có thể là một nút cổ chai hiệu suất trong mã liên kết CPU và nhiều luồng.

Vì Gil chỉ cho phép một luồng thực hiện tại một thời điểm ngay cả trong một kiến ​​trúc đa luồng với nhiều hơn một lõi CPU, Gil đã nổi tiếng như một tính năng khét tiếng của Python.

Trong bài viết này, bạn sẽ tìm hiểu làm thế nào Gil ảnh hưởng đến hiệu suất của các chương trình Python của bạn và cách bạn có thể giảm thiểu tác động của nó đối với mã của bạn.

Gil đã giải quyết vấn đề gì cho Python?

Python sử dụng đếm tham chiếu để quản lý bộ nhớ. Điều đó có nghĩa là các đối tượng được tạo trong Python có biến đếm tham chiếu để theo dõi số lượng tham chiếu trỏ đến đối tượng. Khi số lượng này đạt đến 0, bộ nhớ bị chiếm bởi đối tượng được phát hành.

Hãy cùng xem một ví dụ về mã ngắn gọn để chứng minh cách đếm tham chiếu hoạt động:

>>>

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

Trong ví dụ trên, số lượng tham chiếu cho đối tượng danh sách trống [] là 3. Đối tượng danh sách được tham chiếu bởi a,

# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)
0 và đối số được chuyển cho
# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)
1.

Quay lại Gil:

Vấn đề là biến số lượng tham chiếu này cần được bảo vệ khỏi các điều kiện chủng tộc trong đó hai luồng tăng hoặc giảm giá trị của nó đồng thời. Nếu điều này xảy ra, nó có thể gây ra bộ nhớ bị rò rỉ không bao giờ được phát hành hoặc thậm chí tệ hơn, giải phóng bộ nhớ không chính xác trong khi tham chiếu đến đối tượng đó vẫn tồn tại. Điều này có thể gây ra các vụ tai nạn hoặc các lỗi khác kỳ lạ khác trong các chương trình Python của bạn.

Biến số tham chiếu này có thể được giữ an toàn bằng cách thêm khóa vào tất cả các cấu trúc dữ liệu được chia sẻ trên các luồng để chúng không được sửa đổi không nhất quán.

Nhưng việc thêm một khóa vào mỗi đối tượng hoặc nhóm đối tượng có nghĩa là nhiều khóa sẽ tồn tại có thể gây ra một vấn đề khác, DEADLOCKS (bế tắc chỉ có thể xảy ra nếu có nhiều hơn một khóa). Một tác dụng phụ khác sẽ giảm hiệu suất do việc mua lại và phát hành khóa lặp đi lặp lại.

GIL là một khóa duy nhất trên chính trình thông dịch, thêm một quy tắc thực thi bất kỳ mã byte python nào yêu cầu có được khóa phiên dịch. Điều này ngăn ngừa bế tắc (vì chỉ có một khóa) và không giới thiệu nhiều hiệu suất. Nhưng nó có hiệu quả làm cho bất kỳ chương trình Python liên kết với CPU nào.

Gil, mặc dù được các phiên dịch viên sử dụng cho các ngôn ngữ khác như Ruby, không phải là giải pháp duy nhất cho vấn đề này. Một số ngôn ngữ tránh được yêu cầu của Gil đối với quản lý bộ nhớ an toàn bằng luồng bằng cách sử dụng các phương pháp khác ngoài việc đếm tham chiếu, chẳng hạn như thu gom rác.

Mặt khác, điều này có nghĩa là các ngôn ngữ đó thường phải bù đắp cho việc mất lợi ích hiệu suất có ren đơn của GIL bằng cách thêm các tính năng tăng hiệu suất khác như trình biên dịch JIT.

Tại sao Gil được chọn làm giải pháp?

Vì vậy, tại sao một cách tiếp cận dường như rất cản trở được sử dụng trong Python? Đó có phải là một quyết định tồi của các nhà phát triển Python?

Chà, theo lời của Larry Hastings, quyết định thiết kế của Gil là một trong những điều khiến Python trở nên phổ biến như ngày nay.

Python đã xuất hiện từ những ngày mà các hệ điều hành không có khái niệm về chủ đề. Python được thiết kế để dễ sử dụng để làm cho sự phát triển nhanh hơn và ngày càng nhiều nhà phát triển bắt đầu sử dụng nó.

Rất nhiều tiện ích mở rộng đã được viết cho các thư viện C hiện tại có các tính năng cần thiết trong Python. Để ngăn chặn những thay đổi không nhất quán, các tiện ích mở rộng C này yêu cầu quản lý bộ nhớ an toàn luồng mà GiL cung cấp.

Gil rất đơn giản để thực hiện và dễ dàng được thêm vào Python. Nó cung cấp hiệu suất tăng cho các chương trình đơn luồng vì chỉ cần một khóa cần được quản lý.

Các thư viện C không an toàn chủ đề trở nên dễ dàng hơn để tích hợp. Và các phần mở rộng C này đã trở thành một trong những lý do tại sao Python được các cộng đồng khác nhau chấp nhận.

Như bạn có thể thấy, Gil là một giải pháp thực dụng cho một vấn đề khó khăn mà các nhà phát triển Cpython phải đối mặt sớm trong cuộc sống của Python.

Tác động đến các chương trình Python đa luồng

Khi bạn nhìn vào một chương trình Python điển hình, hoặc bất kỳ chương trình máy tính nào cho vấn đề đó, đó là một sự khác biệt giữa các chương trình CPU trong hiệu suất của họ và các chương trình được giới thiệu I/O.

Các chương trình ràng buộc CPU là những chương trình đang đẩy CPU đến giới hạn của nó. Điều này bao gồm các chương trình thực hiện các tính toán toán học như phép nhân ma trận, tìm kiếm, xử lý hình ảnh, v.v.

Các chương trình ràng buộc I/O là những chương trình dành thời gian chờ đợi đầu vào/đầu ra có thể đến từ người dùng, tệp, cơ sở dữ liệu, mạng, v.v. Nhận những gì họ cần từ nguồn do thực tế là nguồn có thể cần thực hiện xử lý riêng trước khi đầu vào/đầu ra đã sẵn sàng, ví dụ, người dùng nghĩ về những gì cần nhập vào dấu nhắc đầu vào hoặc truy vấn cơ sở dữ liệu chạy trong quá trình riêng.

Hãy cùng xem một chương trình ràng buộc CPU đơn giản thực hiện đếm ngược:

# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)

Chạy mã này trên hệ thống của tôi với 4 lõi đã cho đầu ra sau:

$ python single_threaded.py
Time taken in seconds - 6.20024037361145

Bây giờ tôi đã sửa đổi mã một chút để làm với cùng một đếm ngược bằng hai luồng song song:

# multi_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()

print('Time taken in seconds -', end - start)

Và khi tôi chạy nó một lần nữa:

$ python multi_threaded.py
Time taken in seconds - 6.924342632293701

Như bạn có thể thấy, cả hai phiên bản đều mất gần như cùng một thời gian để hoàn thành. Trong phiên bản đa luồng, Gil đã ngăn chặn các luồng liên kết CPU thực hiện trong Parellel.

GIL không có nhiều tác động đến hiệu suất của các chương trình đa luồng ràng buộc I/O vì khóa được chia sẻ giữa các luồng trong khi chúng đang chờ I/O.

Nhưng một chương trình có chủ đề hoàn toàn ràng buộc CPU, ví dụ, một chương trình xử lý hình ảnh trong các phần sử dụng các luồng, sẽ không chỉ trở thành một luồng do khóa mà còn thấy thời gian thực hiện tăng lên, như đã thấy trong ví dụ trên , so với một kịch bản mà nó được viết là hoàn toàn đơn lẻ.

Sự gia tăng này là kết quả của việc mua và phát hành chi phí được thêm vào bởi khóa.

Tại sao Gil đã bị loại bỏ?

Các nhà phát triển của Python nhận được rất nhiều khiếu nại về điều này nhưng một ngôn ngữ phổ biến như Python không thể mang lại sự thay đổi đáng kể như việc loại bỏ GiL mà không gây ra các vấn đề không tương thích ngược.

Gil rõ ràng có thể được loại bỏ và điều này đã được thực hiện nhiều lần trong quá khứ bởi các nhà phát triển và nhà nghiên cứu nhưng tất cả những nỗ lực đó đã phá vỡ các phần mở rộng C hiện tại phụ thuộc rất nhiều vào giải pháp mà Gil cung cấp.

Tất nhiên, có những giải pháp khác cho vấn đề mà Gil giải quyết nhưng một số trong số chúng làm giảm hiệu suất của các chương trình I/O đơn và đa luồng và một số trong số chúng quá khó khăn. Rốt cuộc, bạn sẽ muốn các chương trình Python hiện tại của mình chạy chậm hơn sau khi một phiên bản mới ra mắt, phải không?

Người tạo ra và BDFL của Python, Guido Van Rossum, đã đưa ra câu trả lời cho cộng đồng vào tháng 9 năm 2007 trong bài viết của mình, Thật dễ dàng để loại bỏ Gil Gil:

Chỉ có một tập hợp các bản vá vào PY3K chỉ khi hiệu suất cho chương trình đơn luồng (và cho một chương trình đa luồng nhưng liên kết với I/O) không làm giảm

Và điều kiện này đã được thực hiện bởi bất kỳ nỗ lực nào được thực hiện kể từ đó.

Tại sao nó lại bị loại bỏ trong Python 3?

Python 3 đã có cơ hội bắt đầu nhiều tính năng từ đầu và trong quá trình này, đã phá vỡ một số phần mở rộng C hiện có, sau đó yêu cầu các thay đổi được cập nhật và chuyển để làm việc với Python 3. Đây là lý do tại sao các phiên bản đầu của Python 3 thấy sự chấp nhận chậm hơn của cộng đồng.

Nhưng tại sao không phải Gil Gil bị loại bỏ cùng?

Việc loại bỏ Gil sẽ làm cho Python 3 chậm hơn so với Python 2 trong hiệu suất duy nhất và bạn có thể tưởng tượng điều gì sẽ dẫn đến. Bạn có thể tranh luận với lợi ích hiệu suất đơn của Gil. Vì vậy, kết quả là Python 3 vẫn có Gil.

Nhưng Python 3 đã mang đến một sự cải thiện lớn cho Gil hiện tại

Chúng tôi đã thảo luận về tác động của Gil đối với chỉ các chương trình đa luồng giới hạn của CPU và chỉ có một số chương trình mà một số chủ đề là I/O bị ràng buộc và một số bị ràng buộc bởi CPU thì sao?

Trong các chương trình như vậy, Python sườn Gil được biết là bỏ đói các chủ đề I/O bằng cách không cho họ cơ hội để có được Gil từ các chủ đề ràng buộc CPU.

Điều này là do một cơ chế được xây dựng thành Python buộc các chủ đề phải giải phóng GIL sau một khoảng thời gian sử dụng liên tục cố định và nếu không ai khác có được GIL, cùng một luồng có thể tiếp tục sử dụng.after a fixed interval of continuous use and if nobody else acquired the GIL, the same thread could continue its use.

>>>

>>> import sys
>>> # The interval is set to 100 instructions:
>>> sys.getcheckinterval()
100

Vấn đề trong cơ chế này là hầu hết thời gian luồng liên kết CPU sẽ tự phục hồi Gil trước khi các luồng khác có thể có được nó. Điều này được nghiên cứu bởi David Beazley và trực quan hóa có thể được tìm thấy ở đây.

Vấn đề này đã được khắc phục trong Python 3.2 vào năm 2009 bởi Antoine Pitrou, người đã thêm một cơ chế xem xét số lượng yêu cầu mua lại GIL bằng các chủ đề khác đã bị hủy và không cho phép chủ đề hiện tại phản ứng lại Gil trước khi các chủ đề khác có cơ hội chạy.

Cách đối phó với Python từ Gil

Nếu Gil gây ra vấn đề cho bạn, ở đây một vài cách tiếp cận bạn có thể thử:

Đa xử lý VS đa luồng: Cách phổ biến nhất là sử dụng phương pháp đa xử lý nơi bạn sử dụng nhiều quy trình thay vì các luồng. Mỗi quá trình Python đều có trình thông dịch Python và không gian bộ nhớ riêng để Gil giành chiến thắng là một vấn đề. Python có mô -đun

# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)
2 cho phép chúng tôi tạo các quy trình dễ dàng như thế này: The most popular way is to use a multi-processing approach where you use multiple processes instead of threads. Each Python process gets its own Python interpreter and memory space so the GIL won’t be a problem. Python has a
# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)
2 module which lets us create processes easily like this:

from multiprocessing import Pool
import time

COUNT = 50000000
def countdown(n):
    while n>0:
        n -= 1

if __name__ == '__main__':
    pool = Pool(processes=2)
    start = time.time()
    r1 = pool.apply_async(countdown, [COUNT//2])
    r2 = pool.apply_async(countdown, [COUNT//2])
    pool.close()
    pool.join()
    end = time.time()
    print('Time taken in seconds -', end - start)

Chạy này trên hệ thống của tôi đã cho đầu ra này:

$ python multiprocess.py
Time taken in seconds - 4.060242414474487

Một sự gia tăng hiệu suất tốt so với phiên bản đa luồng, phải không?

Thời gian didn đã giảm xuống một nửa so với những gì chúng ta đã thấy ở trên vì quản lý quy trình có chi phí riêng. Nhiều quy trình nặng hơn nhiều luồng, vì vậy, hãy nhớ rằng điều này có thể trở thành một nút cổ chai tỷ lệ.

Thông dịch viên Python thay thế: Python có nhiều triển khai phiên dịch. Cpython, Jython, Ironpython và Pypy, được viết bằng C, Java, C# và Python, là những cái phổ biến nhất. Gil chỉ tồn tại trong triển khai Python ban đầu là Cpython. Nếu chương trình của bạn, với các thư viện của nó, có sẵn cho một trong các triển khai khác thì bạn cũng có thể thử chúng. Python has multiple interpreter implementations. CPython, Jython, IronPython and PyPy, written in C, Java, C# and Python respectively, are the most popular ones. GIL exists only in the original Python implementation that is CPython. If your program, with its libraries, is available for one of the other implementations then you can try them out as well.

Chỉ cần chờ đợi nó: Trong khi nhiều người dùng Python tận dụng lợi ích hiệu suất đơn của Gil. Các lập trình viên đa luồng don don phải băn khoăn như một số tâm trí sáng giá nhất trong cộng đồng Python đang làm việc để loại bỏ Gil khỏi Cpython. Một nỗ lực như vậy được gọi là Gilectomy. While many Python users take advantage of the single-threaded performance benefits of GIL. The multi-threading programmers don’t have to fret as some of the brightest minds in the Python community are working to remove the GIL from CPython. One such attempt is known as the Gilectomy.

Python Gil thường được coi là một chủ đề bí ẩn và khó khăn. Nhưng hãy nhớ rằng là một Pythonista, bạn thường chỉ bị ảnh hưởng bởi nó nếu bạn đang viết các phần mở rộng C hoặc nếu bạn sử dụng nhiều luồng liên kết CPU trong các chương trình của mình.

Trong trường hợp đó, bài viết này sẽ cung cấp cho bạn mọi thứ bạn cần để hiểu Gil là gì và làm thế nào để đối phó với nó trong các dự án của riêng bạn. Và nếu bạn muốn hiểu hoạt động bên trong cấp thấp của Gil, tôi sẽ khuyên bạn nên xem The Hiểu về Python Gil Talk của David Beazley.