Khóa có không cần thiết trong mã Python đa luồng vì GIL không?


76

Nếu bạn đang dựa vào việc triển khai Python có Khóa thông dịch viên toàn cầu (tức là CPython) và viết mã đa luồng, bạn có thực sự cần khóa không?

Nếu GIL không cho phép nhiều lệnh được thực thi song song, thì dữ liệu được chia sẻ có cần thiết phải bảo vệ không?

xin lỗi nếu đây là một câu hỏi ngớ ngẩn, nhưng đó là điều mà tôi luôn thắc mắc về Python trên các máy đa xử lý / lõi.

điều tương tự sẽ áp dụng cho bất kỳ triển khai ngôn ngữ nào khác có GIL.


1
Cũng lưu ý rằng GIL là và chi tiết triển khai. Ví dụ như IronPython và Jython không có GIL.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

Câu trả lời:


72

Bạn sẽ vẫn cần khóa nếu bạn chia sẻ trạng thái giữa các luồng. GIL chỉ bảo vệ trình thông dịch nội bộ. Bạn vẫn có thể có các bản cập nhật không nhất quán trong mã của riêng mình.

Ví dụ:

#!/usr/bin/env python
import threading

shared_balance = 0

class Deposit(threading.Thread):
    def run(self):
        for _ in xrange(1000000):
            global shared_balance
            balance = shared_balance
            balance += 100
            shared_balance = balance

class Withdraw(threading.Thread):
    def run(self):
        for _ in xrange(1000000):
            global shared_balance
            balance = shared_balance
            balance -= 100
            shared_balance = balance

threads = [Deposit(), Withdraw()]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

print shared_balance

Tại đây, mã của bạn có thể bị gián đoạn giữa việc đọc trạng thái được chia sẻ ( balance = shared_balance) và ghi kết quả đã thay đổi trở lại ( shared_balance = balance), gây ra bản cập nhật bị mất. Kết quả là một giá trị ngẫu nhiên cho trạng thái được chia sẻ.

Để làm cho các bản cập nhật nhất quán, các phương thức chạy sẽ cần phải khóa trạng thái được chia sẻ xung quanh các phần đọc-sửa-ghi (bên trong các vòng lặp) hoặc có một số cách để phát hiện khi nào trạng thái được chia sẻ đã thay đổi kể từ khi nó được đọc .


Ví dụ mã cung cấp một sự hiểu biết rõ ràng và trực quan! Bài đăng tốt đẹp Harris! Tôi ước tôi có thể bỏ phiếu hai lần!
RayLuo

Sẽ an toàn nếu chỉ có một dòng shared_balance += 100shared_balance -= 100?
mrgloom

24

Không - GIL chỉ bảo vệ nội bộ python khỏi nhiều luồng thay đổi trạng thái của chúng. Đây là mức khóa rất thấp, chỉ đủ để giữ cho cấu trúc của python ở trạng thái nhất quán. Nó không bao gồm việc khóa cấp độ ứng dụng mà bạn cần làm để bảo vệ an toàn luồng trong mã của riêng bạn.

Bản chất của khóa là đảm bảo rằng một khối mã cụ thể chỉ được thực thi bởi một luồng. GIL thực thi điều này đối với các khối có kích thước bằng một mã bytecode, nhưng thông thường bạn muốn khóa mở rộng một khối mã lớn hơn thế này.



9

Bài đăng này mô tả GIL ở mức khá cao:

Đặc biệt quan tâm là những trích dẫn sau:

Cứ sau mười hướng dẫn (mặc định này có thể được thay đổi), lõi sẽ giải phóng GIL cho luồng hiện tại. Tại thời điểm đó, HĐH chọn một luồng từ tất cả các luồng cạnh tranh cho khóa (có thể chọn cùng một luồng vừa giải phóng GIL - bạn không có bất kỳ quyền kiểm soát nào đối với luồng được chọn); luồng đó nhận GIL và sau đó chạy thêm mười mã byte khác.

Lưu ý cẩn thận rằng GIL chỉ hạn chế mã Python thuần túy. Tiện ích mở rộng (thư viện Python bên ngoài thường được viết bằng C) có thể được viết để giải phóng khóa, sau đó cho phép trình thông dịch Python chạy riêng biệt với tiện ích mở rộng cho đến khi tiện ích mở rộng yêu cầu lại khóa.

Có vẻ như GIL chỉ cung cấp ít trường hợp có thể hơn cho một công tắc ngữ cảnh và làm cho các hệ thống đa lõi / bộ xử lý hoạt động như một lõi đơn, đối với từng phiên bản trình thông dịch python, vì vậy, bạn vẫn cần sử dụng cơ chế đồng bộ hóa.


2
Lưu ý, sys.getcheckinterval()cho bạn biết có bao nhiêu lệnh bytecode được thực thi giữa các "bản phát hành GIL" (và đó là 100 (không phải 10) kể từ ít nhất 2,5). Trong 3.2, nó có thể chuyển sang khoảng thời gian dựa trên thời gian (5ms hoặc lâu hơn) thay vì đếm lệnh. Thay đổi này cũng có thể được áp dụng cho 2.7 mặc dù nó vẫn đang được tiến hành.
Peter Hansen

8

Khóa thông dịch viên toàn cầu ngăn các luồng truy cập trình thông dịch đồng thời (do đó CPython chỉ sử dụng một lõi). Tuy nhiên, theo tôi hiểu, các luồng vẫn bị gián đoạn và được lên lịch trước , có nghĩa là bạn vẫn cần khóa trên các cấu trúc dữ liệu được chia sẻ, kẻo các luồng của bạn dẫm chân lên nhau.

Câu trả lời mà tôi đã gặp hết lần này đến lần khác là đa luồng trong Python hiếm khi đáng giá, vì điều này. Tôi đã nghe những điều tốt đẹp về dự án PyProcessing , dự án này làm cho việc chạy nhiều quy trình trở nên "đơn giản" như đa luồng, với cấu trúc dữ liệu được chia sẻ, hàng đợi, v.v. (PyProcessing sẽ được đưa vào thư viện tiêu chuẩn của Python 2.6 sắp tới dưới dạng mô-đun đa xử lý .) Điều này giúp bạn tìm hiểu GIL, vì mỗi quy trình đều có trình thông dịch riêng.


4

Nghĩ theo cách này:

Trên một máy tính xử lý đơn, đa luồng xảy ra bằng cách tạm dừng một luồng và bắt đầu một luồng khác đủ nhanh để làm cho nó có vẻ đang chạy cùng một lúc. Điều này giống như Python với GIL: chỉ có một luồng thực sự đang chạy.

Vấn đề là luồng có thể bị treo ở bất kỳ đâu, ví dụ: nếu tôi muốn tính b = (a + b) * 3, điều này có thể tạo ra các hướng dẫn như sau:

1    a += b
2    a *= 3
3    b = a

Bây giờ, giả sử rằng đang chạy trong một luồng và luồng đó bị treo sau dòng 1 hoặc 2 và sau đó một luồng khác bắt đầu và chạy:

b = 5

Sau đó, khi luồng khác tiếp tục, b sẽ bị ghi đè bởi các giá trị được tính toán cũ, điều này có thể không phải là những gì mong đợi.

Vì vậy, bạn có thể thấy rằng mặc dù chúng không THỰC SỰ đang chạy cùng một lúc, bạn vẫn cần khóa.


1

Bạn vẫn cần sử dụng khóa (mã của bạn có thể bị gián đoạn bất kỳ lúc nào để thực thi một chuỗi khác và điều này có thể gây ra sự mâu thuẫn dữ liệu). Vấn đề với GIL là nó ngăn mã Python sử dụng nhiều lõi hơn cùng lúc (hoặc nhiều bộ xử lý nếu chúng có sẵn).


1

Khóa vẫn cần thiết. Tôi sẽ cố gắng giải thích tại sao chúng lại cần thiết.

Bất kỳ hoạt động / lệnh nào được thực thi trong trình thông dịch. GIL đảm bảo rằng trình thông dịch được giữ bởi một luồng duy nhất tại một thời điểm cụ thể . Và chương trình của bạn với nhiều luồng hoạt động trong một trình thông dịch duy nhất. Tại bất kỳ thời điểm cụ thể nào, trình thông dịch này được giữ bởi một luồng duy nhất. Nó có nghĩa là chỉ luồng đang giữ trình thông dịch mới chạy bất kỳ lúc nào.

Giả sử có hai luồng, chẳng hạn t1 và t2, và cả hai đều muốn thực hiện hai lệnh đang đọc giá trị của một biến toàn cục và tăng nó lên.

#increment value
global var
read_var = var
var = read_var + 1

Như đã nói ở trên, GIL chỉ đảm bảo rằng hai luồng không thể thực hiện một lệnh đồng thời, có nghĩa là cả hai luồng không thể thực thi read_var = vartại bất kỳ thời điểm cụ thể nào. Nhưng họ có thể thực hiện lần lượt các chỉ dẫn và bạn vẫn có thể gặp sự cố. Hãy xem xét tình huống này:

  • Giả sử read_var là 0.
  • GIL được giữ bởi luồng t1.
  • t1 thực hiện read_var = var. Vì vậy, read_var ở t1 là 0. GIL sẽ chỉ đảm bảo rằng thao tác đọc này sẽ không được thực thi cho bất kỳ luồng nào khác tại thời điểm này.
  • GIL được cấp cho luồng t2.
  • t2 thực hiện read_var = var. Nhưng read_var vẫn là 0. Vì vậy, read_var trong t2 là 0.
  • GIL được trao cho t1.
  • t1 thực thi var = read_var+1và var trở thành 1.
  • GIL được trao cho t2.
  • t2 cho rằng read_var = 0, vì đó là những gì nó đọc.
  • t2 thực thi var = read_var+1và var trở thành 1.
  • Kỳ vọng của chúng tôi đã vartrở thành 2.
  • Vì vậy, một khóa phải được sử dụng để giữ cả việc đọc và tăng dần như một hoạt động nguyên tử.
  • Câu trả lời của Harris sẽ giải thích nó thông qua một ví dụ mã.

0

Một chút cập nhật từ ví dụ của Will Harris:

class Withdraw(threading.Thread):  
def run(self):            
    for _ in xrange(1000000):  
        global shared_balance  
        if shared_balance >= 100:
          balance = shared_balance
          balance -= 100  
          shared_balance = balance

Đặt một tuyên bố kiểm tra giá trị khi rút tiền và tôi không thấy tiêu cực nữa và các cập nhật có vẻ nhất quán. Câu hỏi của tôi là:

Nếu GIL ngăn chỉ một luồng có thể được thực thi tại bất kỳ thời điểm nguyên tử nào, thì giá trị cũ sẽ ở đâu? Nếu không có giá trị cũ, tại sao chúng ta cần khóa? (Giả sử chúng ta chỉ nói về mã python thuần túy)

Nếu tôi hiểu đúng, kiểm tra điều kiện ở trên sẽ không hoạt động trong môi trường phân luồng thực . Khi nhiều luồng đang thực thi đồng thời, giá trị cũ có thể được tạo ra do đó trạng thái chia sẻ không nhất quán, khi đó bạn thực sự cần một khóa. Nhưng nếu python thực sự chỉ cho phép chỉ một luồng bất kỳ lúc nào (phân luồng theo thời gian), thì không thể có giá trị cũ tồn tại, phải không?


Có vẻ như GIL không khóa chuỗi toàn bộ thời gian và chuyển đổi ngữ cảnh vẫn có thể xảy ra. Vì vậy, tôi đã sai, khóa vẫn là cần thiết.
jimx 22/12/08
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.