Hướng dẫn này sẽ tập trung vào một trong những chủ đề quan trọng của Python, GIL. Chúng tôi cũng sẽ đề cập đến cách GIL tác động đến hiệu suất của chương trình Python với việc triển khai mã. Trước khi đi sâu vào chủ đề này, chúng ta hãy có một ý tưởng cơ bản về GIL
GIL hoặc Khóa phiên dịch toàn cầu
Khóa phiên dịch toàn cầu Python hoặc GIL là một phần quan trọng của lập trình đa luồng. Nó là một loại khóa quy trình được sử dụng khi làm việc với nhiều quy trình. Nó chỉ cung cấp quyền kiểm soát cho một luồng. Nói chung, Python sử dụng một luồng duy nhất để chạy một quy trình duy nhất. Chúng tôi nhận được kết quả hiệu suất giống nhau của các quy trình đơn luồng và đa luồng bằng GIL. Nó hạn chế đạt được đa luồng trong Python vì nó ngăn chặn các luồng và hoạt động như một luồng đơn lẻ
Lưu ý - Python không hỗ trợ đa luồng vì các gói luồng không thể cho phép chúng tôi sử dụng nhiều lõi CPU
Tại sao các nhà phát triển Python sử dụng GIL?
Python cung cấp tính năng truy cập tham chiếu duy nhất, được sử dụng để quản lý bộ nhớ. Bộ đếm tham chiếu đếm tổng số tham chiếu được tạo nội bộ trong Python để gán giá trị cho một đối tượng dữ liệu. Khi số tham chiếu về 0, bộ nhớ được chỉ định của đối tượng sẽ được giải phóng. Hãy xem ví dụ dưới đây
Thí dụ -
Mối quan tâm chính với biến đếm tham chiếu là nó có thể bị ảnh hưởng khi hai hoặc ba luồng cố gắng tăng hoặc giảm giá trị của nó đồng thời. Nó được gọi là điều kiện cuộc đua. Nếu tình trạng này xảy ra, có thể do bộ nhớ bị rò rỉ không bao giờ được phát hành. Nó có thể bị lỗi hoặc lỗi trong chương trình Python
GIL giúp chúng tôi loại bỏ tình huống như vậy bằng cách sử dụng khóa cho tất cả các cấu trúc dữ liệu dùng chung giữa các luồng để chúng không bị thay đổi một cách nhất quán. Python cung cấp một cách dễ dàng để triển khai GIL vì nó liên quan đến quản lý bộ nhớ an toàn cho luồng. GIL yêu cầu cung cấp một khóa duy nhất cho một luồng để xử lý bằng Python. Nó làm tăng hiệu suất của một chương trình đơn luồng vì chỉ cần xử lý một khóa. Nó cũng giúp tạo bất kỳ chương trình nào gắn với CPU và ngăn chặn tình trạng bế tắc
Tác động đối với các chương trình Python đa luồng
Có sự khác biệt giữa giới hạn CPU về hiệu suất của chúng và giới hạn I/O đối với 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. Các chương trình liên kết với CPU thường đẩy CPU đến giới hạn của nó. Các chương trình này thường được sử dụng để tính toán như phép nhân ma trận, 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 để nhận đầu vào/đầu ra có thể được tạo bởi người dùng, tệp, cơ sở dữ liệu, mạng, v.v. Các chương trình như vậy phải đợi một khoảng thời gian đáng kể cho đến khi nguồn cung cấp đầu vào. Mặt khác, nguồn cũng có thời gian xử lý riêng. Ví dụ: người dùng đang suy nghĩ về những gì sẽ nhập làm đầu vào
Hãy hiểu ví dụ sau
Thí dụ -
đầu ra
Time taken in seconds - 7.422671556472778
Bây giờ chúng tôi sửa đổi mã trên bằng cách chạy hai chủ đề
Ví dụ - 2
đầu ra
Time taken in seconds - 6.90830135345459
Như chúng ta có thể thấy rằng cả hai mã mất cùng thời gian để hoàn thành. GIL đã ngăn các luồng liên kết với CPU thực thi song song trong mã thứ hai
Tại sao GIL vẫn chưa bị xóa?
Nhiều lập trình viên phàn nàn về điều này, nhưng Python không thể mang lại những thay đổi quan trọng như việc loại bỏ GIL. Một lý do khác là GIL không được cải thiện cho đến bây giờ. Nếu nó thay đổi trong Python 3, nó sẽ tạo ra một số vấn đề nghiêm trọng. Thay vì loại bỏ GIL, khái niệm GIL có thể cải thiện. Theo Guido van Rossom -
"Tôi chỉ hoan nghênh một tập hợp các bản vá lỗi trong Py3k nếu hiệu suất của chương trình đơn luồng [và cho chương trình đa luồng nhưng có giới hạn I/O] không giảm"
Ngoài ra còn có nhiều phương pháp giải quyết cùng một vấn đề mà GIL đã giải quyết, nhưng có những phương pháp khó thực hiện
Cách đối phó với GIL của Python
Sử dụng đa xử lý là cách phù hợp nhất để ngăn chương trình khỏi GIL. Python cung cấp nhiều trình thông dịch khác nhau cho mỗi quy trình để chạy, do đó, trong trường hợp đó, một luồng đơn được cung cấp cho mỗi quy trình trong đa xử lý. Hãy hiểu ví dụ sau
Thí dụ -
đầu ra
Time taken in seconds - 3.3707828521728516
Có vẻ như hiệu suất khá được tăng lên nhưng quản lý quy trình có chi phí riêng và nhiều quy trình nặng hơn nhiều luồng
Phần kết luận
Trong hướng dẫn này, chúng tôi đã thảo luận về GIL và cách chúng tôi có thể sử dụng nó. Nó trao quyền điều khiển cho một luồng để thực thi tại thời điểm. Hướng dẫn này cũng giải thích tại sao GIL lại quan trọng đối với các lập trình viên Python
Như bạn có thể biết, GIL là viết tắt của Khóa phiên dịch toàn cầu và nhiệm vụ của nó là làm cho trình thông dịch CPython an toàn theo luồng. GIL chỉ cho phép một luồng hệ điều hành thực thi mã byte Python tại bất kỳ thời điểm nào và hậu quả của việc này là không thể tăng tốc mã Python sử dụng nhiều CPU bằng cách phân phối công việc giữa nhiều luồng. Tuy nhiên, đây không phải là tác động tiêu cực duy nhất của GIL. GIL giới thiệu chi phí làm cho các chương trình đa luồng chậm hơn và điều đáng ngạc nhiên hơn là nó thậm chí có thể ảnh hưởng đến các luồng liên kết I/O
Trong bài đăng này, tôi muốn nói với bạn nhiều hơn về những tác động không rõ ràng của GIL. Đồng thời, chúng ta sẽ thảo luận về GIL thực sự là gì, tại sao nó tồn tại, cách thức hoạt động và cách nó sẽ ảnh hưởng đến tính đồng thời của Python trong tương lai
Ghi chú. Trong bài viết này tôi đang đề cập đến CPython 3. 9. Một số chi tiết triển khai chắc chắn sẽ thay đổi khi CPython phát triển. Tôi sẽ cố gắng theo dõi những thay đổi quan trọng và thêm ghi chú cập nhật
Luồng hệ điều hành, luồng Python và GIL
Trước tiên, hãy để tôi nhắc bạn các luồng Python là gì và cách thức hoạt động của đa luồng trong Python. Khi bạn chạy tệp thực thi
import threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
6, HĐH sẽ bắt đầu một quy trình mới với một luồng thực thi được gọi là luồng chính. Như trong trường hợp của bất kỳ chương trình C nào khác, luồng chính bắt đầu thực thi import threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
6 bằng cách nhập hàm import threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
8 của nó. Tất cả các luồng chính thực hiện tiếp theo có thể được tóm tắt bằng ba bước- khởi tạo trình thông dịch;
- biên dịch mã Python thành mã byte;
- nhập vòng đánh giá để thực thi mã byte
Luồng chính là luồng hệ điều hành thông thường thực thi mã C đã biên dịch. Trạng thái của nó bao gồm các giá trị của các thanh ghi CPU và ngăn xếp cuộc gọi của các hàm C. Tuy nhiên, một chuỗi Python phải nắm bắt ngăn xếp cuộc gọi của các hàm Python, trạng thái ngoại lệ và những thứ khác liên quan đến Python. Vì vậy, những gì CPython làm là đặt những thứ đó vào a và liên kết trạng thái luồng với luồng hệ điều hành. Nói cách khác,
import threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
9Vòng đánh giá là một vòng lặp vô hạn chứa một công tắc khổng lồ trên tất cả các hướng dẫn mã byte có thể. Để vào vòng lặp, một luồng phải giữ GIL. Chủ đề chính lấy GIL trong quá trình khởi tạo, vì vậy có thể truy cập miễn phí. Khi nó đi vào vòng lặp, nó chỉ bắt đầu thực hiện từng lệnh mã byte một theo công tắc
Đôi khi, một luồng phải tạm dừng thực thi mã byte. Nó kiểm tra xem có bất kỳ lý do nào để làm điều đó khi bắt đầu mỗi lần lặp lại vòng đánh giá hay không. Chúng tôi quan tâm đến một lý do như vậy. một chủ đề khác đã yêu cầu GIL. Đây là cách logic này được triển khai trong mã
PyObject*
_PyEval_EvalFrameDefault[PyThreadState *tstate, PyFrameObject *f, int throwflag]
{
// .. declaration of local variables and other boring stuff
// the evaluation loop
for [;;] {
// `eval_breaker` tells whether we should suspend bytecode execution
// e.g. other thread requested the GIL
if [_Py_atomic_load_relaxed[eval_breaker]] {
// `eval_frame_handle_pending[]` suspends bytecode execution
// e.g. when another thread requests the GIL,
// this function drops the GIL and waits for the GIL again
if [eval_frame_handle_pending[tstate] != 0] {
goto error;
}
}
// get next bytecode instruction
NEXTOPARG[];
switch [opcode] {
case TARGET[NOP] {
FAST_DISPATCH[]; // next iteration
}
case TARGET[LOAD_FAST] {
// .. code for loading local variable
FAST_DISPATCH[]; // next iteration
}
// .. 117 more cases for every possible opcode
}
// .. error handling
}
// .. termination
}
Trong chương trình Python đơn luồng, luồng chính là luồng duy nhất và nó không bao giờ giải phóng GIL. Bây giờ hãy xem điều gì xảy ra trong một chương trình đa luồng. Chúng tôi sử dụng mô-đun tiêu chuẩn
def countdown[n]:
while n > 0:
n -= 1
0 để bắt đầu một chuỗi Python mớiimport threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
Phương thức
def countdown[n]:
while n > 0:
n -= 1
1 của phiên bản def countdown[n]:
while n > 0:
n -= 1
2 tạo một chuỗi hệ điều hành mới. Trên các hệ thống giống Unix bao gồm Linux và macOS, nó gọi hàm pthread_create[] cho mục đích đó. Chuỗi mới được tạo bắt đầu thực thi hàm def countdown[n]:
while n > 0:
n -= 1
3 với đối số def countdown[n]:
while n > 0:
n -= 1
4. Đối số là một cấu trúc chứa hàm đích, các đối số đã truyền và trạng thái luồng cho luồng hệ điều hành mới. Hàm thực hiện một số việc, nhưng quan trọng nhất, nó lấy GIL và sau đó đi vào vòng đánh giá để thực thi mã byte của hàm đíchĐể có được GIL, trước tiên, một luồng sẽ kiểm tra xem một số luồng khác có giữ GIL hay không. Nếu đây không phải là trường hợp, luồng có được GIL ngay lập tức. Mặt khác, nó sẽ đợi cho đến khi GIL được phát hành. Nó chờ một khoảng thời gian cố định được gọi là khoảng thời gian chuyển đổi [5 ms theo mặc định] và nếu GIL không được giải phóng trong thời gian đó, nó sẽ đặt các cờ
def countdown[n]:
while n > 0:
n -= 1
7 và def countdown[n]:
while n > 0:
n -= 1
8. Cờ def countdown[n]:
while n > 0:
n -= 1
7 báo cho chuỗi giữ GIL tạm dừng thực thi mã byte và def countdown[n]:
while n > 0:
n -= 1
8 giải thích lý do tại sao. Chuỗi giữ GIL nhìn thấy các cờ khi nó bắt đầu lần lặp tiếp theo của vòng đánh giá và giải phóng GIL. Nó thông báo các luồng đang chờ GIL và một trong số chúng có được GIL. Hệ điều hành quyết định chuỗi nào sẽ đánh thức, do đó, chuỗi đó có thể hoặc không phải là chuỗi đặt cờĐó là mức tối thiểu của những gì chúng ta cần biết về GIL. Bây giờ hãy để tôi chứng minh tác dụng của nó mà tôi đã nói trước đó. Nếu bạn thấy chúng thú vị, hãy tiếp tục với các phần tiếp theo, trong đó chúng tôi nghiên cứu chi tiết hơn về GIL
Tác dụng của GIL
Tác dụng đầu tiên của GIL được nhiều người biết đến. nhiều luồng Python không thể chạy song song. Do đó, một chương trình đa luồng không nhanh hơn chương trình đơn luồng tương đương ngay cả trên máy đa lõi. Là một nỗ lực ngây thơ để song song hóa mã Python, hãy xem xét chức năng giới hạn CPU sau đây thực hiện thao tác giảm dần một số lần nhất định
________số 8_______
Bây giờ, giả sử chúng ta muốn thực hiện giảm 100.000.000. Chúng tôi có thể chạy
from threading import Thread
import socket
def run_server[host='127.0.0.1', port=33333]:
sock = socket.socket[]
sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1]
sock.bind[[host, port]]
sock.listen[]
while True:
client_sock, addr = sock.accept[]
print['Connection from', addr]
Thread[target=handle_client, args=[client_sock,]].start[]
def handle_client[sock]:
while True:
received_data = sock.recv[4096]
if not received_data:
break
sock.sendall[received_data]
print['Client disconnected:', sock.getpeername[]]
sock.close[]
if __name__ == '__main__':
run_server[]
1 trong một luồng hoặc from threading import Thread
import socket
def run_server[host='127.0.0.1', port=33333]:
sock = socket.socket[]
sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1]
sock.bind[[host, port]]
sock.listen[]
while True:
client_sock, addr = sock.accept[]
print['Connection from', addr]
Thread[target=handle_client, args=[client_sock,]].start[]
def handle_client[sock]:
while True:
received_data = sock.recv[4096]
if not received_data:
break
sock.sendall[received_data]
print['Client disconnected:', sock.getpeername[]]
sock.close[]
if __name__ == '__main__':
run_server[]
2 trong hai luồng hoặc from threading import Thread
import socket
def run_server[host='127.0.0.1', port=33333]:
sock = socket.socket[]
sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1]
sock.bind[[host, port]]
sock.listen[]
while True:
client_sock, addr = sock.accept[]
print['Connection from', addr]
Thread[target=handle_client, args=[client_sock,]].start[]
def handle_client[sock]:
while True:
received_data = sock.recv[4096]
if not received_data:
break
sock.sendall[received_data]
print['Client disconnected:', sock.getpeername[]]
sock.close[]
if __name__ == '__main__':
run_server[]
3 trong bốn luồng, v.v. Trong ngôn ngữ không có GIL như C, chúng ta sẽ thấy tốc độ tăng khi chúng ta tăng số lượng luồng. Chạy Python trên MacBook Pro của tôi với hai lõi và siêu phân luồng, tôi thấy như sauSố luồngGiảm dần trên mỗi luồng [n]Thời gian tính bằng giây [tốt nhất trong số 3]1100,000,0006. 52250,000,0006. 57425,000,0006. 59812.500.0006. 58Thời gian không thay đổi. Trên thực tế, các chương trình đa luồng có thể chạy chậm hơn do chi phí liên quan đến chuyển ngữ cảnh. Khoảng thời gian chuyển đổi mặc định là 5 ms, do đó, chuyển đổi ngữ cảnh không xảy ra thường xuyên. Nhưng nếu chúng ta giảm khoảng thời gian chuyển đổi, chúng ta sẽ thấy sự chậm lại. Thông tin thêm về lý do tại sao chúng ta có thể cần phải làm điều đó sau
Mặc dù các luồng Python không thể giúp chúng tôi tăng tốc mã sử dụng nhiều CPU, nhưng chúng rất hữu ích khi chúng tôi muốn thực hiện đồng thời nhiều tác vụ liên quan đến I/O. Hãy xem xét một máy chủ lắng nghe các kết nối đến và khi nhận được kết nối, nó sẽ chạy một chức năng xử lý trong một luồng riêng biệt. Hàm xử lý nói chuyện với máy khách bằng cách đọc và ghi vào ổ cắm của máy khách. Khi đọc từ ổ cắm, luồng chỉ bị treo cho đến khi máy khách gửi thứ gì đó. Đây là nơi đa luồng giúp. một chủ đề khác có thể chạy trong khi chờ đợi
Để cho phép các luồng khác chạy trong khi luồng giữ GIL đang đợi I/O, CPython triển khai tất cả các thao tác I/O bằng cách sử dụng mẫu sau
- giải phóng GIL;
- thực hiện thao tác, e. g.
4,from threading import Thread import socket def run_server[host='127.0.0.1', port=33333]: sock = socket.socket[] sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1] sock.bind[[host, port]] sock.listen[] while True: client_sock, addr = sock.accept[] print['Connection from', addr] Thread[target=handle_client, args=[client_sock,]].start[] def handle_client[sock]: while True: received_data = sock.recv[4096] if not received_data: break sock.sendall[received_data] print['Client disconnected:', sock.getpeername[]] sock.close[] if __name__ == '__main__': run_server[]
5,from threading import Thread import socket def run_server[host='127.0.0.1', port=33333]: sock = socket.socket[] sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1] sock.bind[[host, port]] sock.listen[] while True: client_sock, addr = sock.accept[] print['Connection from', addr] Thread[target=handle_client, args=[client_sock,]].start[] def handle_client[sock]: while True: received_data = sock.recv[4096] if not received_data: break sock.sendall[received_data] print['Client disconnected:', sock.getpeername[]] sock.close[] if __name__ == '__main__': run_server[]
6;from threading import Thread import socket def run_server[host='127.0.0.1', port=33333]: sock = socket.socket[] sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1] sock.bind[[host, port]] sock.listen[] while True: client_sock, addr = sock.accept[] print['Connection from', addr] Thread[target=handle_client, args=[client_sock,]].start[] def handle_client[sock]: while True: received_data = sock.recv[4096] if not received_data: break sock.sendall[received_data] print['Client disconnected:', sock.getpeername[]] sock.close[] if __name__ == '__main__': run_server[]
- có được GIL
Do đó, một luồng có thể giải phóng GIL một cách tự nguyện trước khi một luồng khác đặt
def countdown[n]:
while n > 0:
n -= 1
7 và def countdown[n]:
while n > 0:
n -= 1
8. Nói chung, một luồng chỉ cần giữ GIL khi nó hoạt động với các đối tượng Python. Vì vậy, CPython áp dụng mô hình phát hành-thực hiện-thu được không chỉ cho các thao tác I/O mà còn cho các lệnh gọi chặn khác vào HĐH như select[] và pthread_mutex_lock[] và cho các tính toán nặng bằng ngôn ngữ C thuần túy. Ví dụ: các hàm băm trong mô-đun tiêu chuẩn from threading import Thread
import socket
def run_server[host='127.0.0.1', port=33333]:
sock = socket.socket[]
sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1]
sock.bind[[host, port]]
sock.listen[]
while True:
client_sock, addr = sock.accept[]
print['Connection from', addr]
Thread[target=handle_client, args=[client_sock,]].start[]
def handle_client[sock]:
while True:
received_data = sock.recv[4096]
if not received_data:
break
sock.sendall[received_data]
print['Client disconnected:', sock.getpeername[]]
sock.close[]
if __name__ == '__main__':
run_server[]
9 giải phóng GIL. Điều này cho phép chúng tôi thực sự tăng tốc mã Python gọi các chức năng như vậy bằng cách sử dụng đa luồngGiả sử chúng tôi muốn tính toán giá trị băm SHA-256 của tám thông báo 128 MB. Chúng tôi có thể tính toán _______28_______0 cho mỗi thư trong một chuỗi, nhưng chúng tôi cũng có thể phân phối công việc giữa nhiều chuỗi. Nếu tôi so sánh trên máy của mình, tôi nhận được kết quả như sau
Số lượng chủ đềTổng kích thước của tin nhắn trên mỗi chủ đềThời gian tính bằng giây [tốt nhất trong số 3]11 GB3. 302512 MB1. 684256 MB1. 508128 MB1. 60Chuyển từ một luồng sang hai luồng gần như tăng tốc gấp 2 lần vì các luồng chạy song song. Thêm nhiều luồng không giúp được gì nhiều vì máy của tôi chỉ có hai lõi vật lý. Kết luận ở đây là có thể tăng tốc mã Python sử dụng nhiều CPU bằng cách sử dụng đa luồng nếu mã gọi các hàm C giải phóng GIL. Lưu ý rằng các chức năng như vậy có thể được tìm thấy không chỉ trong thư viện tiêu chuẩn mà còn trong các mô-đun của bên thứ ba nặng về tính toán như NumPy. Bạn thậm chí có thể tự viết
Chúng tôi đã đề cập đến các luồng liên kết với CPU – các luồng luôn tính toán thứ gì đó và các luồng liên kết với I/O – các luồng hầu hết thời gian chờ I/O. Hiệu ứng thú vị nhất của GIL diễn ra khi chúng ta kết hợp cả hai. Hãy xem xét một máy chủ tiếng vang TCP đơn giản lắng nghe các kết nối đến và khi máy khách kết nối, sẽ tạo ra một luồng mới để xử lý máy khách
from threading import Thread
import socket
def run_server[host='127.0.0.1', port=33333]:
sock = socket.socket[]
sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1]
sock.bind[[host, port]]
sock.listen[]
while True:
client_sock, addr = sock.accept[]
print['Connection from', addr]
Thread[target=handle_client, args=[client_sock,]].start[]
def handle_client[sock]:
while True:
received_data = sock.recv[4096]
if not received_data:
break
sock.sendall[received_data]
print['Client disconnected:', sock.getpeername[]]
sock.close[]
if __name__ == '__main__':
run_server[]
Máy chủ này có thể xử lý bao nhiêu yêu cầu mỗi giây? . Đây có lẽ không phải là thước đo chính xác vì máy khách và máy chủ chạy trên cùng một máy, nhưng đó không phải là vấn đề. Vấn đề là để xem RPS giảm như thế nào khi máy chủ thực hiện một số tác vụ liên quan đến CPU trong một luồng riêng biệt
Hãy xem xét cùng một máy chủ nhưng với một chuỗi giả bổ sung tăng và giảm một biến trong một vòng lặp vô hạn [mọi tác vụ liên kết với CPU sẽ thực hiện giống như vậy]
# .. the same server code
def compute[]:
n = 0
while True:
n += 1
n -= 1
if __name__ == '__main__':
Thread[target=compute].start[]
run_server[]
Bạn mong đợi RPS thay đổi như thế nào? . RPS giảm xuống 100, thấp hơn 300 lần. Và điều này rất đáng ngạc nhiên nếu bạn đã quen với cách các hệ điều hành lên lịch cho các luồng. Để hiểu ý tôi muốn nói gì, hãy chạy máy chủ và luồng liên kết với CPU dưới dạng các quy trình riêng biệt để chúng không bị ảnh hưởng bởi GIL. Chúng tôi có thể chia mã thành hai tệp khác nhau hoặc chỉ sử dụng mô-đun tiêu chuẩn
# .. the same server code
def compute[]:
n = 0
while True:
n += 1
n -= 1
if __name__ == '__main__':
Thread[target=compute].start[]
run_server[]
1 để tạo ra một quy trình mới như vậyfrom multiprocessing import Process
# .. the same server code
if __name__ == '__main__':
Process[target=compute].start[]
run_server[]
Và điều này mang lại khoảng 20k RPS. Ngoài ra, nếu chúng tôi bắt đầu hai, ba hoặc bốn quy trình liên kết với CPU, thì RPS sẽ giữ nguyên. Bộ lập lịch hệ điều hành ưu tiên chuỗi liên kết I/O, đây là điều nên làm
Trong ví dụ máy chủ, luồng liên kết I/O chờ ổ cắm sẵn sàng để đọc và ghi, nhưng hiệu suất của bất kỳ luồng liên kết I/O nào khác sẽ giảm giống như vậy. Hãy xem xét một chuỗi giao diện người dùng đang đợi đầu vào của người dùng. Nó sẽ đóng băng thường xuyên nếu bạn chạy nó cùng với luồng liên kết với CPU. Rõ ràng, đây không phải là cách các luồng hệ điều hành bình thường hoạt động và nguyên nhân là do GIL. Nó can thiệp vào bộ lập lịch hệ điều hành
Vấn đề này thực sự nổi tiếng trong số các nhà phát triển CPython. Họ gọi nó là hiệu ứng đoàn xe. David Beazley đã nói về nó vào năm 2010 và cũng mở ra một vấn đề liên quan về lỗi. con trăn. tổ chức. Vào năm 2021, 11 năm sau, vấn đề đã được đóng lại. Tuy nhiên chưa khắc phục được. Trong phần còn lại của bài đăng này, chúng tôi sẽ cố gắng tìm hiểu tại sao
hiệu ứng đoàn xe
Hiệu ứng đoàn xe diễn ra bởi vì mỗi khi luồng liên kết I/O thực hiện một thao tác I/O, nó sẽ giải phóng GIL và khi nó cố gắng lấy lại GIL sau thao tác, GIL có thể đã được CPU sử dụng . Vì vậy, luồng liên kết I/O phải đợi ít nhất 5 ms trước khi có thể đặt
def countdown[n]:
while n > 0:
n -= 1
7 và def countdown[n]:
while n > 0:
n -= 1
8 để buộc luồng liên kết CPU giải phóng GILHệ điều hành có thể lên lịch cho luồng liên kết với CPU ngay khi luồng liên kết với I/O giải phóng GIL. Chuỗi liên kết I/O chỉ có thể được lên lịch khi thao tác I/O hoàn tất, do đó, nó có ít cơ hội lấy GIL trước hơn. Nếu hoạt động thực sự nhanh, chẳng hạn như không bị chặn
# .. the same server code
def compute[]:
n = 0
while True:
n += 1
n -= 1
if __name__ == '__main__':
Thread[target=compute].start[]
run_server[]
4, thì cơ hội thực sự khá tốt nhưng chỉ trên máy lõi đơn nơi HĐH phải quyết định luồng nào sẽ lên lịchTrên máy đa lõi, hệ điều hành không phải quyết định lập lịch cho luồng nào trong hai luồng. Nó có thể lên lịch cả trên các lõi khác nhau. Kết quả là luồng liên kết với CPU gần như được đảm bảo để có được GIL trước và mỗi thao tác I/O trong luồng liên kết với I/O tốn thêm 5 mili giây
Lưu ý rằng một luồng buộc phải giải phóng GIL sẽ đợi cho đến khi một luồng khác lấy nó, vì vậy luồng liên kết I/O sẽ lấy được GIL sau một khoảng thời gian chuyển đổi. Nếu không có logic này, hiệu ứng đoàn xe sẽ còn nghiêm trọng hơn
Bây giờ, 5 ms là bao nhiêu? . Nếu một luồng đợi vài giây cho đến khi dữ liệu trên ổ cắm có sẵn để đọc, thì thêm 5 ms không quan trọng lắm. Nhưng một số thao tác I/O thực sự nhanh. Ví dụ: chỉ chặn
# .. the same server code
def compute[]:
n = 0
while True:
n += 1
n -= 1
if __name__ == '__main__':
Thread[target=compute].start[]
run_server[]
4 khi bộ đệm gửi đầy và trả về ngay lập tức nếu không. Vì vậy, nếu các thao tác I/O mất vài phần triệu giây, thì một phần nghìn giây chờ đợi GIL có thể có tác động rất lớnMáy chủ echo không có luồng liên kết với CPU xử lý 30k RPS, có nghĩa là một yêu cầu đơn lẻ mất khoảng 1/30k ≈ 30 µs. Với luồng liên kết với CPU,
from threading import Thread
import socket
def run_server[host='127.0.0.1', port=33333]:
sock = socket.socket[]
sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1]
sock.bind[[host, port]]
sock.listen[]
while True:
client_sock, addr = sock.accept[]
print['Connection from', addr]
Thread[target=handle_client, args=[client_sock,]].start[]
def handle_client[sock]:
while True:
received_data = sock.recv[4096]
if not received_data:
break
sock.sendall[received_data]
print['Client disconnected:', sock.getpeername[]]
sock.close[]
if __name__ == '__main__':
run_server[]
5 và # .. the same server code
def compute[]:
n = 0
while True:
n += 1
n -= 1
if __name__ == '__main__':
Thread[target=compute].start[]
run_server[]
4 thêm 5 ms = 5.000 µs cho mỗi yêu cầu và một yêu cầu hiện mất 10.030 µs. Đây là khoảng hơn 300 lần. Do đó, thông lượng ít hơn 300 lần. Các con số phù hợpBạn có thể yêu cầu. Hiệu ứng đoàn xe có phải là vấn đề trong các ứng dụng trong thế giới thực không? . Tôi chưa bao giờ gặp phải nó, tôi cũng không thể tìm thấy bằng chứng cho thấy bất kỳ ai khác đã làm. Mọi người không phàn nàn và đây là một phần lý do tại sao sự cố vẫn chưa được khắc phục
Nhưng nếu hiệu ứng đoàn xe gây ra các vấn đề về hiệu suất trong ứng dụng của bạn thì sao?
Sửa hiệu ứng đoàn xe
Vì vấn đề là luồng liên kết I/O đợi khoảng thời gian chuyển đổi cho đến khi nó yêu cầu GIL, nên chúng tôi có thể thử đặt khoảng thời gian chuyển đổi thành một giá trị nhỏ hơn. Python cung cấp chức năng cho mục đích đó. Đối số
# .. the same server code
def compute[]:
n = 0
while True:
n += 1
n -= 1
if __name__ == '__main__':
Thread[target=compute].start[]
run_server[]
9 là một giá trị dấu phẩy động đại diện cho giây. Khoảng thời gian chuyển đổi được đo bằng micro giây, vì vậy giá trị nhỏ nhất là from multiprocessing import Process
# .. the same server code
if __name__ == '__main__':
Process[target=compute].start[]
run_server[]
0. Đây là RPS tôi nhận được nếu tôi thay đổi khoảng thời gian chuyển đổi và số luồng CPUKhoảng thời gian chuyển đổi tính bằng giâyRPS không có luồng CPURPS có một luồng CPURPS có hai luồng CPURPS có bốn luồng CPU0. 130,0005200. 0130,0005030150. 00530,00010050300. 00130,0005002802000. 000130,0003,2001,70010000. 0000130,00011,0005,5002,8000. 00000130,00010,0004,5002,500Kết quả cho thấy một số điều
- Khoảng thời gian chuyển đổi không liên quan nếu luồng liên kết I/O là luồng duy nhất
- Khi chúng tôi thêm một luồng liên kết với CPU, RPS giảm đáng kể
- Khi chúng tôi tăng gấp đôi số luồng liên kết với CPU, thì RPS giảm một nửa
- Khi chúng tôi giảm khoảng thời gian chuyển đổi, RPS tăng gần như tương ứng cho đến khi khoảng thời gian chuyển đổi trở nên quá nhỏ. Điều này là do chi phí chuyển ngữ cảnh trở nên đáng kể
Khoảng thời gian chuyển đổi nhỏ hơn làm cho các luồng liên kết I/O phản ứng nhanh hơn. Nhưng khoảng thời gian chuyển đổi quá nhỏ sẽ gây ra nhiều chi phí do số lượng lớn chuyển đổi ngữ cảnh gây ra. Nhắc lại hàm
from multiprocessing import Process
# .. the same server code
if __name__ == '__main__':
Process[target=compute].start[]
run_server[]
1. Chúng tôi thấy rằng chúng tôi không thể tăng tốc nó với nhiều luồng. Nếu chúng ta đặt khoảng thời gian chuyển đổi quá nhỏ, thì chúng ta cũng sẽ thấy hiện tượng chậm lạiKhoảng thời gian chuyển đổi tính bằng giâyThời gian tính bằng giây [luồng. 1]Thời gian tính bằng giây [chủ đề. 2]Thời gian tính bằng giây [luồng. 4] Thời gian tính bằng giây [luồng. 8]0. 17. 296. 806. 506. 610. 016. 626. 617. 156. 710. 0056. 536. 587. 207. 190. 0017. 027. 367. 567. 120. 00016. 779. 209. 369. 840. 000016. 6812. 2919. 1530. 530. 0000016. 8917. 1631. 6886. 44Một lần nữa, khoảng thời gian chuyển đổi không thành vấn đề nếu chỉ có một luồng. Ngoài ra, số lượng luồng không thành vấn đề nếu khoảng thời gian chuyển đổi đủ lớn. Một khoảng thời gian chuyển đổi nhỏ và một số luồng là khi bạn có hiệu suất kém
Kết luận là việc thay đổi khoảng thời gian chuyển đổi là một tùy chọn để khắc phục hiệu ứng đoàn xe, nhưng bạn nên cẩn thận đo lường mức độ thay đổi ảnh hưởng đến ứng dụng của mình
Cách thứ 2 sửa hiệu ứng đoàn xe còn hack hơn. Vì sự cố ít nghiêm trọng hơn nhiều trên các máy đơn lõi, nên chúng tôi có thể cố gắng hạn chế tất cả các luồng Python ở một lõi đơn. Điều này sẽ buộc HĐH phải chọn luồng nào để lên lịch và luồng liên kết I/O sẽ có mức độ ưu tiên
Không phải hệ điều hành nào cũng cung cấp cách hạn chế một nhóm luồng đối với một số lõi nhất định. Theo như tôi hiểu, macOS chỉ cung cấp một cơ chế để đưa ra gợi ý cho bộ lập lịch hệ điều hành. Cơ chế mà chúng ta cần có sẵn trên Linux. Đó là hàm
from multiprocessing import Process
# .. the same server code
if __name__ == '__main__':
Process[target=compute].start[]
run_server[]
2. Nó nhận một luồng và mặt nạ của các lõi CPU và yêu cầu HĐH chỉ lên lịch cho luồng trên các lõi được chỉ định bởi mặt nạfrom multiprocessing import Process
# .. the same server code
if __name__ == '__main__':
Process[target=compute].start[]
run_server[]
2 là một hàm C. Để gọi nó từ Python, bạn có thể sử dụng một cái gì đó như from multiprocessing import Process
# .. the same server code
if __name__ == '__main__':
Process[target=compute].start[]
run_server[]
4. Tôi không muốn gây rối với from multiprocessing import Process
# .. the same server code
if __name__ == '__main__':
Process[target=compute].start[]
run_server[]
4, vì vậy tôi chỉ sửa đổi mã nguồn CPython. Sau đó, tôi đã biên dịch tệp thực thi, chạy máy chủ echo trên máy Ubuntu lõi kép và nhận được kết quả sauSố luồng liên kết với CPU01248RPS24k12k3k3010Máy chủ có thể chịu đựng khá tốt một luồng liên kết với CPU. Nhưng vì luồng liên kết I/O cần phải cạnh tranh với tất cả các luồng liên kết CPU cho GIL, nên khi chúng tôi thêm nhiều luồng hơn, hiệu suất sẽ giảm mạnh. Bản sửa lỗi giống như một bản hack hơn. Tại sao các nhà phát triển CPython không triển khai một GIL phù hợp?
Cập nhật từ ngày 7 tháng 10 năm 2021. Bây giờ tôi đã biết rằng việc hạn chế các luồng trong một lõi chỉ giúp tạo ra hiệu ứng đoàn xe khi máy khách bị hạn chế trong cùng một lõi, đó là cách tôi thiết lập điểm chuẩn. Xem để biết chi tiết
Một GIL thích hợp
Vấn đề cơ bản với GIL là nó can thiệp vào bộ lập lịch của hệ điều hành. Lý tưởng nhất là bạn muốn chạy một chuỗi liên kết I/O ngay khi thao tác I/O mà nó chờ hoàn tất. Và đó là những gì bộ lập lịch hệ điều hành thường làm. Tuy nhiên, trong CPython, luồng sau đó ngay lập tức bị kẹt khi chờ GIL, do đó, quyết định của bộ lập lịch hệ điều hành không thực sự có ý nghĩa gì. Bạn có thể cố gắng loại bỏ khoảng thời gian chuyển đổi để một luồng muốn GIL nhận được nó ngay lập tức, nhưng sau đó bạn gặp sự cố với các luồng liên kết với CPU vì chúng luôn muốn có GIL
Giải pháp thích hợp là phân biệt giữa các chủ đề. Một luồng liên kết I/O có thể lấy GIL khỏi luồng liên kết CPU mà không cần chờ, nhưng các luồng có cùng mức độ ưu tiên sẽ đợi lẫn nhau. Bộ lập lịch hệ điều hành đã phân biệt giữa các luồng, nhưng bạn không thể dựa vào nó vì nó không biết gì về GIL. Có vẻ như tùy chọn duy nhất là triển khai logic lập lịch trong trình thông dịch
Sau khi David Beazley mở vấn đề, các nhà phát triển CPython đã thực hiện một số nỗ lực để giải quyết vấn đề. Bản thân Beazley đã đề xuất một bản vá đơn giản. Tóm lại, bản vá này cho phép luồng liên kết I/O chiếm trước luồng liên kết CPU. Theo mặc định, tất cả các luồng được coi là liên kết I/O. Khi một luồng buộc phải giải phóng GIL, nó sẽ được gắn cờ là bị ràng buộc bởi CPU. Khi một luồng giải phóng GIL một cách tự nguyện, cờ sẽ được đặt lại và luồng đó lại được coi là giới hạn I/O
Bản vá của Beazley đã giải quyết tất cả các vấn đề về GIL mà chúng ta đã thảo luận hôm nay. Tại sao nó chưa được hợp nhất? . Nhiều nhất, bạn có thể cần phải cố gắng hơn một chút để tìm thấy chúng. Một giải pháp thích hợp phải lập lịch trình như một hệ điều hành, hoặc như Nir Aides đã nói
Python thực sự cần một bộ lập lịch, không phải khóa
Vì vậy, các Trợ lý đã triển khai một bộ lập lịch chính thức trong. Bản vá đã hoạt động, nhưng một bộ lập lịch không bao giờ là một thứ tầm thường, vì vậy việc hợp nhất nó với CPython đòi hỏi rất nhiều nỗ lực. Cuối cùng, công việc đã bị bỏ dở vì vào thời điểm đó không có đủ bằng chứng cho thấy sự cố gây ra sự cố trong mã sản xuất. Xem thảo luận để biết thêm chi tiết
GIL chưa bao giờ có một lượng lớn người hâm mộ. Những gì chúng ta đã thấy ngày hôm nay chỉ làm cho nó tồi tệ hơn. Chúng tôi trở lại câu hỏi mọi thời đại
Chúng ta không thể loại bỏ GIL?
Bước đầu tiên để loại bỏ GIL là hiểu tại sao nó tồn tại. Hãy suy nghĩ tại sao bạn thường sử dụng khóa trong chương trình đa luồng và bạn sẽ nhận được câu trả lời. Đó là để ngăn chặn các điều kiện chủng tộc và biến một số hoạt động thành nguyên tử từ quan điểm của các chủ đề khác. Giả sử bạn có một chuỗi các câu lệnh sửa đổi một số cấu trúc dữ liệu. Nếu bạn không bao quanh chuỗi bằng khóa, thì một luồng khác có thể truy cập cấu trúc dữ liệu ở đâu đó ở giữa sửa đổi và nhận được chế độ xem không hoàn chỉnh bị hỏng
Hoặc giả sử bạn tăng cùng một biến từ nhiều luồng. Nếu thao tác gia số không phải là nguyên tử và không được khóa bảo vệ, thì giá trị cuối cùng của biến có thể nhỏ hơn tổng số gia số. Đây là một cuộc đua dữ liệu điển hình
- Chủ đề 1 đọc giá trị
6from multiprocessing import Process # .. the same server code if __name__ == '__main__': Process[target=compute].start[] run_server[]
- Chủ đề 2 đọc giá trị
6from multiprocessing import Process # .. the same server code if __name__ == '__main__': Process[target=compute].start[] run_server[]
- Chủ đề 1 viết lại giá trị
8from multiprocessing import Process # .. the same server code if __name__ == '__main__': Process[target=compute].start[] run_server[]
- Chủ đề 2 ghi lại giá trị
8, do đó loại bỏ các thay đổi được thực hiện bởi Chủ đề 1from multiprocessing import Process # .. the same server code if __name__ == '__main__': Process[target=compute].start[] run_server[]
Trong Python, hoạt động
sum = 0
def f[]:
global sum
for _ in range[1000]:
sum += 1
0 không phải là nguyên tử vì nó bao gồm nhiều hướng dẫn mã byte. Để xem nó có thể dẫn đến các cuộc đua dữ liệu như thế nào, hãy đặt khoảng thời gian chuyển đổi thành from multiprocessing import Process
# .. the same server code
if __name__ == '__main__':
Process[target=compute].start[]
run_server[]
0 và chạy chức năng sau trong nhiều luồngsum = 0
def f[]:
global sum
for _ in range[1000]:
sum += 1
Tương tự, trong C, việc tăng một số nguyên như
sum = 0
def f[]:
global sum
for _ in range[1000]:
sum += 1
2 hoặc sum = 0
def f[]:
global sum
for _ in range[1000]:
sum += 1
3 không phải là số nguyên tử vì trình biên dịch dịch các thao tác đó thành một chuỗi các lệnh máy. Chủ đề có thể xen kẽ ở giữaGIL rất hữu ích vì CPython tăng và giảm các số nguyên có thể được chia sẻ giữa các luồng ở mọi nơi. Đây là cách của CPython để thu gom rác. Mọi đối tượng Python đều có trường đếm tham chiếu. Trường này đếm số địa điểm tham chiếu đối tượng. các đối tượng Python khác, các biến C cục bộ và toàn cầu. Thêm một vị trí làm tăng số lượng tham chiếu. Một nơi ít hơn giảm nó. Khi số lượng tham chiếu đạt đến 0, đối tượng sẽ được giải phóng. Nếu không phải là GIL, một số phần giảm có thể ghi đè lên nhau và đối tượng sẽ ở trong bộ nhớ mãi mãi. Tệ hơn nữa, các gia số bị ghi đè có thể dẫn đến một đối tượng được hủy cấp phát có các tham chiếu đang hoạt động
GIL cũng đơn giản hóa việc triển khai các cấu trúc dữ liệu có thể thay đổi được tích hợp sẵn. Danh sách, ký tự và bộ không sử dụng khóa bên trong, nhưng nhờ có GIL, chúng có thể được sử dụng an toàn trong các chương trình đa luồng. Tương tự, GIL cho phép các luồng truy cập an toàn vào dữ liệu toàn cầu và toàn bộ trình thông dịch. các mô-đun đã tải, các đối tượng được phân bổ trước, các chuỗi được thực hiện, v.v.
Cuối cùng, GIL đơn giản hóa việc viết các phần mở rộng C. Các nhà phát triển có thể cho rằng chỉ có một luồng chạy tiện ích mở rộng C của họ tại bất kỳ thời điểm nào. Do đó, họ không cần sử dụng khóa bổ sung để đảm bảo an toàn cho chuỗi mã. Khi họ muốn chạy mã song song, họ có thể phát hành GIL
Tóm lại, những gì GIL làm là làm cho luồng sau an toàn
đếm tham chiếu;
cấu trúc dữ liệu có thể thay đổi;
dữ liệu toàn cầu và toàn trình thông dịch;
tiện ích mở rộng C
Để xóa GIL mà vẫn có trình thông dịch hoạt động, bạn cần tìm các cơ chế thay thế để đảm bảo an toàn cho luồng. Mọi người đã cố gắng làm điều đó trong quá khứ. Nỗ lực đáng chú ý nhất là dự án Cắt bỏ tử cung của Larry Hastings bắt đầu vào năm 2016. Hastings đã rẽ nhánh CPython, loại bỏ GIL, sửa đổi tham chiếu đếm để sử dụng số tăng và giảm nguyên tử, đồng thời đặt nhiều khóa chi tiết để bảo vệ cấu trúc dữ liệu có thể thay đổi và dữ liệu trên toàn trình thông dịch
Gilectomy có thể chạy một số mã Python và chạy song song. Tuy nhiên, hiệu suất đơn luồng của CPython đã bị xâm phạm. Chỉ riêng việc tăng và giảm nguyên tử đã thêm khoảng 30% chi phí. Hastings đã cố gắng giải quyết vấn đề này bằng cách triển khai bộ đếm tham chiếu được đệm. Nói tóm lại, kỹ thuật này giới hạn tất cả các cập nhật số lượng tham chiếu cho một luồng đặc biệt. Các luồng khác chỉ cam kết tăng và giảm vào nhật ký và luồng đặc biệt đọc nhật ký. Điều này đã làm việc, nhưng chi phí vẫn còn đáng kể
Cuối cùng, rõ ràng là Gilectomy sẽ không được sáp nhập vào CPython. Hastings ngừng làm việc trong dự án. Đó không phải là một thất bại hoàn toàn, mặc dù. Nó đã dạy chúng tôi tại sao việc xóa GIL khỏi CPython lại khó. Có hai lý do chính
- Bộ sưu tập rác dựa trên việc đếm tham chiếu không phù hợp với đa luồng. Giải pháp duy nhất là triển khai trình thu gom rác theo dõi mà JVM, CLR, Go và các thời gian chạy khác không có triển khai GIL
- Xóa GIL sẽ phá vỡ các tiện ích mở rộng C hiện có. Không có cách nào xung quanh nó
Ngày nay không ai nghĩ nghiêm túc về việc loại bỏ GIL. Điều đó có nghĩa là chúng ta sẽ sống với GIL mãi mãi?
Tương lai của đồng thời GIL và Python
Điều này nghe có vẻ đáng sợ, nhưng nhiều khả năng CPython sẽ có nhiều GIL hơn là không có GIL nào cả. Theo nghĩa đen, có một sáng kiến để giới thiệu nhiều GIL cho CPython. Nó được gọi là phiên dịch phụ. Ý tưởng là có nhiều thông dịch viên trong cùng một quy trình. Các luồng trong một trình thông dịch vẫn chia sẻ GIL, nhưng nhiều trình thông dịch có thể chạy song song. Không cần GIL để đồng bộ hóa các trình thông dịch vì chúng không có trạng thái toàn cầu chung và không chia sẻ các đối tượng Python. Tất cả trạng thái toàn cầu được tạo cho mỗi trình thông dịch và trình thông dịch chỉ giao tiếp qua tin nhắn truyền đi. Mục tiêu cuối cùng là giới thiệu cho Python một mô hình tương tranh dựa trên việc giao tiếp các quy trình tuần tự được tìm thấy trong các ngôn ngữ như Go và Clojure
Phiên dịch viên đã là một phần của CPython kể từ phiên bản 1. 5 nhưng chỉ như một cơ chế cách ly. Họ lưu trữ dữ liệu cụ thể cho một nhóm chủ đề. các mô-đun đã tải, nội trang, cài đặt nhập, v.v. Chúng không được hiển thị trong Python, nhưng các tiện ích mở rộng C có thể sử dụng chúng thông qua API Python/C. Tuy nhiên, một số ít thực sự làm điều đó,
sum = 0
def f[]:
global sum
for _ in range[1000]:
sum += 1
4 là một ví dụ đáng chú ýCác thông dịch viên ngày nay bị giới hạn bởi thực tế là họ phải chia sẻ GIL. Điều này chỉ có thể thay đổi khi tất cả trạng thái toàn cầu được tạo cho mỗi trình thông dịch. Công việc đang được thực hiện theo hướng đó, nhưng vẫn còn ít thứ tồn tại trên toàn cầu. một số loại tích hợp sẵn, các đơn lẻ như
sum = 0
def f[]:
global sum
for _ in range[1000]:
sum += 1
5, sum = 0
def f[]:
global sum
for _ in range[1000]:
sum += 1
6 và sum = 0
def f[]:
global sum
for _ in range[1000]:
sum += 1
7 và các phần của bộ cấp phát bộ nhớ. Các tiện ích mở rộng của C cũng cần thoát khỏi trạng thái chung trước khi chúng có thể hoạt động với các trình thông dịch conEric Snow đã viết PEP 554 bổ sung mô-đun
sum = 0
def f[]:
global sum
for _ in range[1000]:
sum += 1
8 vào thư viện chuẩn. Ý tưởng là hiển thị API C của trình thông dịch hiện có cho Python và cung cấp các cơ chế giao tiếp giữa các trình thông dịch. Đề xuất nhắm mục tiêu Python 3. 9 nhưng đã bị hoãn lại cho đến khi GIL được thực hiện cho mỗi phiên dịch viên. Thậm chí sau đó nó không được đảm bảo để thành công. Vấn đề tranh luận là liệu Python có thực sự cần một mô hình tương tranh khác hay khôngMột dự án thú vị khác đang diễn ra hiện nay là Faster CPython. Vào tháng 10 năm 2020, Mark Shannon đã đề xuất một kế hoạch giúp CPython ≈5x nhanh hơn trong vài năm. Và nó thực tế hơn nhiều so với âm thanh của nó vì CPython có rất nhiều tiềm năng để tối ưu hóa. Việc bổ sung JIT một mình có thể dẫn đến tăng hiệu suất rất lớn
Trước đây đã có những dự án tương tự, nhưng chúng thất bại vì thiếu kinh phí hoặc chuyên môn phù hợp. Lần này, Microsoft tình nguyện tài trợ cho Faster CPython và để Mark Shannon, Guido van Rossum, Eric Snow thực hiện dự án. Các thay đổi gia tăng đã được chuyển đến CPython – chúng không cũ trong một ngã ba
CPython nhanh hơn tập trung vào hiệu suất đơn luồng. Nhóm không có kế hoạch thay đổi hoặc xóa GIL. Tuy nhiên, nếu dự án thành công, một trong những điểm yếu chính của Python sẽ được khắc phục và câu hỏi về GIL có thể trở nên phù hợp hơn bao giờ hết
P. S
Điểm chuẩn được sử dụng trong bài đăng này có sẵn trên GitHub. Đặc biệt cảm ơn David Beazley vì những buổi nói chuyện tuyệt vời của anh ấy. Các cuộc nói chuyện của Larry Hastings về GIL và Gilectomy [một, hai, ba] cũng rất thú vị để xem. Để hiểu cách thức hoạt động của bộ lập lịch hệ điều hành hiện đại, tôi đã đọc cuốn sách Phát triển nhân Linux của Robert Love. Rất khuyến khích nó
Nếu bạn muốn nghiên cứu chi tiết hơn về GIL, bạn nên đọc mã nguồn. Tệp
sum = 0
def f[]:
global sum
for _ in range[1000]:
sum += 1
9 là một nơi hoàn hảo để bắt đầu. Để giúp bạn với liên doanh này, tôi đã viết phần tiền thưởng sauChi tiết triển khai của GIL *
Về mặt kỹ thuật, GIL là một cờ cho biết GIL có bị khóa hay không, một tập hợp các biến đột biến và các biến có điều kiện kiểm soát cách đặt cờ này và một số biến tiện ích khác như khoảng thời gian chuyển đổi. Tất cả những thứ này được lưu trữ trong cấu trúc
struct _gil_runtime_state {
/* microseconds [the Python API uses seconds, though] */
unsigned long interval;
/* Last PyThreadState holding / having held the GIL. This helps us
know whether anyone else was scheduled after we dropped the GIL. */
_Py_atomic_address last_holder;
/* Whether the GIL is already taken [-1 if uninitialized]. This is
atomic because it can be read without any lock taken in ceval.c. */
_Py_atomic_int locked;
/* Number of GIL switches since the beginning. */
unsigned long switch_number;
/* This condition variable allows one or several threads to wait
until the GIL is released. In addition, the mutex also protects
the above variables. */
PyCOND_T cond;
PyMUTEX_T mutex;
#ifdef FORCE_SWITCHING
/* This condition variable helps the GIL-releasing thread wait for
a GIL-awaiting thread to be scheduled and take the GIL. */
PyCOND_T switch_cond;
PyMUTEX_T switch_mutex;
#endif
};
0struct _gil_runtime_state {
/* microseconds [the Python API uses seconds, though] */
unsigned long interval;
/* Last PyThreadState holding / having held the GIL. This helps us
know whether anyone else was scheduled after we dropped the GIL. */
_Py_atomic_address last_holder;
/* Whether the GIL is already taken [-1 if uninitialized]. This is
atomic because it can be read without any lock taken in ceval.c. */
_Py_atomic_int locked;
/* Number of GIL switches since the beginning. */
unsigned long switch_number;
/* This condition variable allows one or several threads to wait
until the GIL is released. In addition, the mutex also protects
the above variables. */
PyCOND_T cond;
PyMUTEX_T mutex;
#ifdef FORCE_SWITCHING
/* This condition variable helps the GIL-releasing thread wait for
a GIL-awaiting thread to be scheduled and take the GIL. */
PyCOND_T switch_cond;
PyMUTEX_T switch_mutex;
#endif
};
Cấu trúc ________ 61 ______ 0 là một phần của trạng thái toàn cầu. Nó được lưu trữ trong cấu trúc
struct _gil_runtime_state {
/* microseconds [the Python API uses seconds, though] */
unsigned long interval;
/* Last PyThreadState holding / having held the GIL. This helps us
know whether anyone else was scheduled after we dropped the GIL. */
_Py_atomic_address last_holder;
/* Whether the GIL is already taken [-1 if uninitialized]. This is
atomic because it can be read without any lock taken in ceval.c. */
_Py_atomic_int locked;
/* Number of GIL switches since the beginning. */
unsigned long switch_number;
/* This condition variable allows one or several threads to wait
until the GIL is released. In addition, the mutex also protects
the above variables. */
PyCOND_T cond;
PyMUTEX_T mutex;
#ifdef FORCE_SWITCHING
/* This condition variable helps the GIL-releasing thread wait for
a GIL-awaiting thread to be scheduled and take the GIL. */
PyCOND_T switch_cond;
PyMUTEX_T switch_mutex;
#endif
};
2, cấu trúc này là một phần của struct _gil_runtime_state {
/* microseconds [the Python API uses seconds, though] */
unsigned long interval;
/* Last PyThreadState holding / having held the GIL. This helps us
know whether anyone else was scheduled after we dropped the GIL. */
_Py_atomic_address last_holder;
/* Whether the GIL is already taken [-1 if uninitialized]. This is
atomic because it can be read without any lock taken in ceval.c. */
_Py_atomic_int locked;
/* Number of GIL switches since the beginning. */
unsigned long switch_number;
/* This condition variable allows one or several threads to wait
until the GIL is released. In addition, the mutex also protects
the above variables. */
PyCOND_T cond;
PyMUTEX_T mutex;
#ifdef FORCE_SWITCHING
/* This condition variable helps the GIL-releasing thread wait for
a GIL-awaiting thread to be scheduled and take the GIL. */
PyCOND_T switch_cond;
PyMUTEX_T switch_mutex;
#endif
};
3 mà tất cả các luồng Python đều có quyền truy cậpstruct _ceval_runtime_state {
_Py_atomic_int signals_pending;
struct _gil_runtime_state gil;
};
typedef struct pyruntimestate {
// ...
struct _ceval_runtime_state ceval;
struct _gilstate_runtime_state gilstate;
// ...
} _PyRuntimeState;
Lưu ý rằng
struct _gil_runtime_state {
/* microseconds [the Python API uses seconds, though] */
unsigned long interval;
/* Last PyThreadState holding / having held the GIL. This helps us
know whether anyone else was scheduled after we dropped the GIL. */
_Py_atomic_address last_holder;
/* Whether the GIL is already taken [-1 if uninitialized]. This is
atomic because it can be read without any lock taken in ceval.c. */
_Py_atomic_int locked;
/* Number of GIL switches since the beginning. */
unsigned long switch_number;
/* This condition variable allows one or several threads to wait
until the GIL is released. In addition, the mutex also protects
the above variables. */
PyCOND_T cond;
PyMUTEX_T mutex;
#ifdef FORCE_SWITCHING
/* This condition variable helps the GIL-releasing thread wait for
a GIL-awaiting thread to be scheduled and take the GIL. */
PyCOND_T switch_cond;
PyMUTEX_T switch_mutex;
#endif
};
4 là một cấu trúc khác với struct _gil_runtime_state {
/* microseconds [the Python API uses seconds, though] */
unsigned long interval;
/* Last PyThreadState holding / having held the GIL. This helps us
know whether anyone else was scheduled after we dropped the GIL. */
_Py_atomic_address last_holder;
/* Whether the GIL is already taken [-1 if uninitialized]. This is
atomic because it can be read without any lock taken in ceval.c. */
_Py_atomic_int locked;
/* Number of GIL switches since the beginning. */
unsigned long switch_number;
/* This condition variable allows one or several threads to wait
until the GIL is released. In addition, the mutex also protects
the above variables. */
PyCOND_T cond;
PyMUTEX_T mutex;
#ifdef FORCE_SWITCHING
/* This condition variable helps the GIL-releasing thread wait for
a GIL-awaiting thread to be scheduled and take the GIL. */
PyCOND_T switch_cond;
PyMUTEX_T switch_mutex;
#endif
};
0. Nó lưu trữ thông tin về luồng giữ GILimport threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
0Cuối cùng, có một cấu trúc
struct _gil_runtime_state {
/* microseconds [the Python API uses seconds, though] */
unsigned long interval;
/* Last PyThreadState holding / having held the GIL. This helps us
know whether anyone else was scheduled after we dropped the GIL. */
_Py_atomic_address last_holder;
/* Whether the GIL is already taken [-1 if uninitialized]. This is
atomic because it can be read without any lock taken in ceval.c. */
_Py_atomic_int locked;
/* Number of GIL switches since the beginning. */
unsigned long switch_number;
/* This condition variable allows one or several threads to wait
until the GIL is released. In addition, the mutex also protects
the above variables. */
PyCOND_T cond;
PyMUTEX_T mutex;
#ifdef FORCE_SWITCHING
/* This condition variable helps the GIL-releasing thread wait for
a GIL-awaiting thread to be scheduled and take the GIL. */
PyCOND_T switch_cond;
PyMUTEX_T switch_mutex;
#endif
};
6, là một phần của struct _gil_runtime_state {
/* microseconds [the Python API uses seconds, though] */
unsigned long interval;
/* Last PyThreadState holding / having held the GIL. This helps us
know whether anyone else was scheduled after we dropped the GIL. */
_Py_atomic_address last_holder;
/* Whether the GIL is already taken [-1 if uninitialized]. This is
atomic because it can be read without any lock taken in ceval.c. */
_Py_atomic_int locked;
/* Number of GIL switches since the beginning. */
unsigned long switch_number;
/* This condition variable allows one or several threads to wait
until the GIL is released. In addition, the mutex also protects
the above variables. */
PyCOND_T cond;
PyMUTEX_T mutex;
#ifdef FORCE_SWITCHING
/* This condition variable helps the GIL-releasing thread wait for
a GIL-awaiting thread to be scheduled and take the GIL. */
PyCOND_T switch_cond;
PyMUTEX_T switch_mutex;
#endif
};
7. Nó lưu các cờ def countdown[n]:
while n > 0:
n -= 1
7 và def countdown[n]:
while n > 0:
n -= 1
8import threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
1API Python/C cung cấp và chức năng để lấy và phát hành GIL. Các chức năng này cũng đảm nhiệm việc thiết lập
struct _ceval_runtime_state {
_Py_atomic_int signals_pending;
struct _gil_runtime_state gil;
};
2. Dưới mui xe, tất cả các công việc được thực hiện bởi các chức năng và. Chúng được gọi bởi luồng giữ GIL khi nó tạm dừng thực thi mã byteimport threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
2Trên các hệ thống giống Unix, việc triển khai GIL dựa trên các nguyên mẫu được cung cấp bởi thư viện pthreads. Chúng bao gồm các biến đột biến và các biến có điều kiện. Tóm lại, chúng hoạt động như sau. Một luồng gọi
struct _ceval_runtime_state {
_Py_atomic_int signals_pending;
struct _gil_runtime_state gil;
};
5 để khóa mutex. Khi một chủ đề khác làm như vậy, nó sẽ chặn. Hệ điều hành đặt nó vào hàng đợi các luồng chờ mutex và đánh thức nó khi luồng đầu tiên gọi ____66_______6. Mỗi lần chỉ có một luồng có thể chạy mã được bảo vệCác biến có điều kiện cho phép một luồng đợi cho đến khi một luồng khác thực hiện một số điều kiện đúng. Để đợi một biến có điều kiện, một luồng sẽ khóa một mutex và gọi
struct _ceval_runtime_state {
_Py_atomic_int signals_pending;
struct _gil_runtime_state gil;
};
7 hoặc struct _ceval_runtime_state {
_Py_atomic_int signals_pending;
struct _gil_runtime_state gil;
};
8. Các lệnh gọi này mở khóa mutex một cách nguyên tử và tạo khối luồng. Hệ điều hành đặt luồng vào hàng đợi và đánh thức nó khi một luồng khác gọi struct _ceval_runtime_state {
_Py_atomic_int signals_pending;
struct _gil_runtime_state gil;
};
9. Chủ đề được đánh thức sẽ khóa lại mutex và tiếp tục. Đây là cách các biến có điều kiện thường được sử dụngimport threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
3import threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
4Lưu ý rằng chuỗi đang chờ sẽ kiểm tra điều kiện trong một vòng lặp vì điều đó không được đảm bảo là đúng sau thông báo. Mutex đảm bảo rằng luồng đang chờ không bỏ lỡ điều kiện chuyển từ sai sang đúng
Các hàm
struct _ceval_runtime_state {
_Py_atomic_int signals_pending;
struct _gil_runtime_state gil;
};
3 và struct _ceval_runtime_state {
_Py_atomic_int signals_pending;
struct _gil_runtime_state gil;
};
4 sử dụng biến điều kiện typedef struct pyruntimestate {
// ...
struct _ceval_runtime_state ceval;
struct _gilstate_runtime_state gilstate;
// ...
} _PyRuntimeState;
2 để thông báo cho các luồng đang chờ GIL rằng GIL đã được giải phóng và typedef struct pyruntimestate {
// ...
struct _ceval_runtime_state ceval;
struct _gilstate_runtime_state gilstate;
// ...
} _PyRuntimeState;
3 để thông báo cho luồng đang giữ GIL rằng luồng khác đã lấy GIL. Các biến có điều kiện này được bảo vệ bởi hai mutexes. typedef struct pyruntimestate {
// ...
struct _ceval_runtime_state ceval;
struct _gilstate_runtime_state gilstate;
// ...
} _PyRuntimeState;
4 và typedef struct pyruntimestate {
// ...
struct _ceval_runtime_state ceval;
struct _gilstate_runtime_state gilstate;
// ...
} _PyRuntimeState;
5Đây là các bước của
- Khóa đột biến GIL.
7typedef struct pyruntimestate { // ... struct _ceval_runtime_state ceval; struct _gilstate_runtime_state gilstate; // ... } _PyRuntimeState;
- Xem nếu
8. Nếu không, hãy chuyển sang bước 4typedef struct pyruntimestate { // ... struct _ceval_runtime_state ceval; struct _gilstate_runtime_state gilstate; // ... } _PyRuntimeState;
- Đợi GIL. Trong khi
8typedef struct pyruntimestate { // ... struct _ceval_runtime_state ceval; struct _gilstate_runtime_state gilstate; // ... } _PyRuntimeState;
- Hãy nhớ
00import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Đợi sợi giữ GIL thả GIL.
01import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Nếu hết thời gian và
8 vàtypedef struct pyruntimestate { // ... struct _ceval_runtime_state ceval; struct _gilstate_runtime_state gilstate; // ... } _PyRuntimeState;
00 không thay đổi, hãy yêu cầu chủ đề đang giữ GIL bỏ GIL. bộimport threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
04 vàimport threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
05import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Hãy nhớ
- Lấy GIL và thông báo chủ đề đang giữ GIL mà chúng tôi đã lấy
- Khóa công tắc mutex.
06import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Đặt
8typedef struct pyruntimestate { // ... struct _ceval_runtime_state ceval; struct _gilstate_runtime_state gilstate; // ... } _PyRuntimeState;
- Nếu chúng tôi không phải là chủ đề
08, hãy cập nhậtimport threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
08 và tăng số lượngimport threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
00import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Thông báo cho chủ đề phát hành GIL mà chúng tôi đã lấy GIL.
11import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Mở khóa công tắc mutex.
12import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Khóa công tắc mutex.
- Đặt lại
04import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Tính lại
05import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Mở khóa mutex GIL.
15import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
Lưu ý rằng trong khi một luồng chờ GIL, một luồng khác có thể lấy nó, vì vậy cần phải kiểm tra
import threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
00 để đảm bảo rằng một luồng vừa lấy GIL sẽ không bị buộc phải bỏ nóCuối cùng, đây là các bước của
- Khóa đột biến GIL.
7typedef struct pyruntimestate { // ... struct _ceval_runtime_state ceval; struct _gilstate_runtime_state gilstate; // ... } _PyRuntimeState;
- Đặt lại
8typedef struct pyruntimestate { // ... struct _ceval_runtime_state ceval; struct _gilstate_runtime_state gilstate; // ... } _PyRuntimeState;
- Thông báo cho các chủ đề đang chờ GIL rằng chúng tôi bỏ GIL.
20import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Mở khóa mutex GIL.
15import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Nếu
04, hãy đợi chủ đề khác lấy GILimport threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Khóa công tắc mutex.
06import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Nếu chúng ta vẫn là
08, đợi đã.import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
25import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Mở khóa công tắc mutex.
12import threading def f[a, b, c]: # do something pass t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}] t.start[]
- Khóa công tắc mutex.
Lưu ý rằng luồng giải phóng GIL không cần đợi điều kiện trong vòng lặp. Nó chỉ gọi
import threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
25 để đảm bảo rằng nó không yêu cầu GIL ngay lập tức. Nếu chuyển đổi xảy ra, điều này có nghĩa là một luồng khác đã lấy GIL và bạn có thể cạnh tranh lại GILNếu bạn có bất kỳ câu hỏi, nhận xét hoặc đề xuất nào, vui lòng liên hệ với tôi theo địa chỉ victor@tenthousandmeters. com
Cập nhật từ ngày 7 tháng 10 năm 2021. [1] Việc hạn chế luồng vào một lõi không thực sự khắc phục hiệu ứng đoàn xe. Có, nó buộc HĐH phải chọn luồng nào trong số hai luồng để lên lịch, điều này mang lại cho luồng liên kết I/O cơ hội tốt để yêu cầu lại GIL trên thao tác I/O, nhưng nếu thao tác I/O bị chặn . Trong trường hợp này, luồng liên kết I/O chưa sẵn sàng để lập lịch, vì vậy HĐH sẽ lên lịch cho luồng liên kết CPU.
Trong ví dụ về máy chủ echo, mọi
from threading import Thread
import socket
def run_server[host='127.0.0.1', port=33333]:
sock = socket.socket[]
sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1]
sock.bind[[host, port]]
sock.listen[]
while True:
client_sock, addr = sock.accept[]
print['Connection from', addr]
Thread[target=handle_client, args=[client_sock,]].start[]
def handle_client[sock]:
while True:
received_data = sock.recv[4096]
if not received_data:
break
sock.sendall[received_data]
print['Client disconnected:', sock.getpeername[]]
sock.close[]
if __name__ == '__main__':
run_server[]
5 đang bị chặn một cách hiệu quả – máy chủ đợi máy khách đọc phản hồi và gửi tin nhắn tiếp theo. Hạn chế các chủ đề trong một lõi sẽ không giúp ích gì. Nhưng chúng tôi đã thấy RPS được cải thiện. Tại sao vậy? . Tôi đã chạy ứng dụng khách trên cùng một máy và trên cùng một lõi với các luồng của máy chủ. Thiết lập như vậy buộc HĐH phải chọn giữa luồng liên kết với CPU của máy chủ và luồng máy khách khi luồng liên kết với I/O của máy chủ chặn trên from threading import Thread
import socket
def run_server[host='127.0.0.1', port=33333]:
sock = socket.socket[]
sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1]
sock.bind[[host, port]]
sock.listen[]
while True:
client_sock, addr = sock.accept[]
print['Connection from', addr]
Thread[target=handle_client, args=[client_sock,]].start[]
def handle_client[sock]:
while True:
received_data = sock.recv[4096]
if not received_data:
break
sock.sendall[received_data]
print['Client disconnected:', sock.getpeername[]]
sock.close[]
if __name__ == '__main__':
run_server[]
5. Chuỗi khách hàng có nhiều khả năng được lên lịch. Nó gửi tin nhắn tiếp theo và cũng chặn trên from threading import Thread
import socket
def run_server[host='127.0.0.1', port=33333]:
sock = socket.socket[]
sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1]
sock.bind[[host, port]]
sock.listen[]
while True:
client_sock, addr = sock.accept[]
print['Connection from', addr]
Thread[target=handle_client, args=[client_sock,]].start[]
def handle_client[sock]:
while True:
received_data = sock.recv[4096]
if not received_data:
break
sock.sendall[received_data]
print['Client disconnected:', sock.getpeername[]]
sock.close[]
if __name__ == '__main__':
run_server[]
5. Nhưng bây giờ luồng liên kết I/O của máy chủ đã sẵn sàng và cạnh tranh với luồng liên kết CPU. Về cơ bản, chạy ứng dụng khách trên cùng một lõi lõi, khiến HĐH chọn giữa luồng liên kết I/O và luồng liên kết CPU ngay cả khi chặn from threading import Thread
import socket
def run_server[host='127.0.0.1', port=33333]:
sock = socket.socket[]
sock.setsockopt[socket.SOL_SOCKET, socket.SO_REUSEADDR, 1]
sock.bind[[host, port]]
sock.listen[]
while True:
client_sock, addr = sock.accept[]
print['Connection from', addr]
Thread[target=handle_client, args=[client_sock,]].start[]
def handle_client[sock]:
while True:
received_data = sock.recv[4096]
if not received_data:
break
sock.sendall[received_data]
print['Client disconnected:', sock.getpeername[]]
sock.close[]
if __name__ == '__main__':
run_server[]
5Ngoài ra, bạn không cần sửa đổi mã nguồn CPython hoặc gây rối với
from multiprocessing import Process
# .. the same server code
if __name__ == '__main__':
Process[target=compute].start[]
run_server[]
4 để hạn chế các luồng Python đối với một số lõi nhất định. Trong Linux, chức năng from multiprocessing import Process
# .. the same server code
if __name__ == '__main__':
Process[target=compute].start[]
run_server[]
2 được triển khai trên tòa nhà chọc trời import threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
34 và mô-đun tiêu chuẩn import threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
35 tòa nhà chọc trời này cho Python. Cảm ơn Carl Bordum Hansen đã chỉ ra điều này cho tôiNgoài ra còn có lệnh
import threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
36 cho phép bạn đặt mối quan hệ CPU của một quy trình mà không cần chạm vào mã nguồn. Chỉ cần chạy chương trình như thế nàyimport threading
def f[a, b, c]:
# do something
pass
t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
5Cập nhật từ ngày 16 tháng 10 năm 2021. Sam Gross gần đây đã công bố ngã ba CPython của mình để loại bỏ GIL. Bạn có thể coi dự án này là Gilectomy 2. 0. nó thay thế GIL bằng các cơ chế thay thế để đảm bảo an toàn cho luồng, nhưng không giống như Gilectomy, không làm cho mã đơn luồng chậm hơn nhiều. Trên thực tế, Gross đã tối ưu hóa trình thông dịch để hiệu suất đơn luồng của bản rẽ nhánh không có GIL thậm chí còn nhanh hơn cả CPython 3 dòng chính. 9
Dự án này có vẻ như là nỗ lực hứa hẹn nhất để xóa GIL khỏi CPython. Tôi chắc rằng một số ý tưởng do Gross đề xuất sẽ tìm được đường ngược dòng. Để tìm hiểu thêm về dự án và những ý tưởng đằng sau nó, hãy xem tài liệu thiết kế và repo GitHub. Đây cũng là một bài viết hay trên LWN