Trăn nhiều GIL

Một dự án mới nhằm thay đổi thời gian chạy CPython để tăng hiệu suất đa luồng đã thu hút sự chú ý của nhóm phát triển cốt lõi của Python

Trăn nhiều GIL
Bởi Serdar Yegulalp

Nhà văn cao cấp, InfoWorld.

Python stands to lose its GIL, and gain a lot of speedđá bóng (CC0)

Một trong những điểm yếu lâu nay của Python, không có khả năng mở rộng tốt trong môi trường đa luồng, là mục tiêu của một đề xuất mới giữa các nhà phát triển cốt lõi của ngôn ngữ lập trình phổ biến

Nhà phát triển Sam Gross đã đề xuất một thay đổi lớn đối với Khóa phiên dịch toàn cầu hoặc GIL—một thành phần chính trong CPython, triển khai tham chiếu của Python

[ Cũng trên InfoWorld. 11 mẹo tăng tốc chương trình Python ]

Nếu được chấp nhận, đề xuất của Gross sẽ viết lại cách Python tuần tự hóa quyền truy cập vào các đối tượng trong thời gian chạy của nó từ nhiều luồng và sẽ tăng đáng kể hiệu suất đa luồng

GIL từ lâu đã được coi là một trở ngại đối với hiệu suất đa luồng tốt hơn trong CPython (và do đó Python nói chung). Nhiều nỗ lực đã được thực hiện để loại bỏ nó trong nhiều năm, nhưng với cái giá phải trả là làm ảnh hưởng đến hiệu suất đơn luồng—nói cách khác, bằng cách làm cho phần lớn các ứng dụng Python hiện có chậm hơn

Các phép ẩn dụ hiện tại của Python để xử lý luồng và đa xử lý không làm cho nó không thể đạt được tính song song cao. Nhưng chúng gây khó khăn đến mức các nhà phát triển thường chuyển sang các mô-đun của bên thứ ba như Dask để hoàn thành công việc đó

Đề xuất mới thực hiện các thay đổi đối với cách đếm tham chiếu hoạt động đối với các đối tượng Python, để các tham chiếu từ luồng sở hữu một đối tượng được xử lý khác với các tham chiếu đến từ các luồng khác

Hiệu quả tổng thể của thay đổi này và một số thay đổi khác cùng với nó, thực sự làm tăng nhẹ hiệu suất đơn luồng—khoảng 10%, theo một số điểm chuẩn được thực hiện trên phiên bản rẽ nhánh của trình thông dịch so với CPython 3 dòng chính. 9 thông dịch viên. Hiệu suất đa luồng, trên một số tiêu chuẩn, tỷ lệ gần như tuyến tính với mỗi luồng mới trong trường hợp tốt nhất—e. g. , khi sử dụng 20 chủ đề, 18. Tăng tốc 1 lần trên một điểm chuẩn và 19. Tăng tốc 8 lần trên cái khác

Những thay đổi này đủ lớn để một số lượng lớn các thư viện Python hiện có hoạt động trực tiếp với các phần bên trong của Python (e. g. , Cython) sẽ cần phải được viết lại. Nhưng nhịp độ của lịch trình phát hành Python chỉ có nghĩa là những thay đổi đột phá như vậy sẽ cần được thực hiện trong một bản phát hành quan trọng thay vì một bản phát hành nhỏ

Có liên quan

  • con trăn
  • Ngôn ngữ lập trình
  • Phát triển phần mềm

Serdar Yegulalp là một nhà văn cao cấp tại InfoWorld, tập trung vào học máy, container hóa, devops, hệ sinh thái Python và đánh giá định kỳ

Nếu bạn đã làm việc trong hệ sinh thái Python một thời gian, rất có thể bạn đã nghe những lời phàn nàn về mô hình đa luồng của nó. Nhiều khả năng, những lời phàn nàn này tập trung vào GIL. Khóa phiên dịch toàn cầu Python.  

Bài viết này sẽ giúp bạn hiểu chức năng của GIL cũng như các tác động hiệu suất tiềm tàng của nó và cách bỏ qua nó khi cần thiết

Làm rõ đồng thời

Trước khi đào sâu vào Python GIL, hãy thiết lập một khuôn khổ chung xung quanh các khái niệm liên quan đến đồng thời.  

Đồng thời và song song

Đồng thời là một mô hình tính toán trong đó một tác vụ không khóa tài nguyên khi không hoạt động, cho phép các tác vụ khác sử dụng tài nguyên. Đồng thời có thể đạt được bằng cách sử dụng các chương trình đa luồng.  

Một chương trình đa luồng là một chương trình lập lịch biểu rõ ràng cho các luồng để thực thi các đoạn mã. Nếu các luồng này được chạy trên một CPU với cách tiếp cận không chặn, thì chương trình sẽ chạy theo mô hình đồng thời. Mặt khác, nếu các luồng này được chạy đồng thời trên một hoặc nhiều CPU thì chương trình sẽ chạy theo mô hình song song. Song song thường được coi là một chuyên môn hóa của mô hình đồng thời

Giới hạn I/O và CPU

Trong Python, các luồng được triển khai dưới dạng pthread (EEE POSIX 1003. 1c cho Linux và macOS). Pthreads là các chủ đề cấp hệ điều hành. Có nghĩa là, hệ điều hành máy chủ chịu trách nhiệm giám sát và lập lịch trình. Mặc dù điều này có thể gợi ý rằng các luồng Python có thể được HĐH lên lịch để chạy song song, nhưng trên thực tế, một chương trình Python đa luồng sẽ không bao giờ thực sự song song. Trong Python, GIL đảm bảo rằng một và chỉ một luồng có thể được thực thi tại một thời điểm.  

Chủ đề có thể được phân loại thành một trong hai loại

  • CPU bị ràng buộc. Chúng sử dụng CPU mạnh mẽ
  • ràng buộc I/O. Chúng thường bị chặn do thao tác I/O, khiến CPU không hoạt động

Điều quan trọng là phải biết chương trình của bạn có loại luồng nào, vì chúng sẽ xác định cách tiếp cận đồng thời nào nên được sử dụng. Ba cách tiếp cận có thể được minh họa trong bảng dưới đây.  

Chủ đề có thể được phân loại thành một trong hai loại

  • CPU bị ràng buộc. Chúng sử dụng CPU mạnh mẽ
  • ràng buộc I/O. Chúng thường bị chặn do thao tác I/O, khiến CPU không hoạt động

Điều quan trọng là phải biết chương trình của bạn có loại luồng nào, vì chúng sẽ xác định cách tiếp cận đồng thời nào nên được sử dụng. Ba cách tiếp cận có thể được minh họa trong bảng dưới đây.  

Approach
Python package
Better when bound to:
Parallel?
Threading 
threading
I/O
No
_______8
multiprocessing
Python package
0
Python package
1
Python package
2
Python package
3
I/O
No

Khóa phiên dịch viên toàn cầu

Trong một chương trình đa luồng, các luồng chia sẻ cùng một không gian bộ nhớ. Khi có nhiều luồng cố gắng sửa đổi tài nguyên, điều quan trọng là phải đảm bảo tính nhất quán và quyền truy cập độc quyền vào các tài nguyên đó.  

Mã an toàn chủ đề

Trong chương trình an toàn theo luồng, các luồng có thể truy cập cùng cấu trúc dữ liệu một cách an toàn vì cơ chế đồng bộ hóa luôn giữ cấu trúc dữ liệu ở trạng thái nhất quán. Cơ chế Python sử dụng nội bộ để hỗ trợ đồng bộ hóa này cho các chương trình đa luồng là khóa trình thông dịch toàn cục (GIL). Sự bảo vệ của GIL diễn ra ở cấp độ trạng thái thông dịch viên. Chẳng hạn, với GIL tại chỗ, việc tích hợp tiện ích mở rộng C không an toàn theo luồng sẽ dễ dàng hơn vì bạn có thể lấy và giải phóng GIL từ mã C một cách rõ ràng, do đó làm cho tiện ích mở rộng của bạn an toàn theo luồng ở cấp độ Python

Tương tự, GIL cũng cung cấp khả năng bảo vệ cho các cấu trúc dữ liệu C bên trong được sử dụng nhiều trong quản lý bộ nhớ của Python. Chiến lược quản lý bộ nhớ được sử dụng trong Python yêu cầu bảo vệ chống lại các điều kiện chủng tộc, rò rỉ bộ nhớ và các đối tượng phát hành không chính xác. Sự bảo vệ này được đảm bảo thông qua một mutex (một thành phần của GIL), ngăn các luồng sửa đổi cấu trúc dữ liệu được chia sẻ không chính xác

Công việc của GIL là giữ cho các cấu trúc dữ liệu nội bộ được đồng bộ hóa và nhất quán trên tất cả các luồng chia sẻ cùng một không gian bộ nhớ. Sự hiện diện của GIL không làm cho chuỗi mã Python của bạn trở nên an toàn về bản chất. , giống như các phiên bản được chia sẻ của kết nối cơ sở dữ liệu. Nó cũng không đảm bảo tính nhất quán cho các lệnh như phép gán phức hợp.  

x = x + 1

Dòng trên không phải là dòng nguyên tử, do đó, nó có thể bị ngắt giữa chừng nếu luồng đang chạy nó bị rớt GIL (hoặc nếu nó bị buộc phải làm như vậy). Nếu một chủ đề khác với GIL sửa đổi biến x, bạn có thể sẽ gặp điều kiện chủng tộc. Nói chung, chỉ các lệnh nguyên tử mới được đảm bảo an toàn cho luồng. Đối với các hướng dẫn phi nguyên tử, bạn sẽ cần sử dụng (hoặc bất kỳ cơ chế đồng bộ hóa nào khác), giúp một chuỗi có quyền truy cập độc quyền vào các tài nguyên được chia sẻ ở cấp Python, khiến các chuỗi khác chờ cho đến khi khóa được giải phóng

GIL hoạt động như thế nào?

Việc triển khai GIL có thể được tìm thấy trong các tệp nguồn C ceval_gil và pycore_gil. Hãy đi sâu vào nội bộ của nó

Cấu trúc của GIL

Trong mã nguồn, GIL được định nghĩa là “một biến boolean (bị khóa) có quyền truy cập được bảo vệ bởi một mutex (gil_mutex) và những thay đổi của nó được báo hiệu bởi một biến điều kiện (gil_cond). ” Khóa mutex đó có thể được nhìn thấy trong dòng sau

Python package
6

Đặc biệt chú ý đến các thành viên bị khóa, cond và mutex, được sử dụng nhiều trong tất cả các hoạt động liên quan đến GIL trong mô-đun cval_gil—đặc biệt là các chức năng và

Chiến lược của GIL

Cách lấy và bỏ GIL cũng được giải thích trong mã nguồn.  

“Một luồng muốn lấy GIL trước tiên sẽ vượt qua một khoảng thời gian nhất định (

Better when bound to:
1 micro giây) trước khi thiết lập gil_drop_request. ”

Khi một luồng muốn chạy, nó cần lấy GIL. Các hoạt động I/O khiến GIL bị hủy để một luồng khác có thể được thực thi. Điều này được gọi là đa nhiệm hợp tác. Nếu luồng đang chạy không giải phóng GIL, thì nó có thể được báo hiệu để loại bỏ nó sau một khoảng thời gian micro giây. Điều này được gọi là đa nhiệm ưu tiên. Cơ chế này rất quan trọng vì một số luồng liên kết với CPU có thể lạm dụng quyền sở hữu của GIL

Trong hàm take_gil, thời gian chờ khoảng thời gian được thực hiện với lệnh gọi hàm sau.  

Python package
7

Hãy xem để biết thêm chi tiết

GIL đang hoạt động

Để xem cách GIL được lấy và giải phóng bởi một luồng, hãy tưởng tượng bạn có một đoạn mã trong một luồng chứa nội dung sau

Python package
8

Khi chức năng này kết thúc, GIL bị loại bỏ và một luồng khác sẽ lấy nó. Khi chức năng C Sleep kết thúc, macro thứ hai, Py_END_ALLOW_THREADS, sẽ chạy. Do đó, lệnh gọi tới PyEval_RestoreThread sẽ được thực hiện và GIL sẽ bị chiếm lại. Điều này cho phép luồng ban đầu tiếp tục chạy

Python package
9

Lợi ích của GIL

GIL cung cấp các lợi ích sau

  • Nó làm cho các tiện ích mở rộng và thư viện C không an toàn theo luồng dễ dàng tích hợp hơn vào hệ sinh thái Python
  • Trong các chương trình đa luồng, GIL làm cho bộ thu gom rác gắn kết với cơ chế đếm tham chiếu
  • Các chương trình đơn luồng rất hiệu quả

Vấn đề hiệu năng

Như đã đề cập trước đó, bất kỳ luồng nào đang chạy đều cần có GIL. Nó cũng cần hủy và lấy lại nó, và trong khi tất cả điều này xảy ra, các luồng khác cần được báo hiệu, lên lịch, chạy, v.v. Phiên dịch viên cần có thời gian để thực hiện tất cả các hoạt động hậu cần này. Do đó, chương trình Python của bạn sẽ phải chịu chi phí quản lý luồng này.  

Ngoài ra, đối với các ứng dụng có các luồng liên kết với CPU, GIL sẽ làm cho hệ thống hoạt động giống như một chương trình đơn luồng. Nếu bạn không biết về GIL, chương trình Python đa luồng của bạn có thể còn chậm hơn phiên bản đơn luồng của nó

Hãy xem xét một quan niệm sai lầm phổ biến. Hãy tưởng tượng bạn có đoạn mã sau đang chạy trong sản xuất

Better when bound to:
0

Lưu ý rằng có một nhận xét trong mã nói rằng việc thực thi song song hàm set_doc sẽ xảy ra. Đây không phải là sự thật. Như đã đề cập trước đây, trong một ứng dụng đa luồng Python (bất kể bản chất của luồng, I/O hoặc liên kết với CPU), sẽ không có sự thực thi song song như vậy của một luồng. Chương trình này không áp dụng song song, mà đồng thời ở dạng đa nhiệm hợp tác. Nhận xét sai lệch này có vẻ không phải là một vấn đề lớn, nhưng điều gì sẽ xảy ra nếu các mô-đun khác xung quanh mã này được xây dựng dựa trên giả định rằng phần này sẽ chạy song song?

Vượt qua những hạn chế của GIL

Hầu hết các ứng dụng Python không yêu cầu bạn bỏ qua GIL. Hãy nhớ rằng, GIL là bạn của bạn. Nó làm cho các ứng dụng đơn luồng Python hoạt động hiệu quả. Trước khi cố gắng bỏ qua GIL, hãy tìm hiểu bản chất của ứng dụng của bạn. Nếu ứng dụng của bạn rất chuyên sâu về I/O, thì cách tiếp cận đa luồng với GIL tại chỗ có thể sẽ hoạt động tốt. Ngoài ra, nếu bạn cần kiểm soát chi tiết hơn cho cấu trúc I/O và muốn có mọi thứ trên một luồng, bạn có thể thử sử dụng asyncio, cung cấp API cho đa nhiệm hợp tác.  

Nếu bạn vẫn cần tìm hiểu về GIL—có lẽ vì ứng dụng của bạn bị ràng buộc bởi CPU—bạn có thể làm theo một vài chiến lược, được mô tả bên dưới

song song

Bạn có thể sử dụng gói đa xử lý của Python để sinh ra các quy trình con thay vì các luồng. Các quy trình đó có thể được hệ điều hành lên lịch để thực thi trên các CPU khác nhau cùng một lúc, làm cho phần mềm của bạn song song hiệu quả. Bởi vì bạn có thể quản lý các quy trình con đó từ một quy trình của cha mẹ Python, nên ở một mức độ nào đó, một chương trình sẽ kiểm soát các quy trình đang chạy trên một CPU khác.  

Phương pháp này có một nhược điểm lớn. Các quy trình cần nhiều không gian bộ nhớ hơn và do đó, chuyển đổi ngữ cảnh của chúng đắt hơn. Nói cách khác, việc tạo các quy trình mới sẽ tốn nhiều thời gian hơn và nhiều tài nguyên bộ nhớ hơn. Sử dụng phương pháp này một cách khôn ngoan; . Nó thường hoạt động tốt khi bạn có các ứng dụng liên kết với CPU.  

Triển khai Python thay thế

Việc triển khai Python (CPython) chuẩn đi kèm với GIL. Tuy nhiên, có những triển khai khác được viết bằng các ngôn ngữ khác nhau sử dụng các mô hình đa luồng kế thừa của chúng, nghĩa là chúng không đi kèm với GIL. Để đề cập đến một vài trong số những trường hợp này, chúng tôi có IronPython (. Net) và Jython (triển khai Java)

Nếu phạm vi ứng dụng của bạn được bao phủ bởi các triển khai bắt nguồn này, thì bạn có thể cần xem xét phương pháp triệt để hơn này. Hãy nhớ rằng các bản cập nhật trên các phiên bản dẫn xuất này thường chậm hơn so với phiên bản chính tắc

Một điều đáng nói nữa là dự án PyPy (triển khai Python được viết bằng Python) trước đây đã cố gắng loại bỏ GIL. Nếu một trong những sáng kiến ​​này hoạt động hiệu quả, thì việc triển khai này có thể là cách được đề xuất để bỏ qua GIL. Tuy nhiên, hãy nhớ rằng thư viện chuẩn của PyPy không lớn bằng thư viện trong CPython

Phần kết luận

Bài viết này đã xem xét cách GIL của Python hoạt động và nó giải quyết vấn đề gì. GIL có những lợi ích của nó, nhưng chúng đi kèm với những tác động tiềm ẩn về hiệu suất.  

Đa luồng trong Python là một tính năng rất hoàn thiện và trong ngữ cảnh của một ứng dụng chuyên sâu về I/O, nó có thể là một cách tiếp cận tuyệt vời. Hãy tự tin sử dụng nó và nếu nó không đáp ứng nhu cầu của bạn, hãy xem xét xử lý song song thông qua đa xử lý. Một tùy chọn khác là tận dụng các giải pháp đám mây và bên thứ ba hiện tại. Một trong số đó là Granulate, một công cụ cho phép bạn mở rộng quy mô ứng dụng của mình mà không cần thay đổi mã

Python đa luồng GIL là gì?

Khóa phiên dịch toàn cầu Python hay GIL, nói một cách đơ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 trình thông dịch Python. This means that only one thread can be in a state of execution at any point in time.

Đa luồng có khả thi trong Python không?

Python không hỗ trợ đa luồng vì Python trên trình thông dịch Cpython không hỗ trợ thực thi đa lõi thực sự thông qua đa luồng. Tuy nhiên, Python không có thư viện luồng. GIL không ngăn luồng.

Đa xử lý hoạt động như thế nào với GIL?

Gói đa xử lý cung cấp cả đồng thời cục bộ và từ xa, hỗ trợ hiệu quả Khóa thông dịch viên toàn cầu bằng cách sử dụng quy trình con thay vì luồng. Do đó, mô-đun đa xử lý cho phép lập trình viên tận dụng tối đa nhiều bộ xử lý trên một máy nhất định .

Python có đang xóa GIL không?

Dự án “nogil” nhằm loại bỏ GIL khỏi CPython để làm cho các chương trình Python đa luồng hiệu quả hơn, đồng thời duy trì khả năng tương thích ngược và hiệu suất đơn luồng. Nó tồn tại dưới dạng một nhánh rẽ, nhưng mục tiêu cuối cùng là đóng góp những thay đổi này ngược dòng.