Tại sao Python có GIL

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

  1. khởi tạo trình thông dịch;
  2. biên dịch mã Python thành mã byte;
  3. 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[]
9

Vò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ới

import 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ư sau

Số 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. 58

Thờ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

  1. giải phóng GIL;
  2. thực hiện thao tác, e. g.
    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[]
    
    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;
  3. 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ồng

Giả 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. 60

Chuyể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ậy

from 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 GIL

Hệ đ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ịch

Trê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ớn

Má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ợp

Bạ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 CPU

Khoả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,500

Kế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ại

Khoả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. 44

Mộ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ả sau

Số luồng liên kết với CPU01248RPS24k12k3k3010

Má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

  1. Chủ đề 1 đọc giá trị
    from multiprocessing import Process
    
    # .. the same server code
    
    if __name__ == '__main__':
        Process[target=compute].start[]
        run_server[]
    
    6
  2. Chủ đề 2 đọc giá trị
    from multiprocessing import Process
    
    # .. the same server code
    
    if __name__ == '__main__':
        Process[target=compute].start[]
        run_server[]
    
    6
  3. Chủ đề 1 viết lại giá trị
    from multiprocessing import Process
    
    # .. the same server code
    
    if __name__ == '__main__':
        Process[target=compute].start[]
        run_server[]
    
    8
  4. Chủ đề 2 ghi lại giá trị
    from multiprocessing import Process
    
    # .. the same server code
    
    if __name__ == '__main__':
        Process[target=compute].start[]
        run_server[]
    
    8, do đó loại bỏ các thay đổi được thực hiện bởi Chủ đề 1

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ồng

sum = 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ữa

GIL 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

  1. đếm tham chiếu;

  2. cấu trúc dữ liệu có thể thay đổi;

  3. dữ liệu toàn cầu và toàn trình thông dịch;

  4. 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

  1. 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
  2. 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 con

Eric 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ông

Mộ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 sau

Chi 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
};
0

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
};

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ập

struct _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ữ GIL

import threading

def f[a, b, c]:
    # do something
    pass

t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
0

Cuố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
8

import threading

def f[a, b, c]:
    # do something
    pass

t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
1

API 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ã byte

import threading

def f[a, b, c]:
    # do something
    pass

t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
2

Trê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ụng

import threading

def f[a, b, c]:
    # do something
    pass

t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
3

import threading

def f[a, b, c]:
    # do something
    pass

t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
4

Lư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

  1. Khóa đột biến GIL.
    typedef struct pyruntimestate {
        // ...
        struct _ceval_runtime_state ceval;
        struct _gilstate_runtime_state gilstate;
    
        // ...
    } _PyRuntimeState;
    
    7
  2. Xem nếu
    typedef struct pyruntimestate {
        // ...
        struct _ceval_runtime_state ceval;
        struct _gilstate_runtime_state gilstate;
    
        // ...
    } _PyRuntimeState;
    
    8. Nếu không, hãy chuyển sang bước 4
  3. Đợi GIL. Trong khi
    typedef struct pyruntimestate {
        // ...
        struct _ceval_runtime_state ceval;
        struct _gilstate_runtime_state gilstate;
    
        // ...
    } _PyRuntimeState;
    
    8
    1. Hãy nhớ
      import threading
      
      def f[a, b, c]:
          # do something
          pass
      
      t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
      t.start[]
      
      00
    2. Đợi sợi giữ GIL thả GIL.
      import threading
      
      def f[a, b, c]:
          # do something
          pass
      
      t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
      t.start[]
      
      01
    3. Nếu hết thời gian và
      typedef struct pyruntimestate {
          // ...
          struct _ceval_runtime_state ceval;
          struct _gilstate_runtime_state gilstate;
      
          // ...
      } _PyRuntimeState;
      
      8 và
      import threading
      
      def f[a, b, c]:
          # do something
          pass
      
      t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
      t.start[]
      
      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[]
      
      05
  4. Lấy GIL và thông báo chủ đề đang giữ GIL mà chúng tôi đã lấy
    1. Khóa công tắc mutex.
      import threading
      
      def f[a, b, c]:
          # do something
          pass
      
      t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
      t.start[]
      
      06
    2. Đặt
      typedef struct pyruntimestate {
          // ...
          struct _ceval_runtime_state ceval;
          struct _gilstate_runtime_state gilstate;
      
          // ...
      } _PyRuntimeState;
      
      8
    3. Nếu chúng tôi không phải là chủ đề
      import threading
      
      def f[a, b, c]:
          # do something
          pass
      
      t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
      t.start[]
      
      08, hãy cập nhật
      import 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ượng
      import threading
      
      def f[a, b, c]:
          # do something
          pass
      
      t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
      t.start[]
      
      00
    4. Thông báo cho chủ đề phát hành GIL mà chúng tôi đã lấy GIL.
      import threading
      
      def f[a, b, c]:
          # do something
          pass
      
      t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
      t.start[]
      
      11
    5. Mở khóa công tắc mutex.
      import threading
      
      def f[a, b, c]:
          # do something
          pass
      
      t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
      t.start[]
      
      12
  5. Đặt lại
    import threading
    
    def f[a, b, c]:
        # do something
        pass
    
    t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
    t.start[]
    
    04
  6. Tính lại
    import threading
    
    def f[a, b, c]:
        # do something
        pass
    
    t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
    t.start[]
    
    05
  7. Mở khóa mutex GIL.
    import threading
    
    def f[a, b, c]:
        # do something
        pass
    
    t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
    t.start[]
    
    15

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

  1. Khóa đột biến GIL.
    typedef struct pyruntimestate {
        // ...
        struct _ceval_runtime_state ceval;
        struct _gilstate_runtime_state gilstate;
    
        // ...
    } _PyRuntimeState;
    
    7
  2. Đặt lại
    typedef struct pyruntimestate {
        // ...
        struct _ceval_runtime_state ceval;
        struct _gilstate_runtime_state gilstate;
    
        // ...
    } _PyRuntimeState;
    
    8
  3. Thông báo cho các chủ đề đang chờ GIL rằng chúng tôi bỏ GIL.
    import threading
    
    def f[a, b, c]:
        # do something
        pass
    
    t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
    t.start[]
    
    20
  4. Mở khóa mutex GIL.
    import threading
    
    def f[a, b, c]:
        # do something
        pass
    
    t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
    t.start[]
    
    15
  5. Nếu
    import threading
    
    def f[a, b, c]:
        # do something
        pass
    
    t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
    t.start[]
    
    04, hãy đợi chủ đề khác lấy GIL
    1. Khóa công tắc mutex.
      import threading
      
      def f[a, b, c]:
          # do something
          pass
      
      t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
      t.start[]
      
      06
    2. Nếu chúng ta vẫn là
      import threading
      
      def f[a, b, c]:
          # do something
          pass
      
      t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
      t.start[]
      
      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[]
      
      25
    3. Mở khóa công tắc mutex.
      import threading
      
      def f[a, b, c]:
          # do something
          pass
      
      t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
      t.start[]
      
      12

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 GIL


Nế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[]
5

Ngoà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ôi

Ngoà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ày

import threading

def f[a, b, c]:
    # do something
    pass

t = threading.Thread[target=f, args=[1, 2], kwargs={'c': 3}]
t.start[]
5


Cậ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

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.

GIL có tạo chuỗi Python không

Mã an toàn luồng . 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. The GIL's protection occurs at the interpreter-state level. With the GIL in place, for instance, the integration of non-thread-safe C extension is easier because you can explicitly acquire and release the GIL from the C code, thus making your extension thread-safe at the Python level.

Đ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 .

Chủ Đề