Luồng có thực trong Python không?

Vì vậy, bạn đang viết một số mã sử dụng nhiều CPU bằng Python và thực sự cố gắng tìm cách thoát khỏi nhà tù đơn luồng của nó. Bạn có thể đang hướng tới chế độ "song song nopython" của Numba, bạn có thể đang sử dụng các quy trình rẽ nhánh với đa xử lý, bạn có thể đang viết các dịch vụ siêu nhỏ với các bộ điều phối giống như cơ sở dữ liệu hoặc thậm chí viết các chương trình đa luồng của riêng bạn trong C/C++ giống như những người tạo ra TensorFlow đã làm


Trong bài viết này, tôi đang mô tả cơ sở lý luận cho dự án thú cưng của mình, nơi tôi cố gắng triển khai các phương tiện cho mục đích chung đa nhiệm được sử dụng ở dạng mã python đơn giản, sử dụng cách tiếp cận giống như cơ sở dữ liệu để giao tiếp với trình thông dịch, trong khi vẫn giữ GIL [Global



Nó cũng có thể trở nên hữu ích khi hỗ trợ nhiều trình thông dịch sắp tới trong CPython


Theo như tôi biết, không ai đi xa đến mức cố gắng cung cấp cho chương trình Python bộ lưu trữ có thể chia sẻ riêng. Nỗ lực gần nhất gần đây nhất là Chia sẻ đối tượng Python hiện đã chết khá nhiều. Tôi hy vọng dự án của tôi sẽ không gặp số phận tương tự


GIL?

Giả sử ngày mai ai đó đã phát hành phiên bản python không có GIL. Bạn sẽ làm gì tiếp theo? . Cuối cùng, trong các tình huống đơn giản, bạn sẽ truy cập vào một số lượng lớn trạng thái chung bằng cách sử dụng một khóa đơn thực tế hoặc khóa đơn hiệu quả — do đó, phát minh lại GIL. Các đối tượng có thể thay đổi, trình lặp, trình tạo, xử lý ngoại lệ của Python đều là về một máy trạng thái, thực thi từng bước nghiêm ngặt với sự thiếu đồng thời vốn có


Đó là một ngõ cụt khác. bạn không thể làm cho chương trình hiệu quả mà không sửa đổi ngôn ngữ. Tuy nhiên, tôi tin rằng tôi đã tìm ra cách để thực hiện thay đổi nhỏ nhất có thể, giữ hầu hết mã python, tiện ích mở rộng C, GIL thông thường, nhưng giới thiệu một lượng cấu trúc dữ liệu hạn chế trong bộ nhớ dùng chung để chia sẻ dữ liệu chung giữa các quy trình thông dịch viên riêng biệt . Một số bạn có thể đã đoán ra — đó là điều rất gần với những gì RDBMS làm


Đóng ví dụ có thể là một con trăn. Tuy nhiên, cái này nói nhiều hơn về "Python in Postgres", trong khi chúng tôi thực sự muốn có "Postgres in Python", loại truy cập thông thường vào "cơ sở dữ liệu", giống như nó là một đối tượng python thông thường. Giống như ngôn ngữ Clojure, điều này đã truyền cảm hứng cho tôi thực hiện một thủ thuật tương tự cho Python. Chúng tôi cũng muốn chuyển các "đối tượng cơ sở dữ liệu" này cho một số chức năng của bên thứ ba theo cách mã của bên thứ ba không gặp phải sự khác biệt giữa đối tượng python thông thường và đối tượng cơ sở dữ liệu


Thật không may, tôi thấy không có cách nào dễ dàng để chuyển đổi một thực thi tuần tự chung của Python thành một thực thi tuần tự hiệu quả đồng thời. Hãy xem ví dụ


a = CreateSharedDict[]

def func[SharedDict]:
    SharedDict['a'] = 1
    SharedDict['b'] = 2
    return SharedDict

print[func[a]]

Mặc dù mã dường như không gây rắc rối, nhưng một số câu hỏi lớn nảy sinh trong môi trường đồng thời. Điều gì xảy ra khi một chủ đề khác đọc SharedObject cùng một lúc? . g


def func[SharedDict]:
    SharedDict['a'] = 1
    SharedDict['b'] = 1
    SharedDict['b'] += SharedDict['a']
    return SharedDict

Lập trình viên thực sự muốn xem đầu ra


{ a: 1, b: 2 }

Tuy nhiên, với một luồng khác thực hiện "func[a]" tương tự, chúng ta có thể thấy đầu ra


{ a: 1, b: 3 }

Đó không phải là điều mà lập trình viên thường mong đợi. Vì vậy, chúng tôi thực sự muốn tách biệt hai chủ đề, làm cho họ cảm thấy như họ là người dùng duy nhất của Đối tượng được chia sẻ. Giải pháp phổ biến nhất là sử dụng bộ nhớ giao dịch. Nhưng khi nào thì giao dịch bắt đầu và kết thúc? . Nhiều khả năng lập trình viên muốn một cái gì đó như thế này


def func[SharedDict]:
    start_transaction[]
    try:
        SharedDict['a'] = 1
        SharedDict['b'] = 1
        SharedDict['b'] += SharedDict['a']
        return copy[SharedDict]
    finally:
        commit_transaction[]

Điều này yêu cầu lập trình viên đánh dấu rõ ràng điểm bắt đầu và cam kết của giao dịch. Điều thực sự có thể làm dễ dàng nhiệm vụ này là một số trình trang trí để đưa một chức năng vào giao dịch


@transacted
def func[SharedDict]:
    SharedDict['a'] = 1
    SharedDict['b'] = 1
    SharedDict['b'] += SharedDict['a']
    return SharedDict

Trong một ngôn ngữ có sự phụ thuộc dữ liệu rõ ràng, có thể xác định sơ bộ các ô dữ liệu cần thiết và, ví dụ, khóa chúng trong suốt thời gian giao dịch, vì vậy giao dịch sẽ luôn thành công nếu bắt đầu thành công. Tuy nhiên, trong mã python, một số hành động không mong muốn có thể phát sinh ở đây và ở đó, vì vậy chúng tôi có thể yêu cầu khôi phục xung đột với giao dịch khác. Điều này mang lại cho chúng tôi hai yêu cầu chính để bộ nhớ giao dịch này hoạt động


  • khả năng hủy bỏ các thay đổi
  • không có tác dụng phụ không thể hủy bỏ

Cái đầu tiên có thể được thực hiện với các đối tượng dữ liệu ma thuật được tạo đặc biệt cho dữ liệu được chia sẻ của chúng tôi. Phần thứ hai đưa chúng ta trở lại cách chúng ta khai báo giao dịch trong mã của mình. Khi chúng tôi đã khai báo một số chức năng với trình trang trí "@transacted", chúng tôi phải đảm bảo chỉ "các đối tượng ma thuật" được sử dụng bên trong. Tôi không hoàn toàn chắc chắn về cách triển khai điều đó, nhưng một trong những ý tưởng là tạo một "phạm vi toàn cầu" riêng cho các hàm được giao dịch bằng cách sử dụng cho phép đặt phạm vi tùy chỉnh và chấp nhận các đối tượng mã [vì vậy không cần biên dịch lại hàm từ văn bản . Bằng cách này, chúng tôi có thể dịch các đối tượng python thông thường thành các đối tượng có thể chia sẻ khi tham gia giao dịch và có thể dịch ngược lại khi rời khỏi giao dịch. Tất nhiên, gọi một số chức năng bên ngoài từ bên trong giao dịch không được tính là "rời khỏi giao dịch", vì vậy chúng tôi có thể tránh dịch ở đây


Dữ liệu bất biến và gõ nghiêm ngặt [sẽ rất tuyệt nếu có]

Mặc dù python chủ yếu dựa trên dữ liệu có thể thay đổi, nhưng việc có một số dữ liệu không thể thay đổi thực sự hữu ích trong môi trường đa nhiệm. Dữ liệu bất biến về cơ bản là không bị kiểm soát, bởi vì bạn không thể sửa đổi nó và các trình đọc không xung đột với nhau. Nhưng dữ liệu bất biến không phải là ý tưởng đối với python, đó là lý do tại sao lập trình viên cần khai báo rõ ràng khái niệm phi ý thức hệ với nội dung như


@transacted
def func[SharedDict]:
    SharedDict['a'] = 1
    SharedDict['b'] = 1
    SharedDict['b'] += SharedDict['a']
    return immutable[SharedDict]

Cấu trúc đối tượng được xác định trước có tầm quan trọng tương tự đối với đa nhiệm — việc phải quản lý các khóa trong quá trình sửa đổi cấu trúc là một gánh nặng. Thật không may, các giải pháp để xác định các loại xung quanh Python đều tệ hại - chúng cắt xén mã và làm cho nó trông không có gì khó hiểu. Vì vậy, bất kỳ ý tưởng được chào đón


Lười biếng vs Háo hức

Các triển khai phổ biến của bộ nhớ giao dịch phần mềm [STM] sử dụng mô hình giao dịch lười biếng, trong đó bạn tạo một bản sao dữ liệu khi bắt đầu giao dịch, chạy giao dịch và cuối cùng kiểm tra nguyên tử các ô dữ liệu để không bị sửa đổi trong khi thực hiện các thay đổi vào các ô này. Vấn đề chính của phương pháp này là xử lý các tình huống tranh chấp cao. khi bạn có 10 luồng chạy trong 100 mili giây giao dịch bên trong và 10 mili giây giao dịch bên ngoài, bạn sẽ có hơn 99% số lần khôi phục i. e. chương trình đa nhiệm chạy chậm hơn một nhiệm vụ. Bởi vì Python có thể chậm e. g. ai đó bất cẩn thực hiện các yêu cầu HTTP bên trong các giao dịch hoặc chỉ tính toán quan trọng, các giao dịch lười biếng thực sự không phù hợp với mục đích của chúng tôi, ít nhất là một thuật toán giải quyết xung đột chính


Mô hình giao dịch háo hức thực sự giống với quyền truy cập khóa chi tiết thông thường, ngoại trừ việc xử lý "bế tắc". khi một giao dịch gặp tài nguyên đã bị khóa, một giao dịch sẽ giữ khóa và giao dịch khác sẽ giải phóng khóa. Như tôi đã đề cập, trong Python, chúng tôi không thể xác định tất cả các tài nguyên cần thiết trước khi giao dịch bắt đầu. Vì vậy, tôi thực sự đã triển khai khóa đọc-ghi số nhiều có thể hủy bỏ với sự công bằng dựa trên vé. Một cái gì đó chưa từng có theo như tôi biết. Tuy nhiên, vì một lý do - điều này thực sự khó thực hiện một cách hiệu quả, tôi. e. không chỉ sử dụng các spinlock ngây thơ mà còn đặt một chuỗi chờ trẻ hơn vào chế độ ngủ để HĐH [Hệ điều hành] có thể sử dụng CPU [bạn biết] cho một số công việc khác bao gồm chạy giao dịch xung đột có mức độ ưu tiên cao [cũ hơn]. Nói một cách đơn giản, nó hoạt động như thế này



Do đó, đối với các tranh chấp nặng nề, bạn có được hiệu suất của một nhiệm vụ duy nhất, trong khi hiếm khi có tranh chấp, bạn sẽ có được hiệu suất đồng thời tốt. Và người đọc hoàn toàn không chặn nhau, giống như trong mô hình giao dịch lười biếng [vì khóa đọc-ghi]


Bây giờ bạn có thể đang hỏi. nếu tôi muốn một số loại truy cập không chặn đơn giản thì sao? . Tôi không cảm thấy như một điều khiển đồng thời đa phiên bản toàn diện phù hợp ở đây, bởi vì nó không phải là ý thức hệ đối với mã python [chỉ mong đợi một trạng thái duy nhất] và sẽ trở thành một gánh nặng không cần thiết và nặng nề


Quản lý bộ nhớ

Trình quản lý bộ nhớ thích hợp nhất cho bộ nhớ dùng chung mà tôi tìm thấy là "Bộ nhớ dùng chung được quản lý" của Boost, mặc dù nó chỉ triển khai một tập hợp con nhỏ các tính năng cần thiết cho một trình quản lý bộ nhớ hoạt động hoàn chỉnh, vì vậy tôi đã triển khai một trình quản lý bộ nhớ đơn giản bao gồm các đống liên kết luồng [do đó . Các khối nhỏ được kết hợp thành các phân đoạn, mỗi phân đoạn chứa các khối có cùng kích thước. Điều này tương tự với trình quản lý bộ nhớ giống như Tích trữ, mặc dù cách triển khai hiện tại của tôi thực sự ngây thơ và không thể giải phóng các trang trở lại HĐH [vẫn có thể sử dụng lại các khối đã giải phóng từ các trang này]


Phần thú vị nhất bắt đầu từ việc khôi phục bộ nhớ. Chúng tôi có thể đã sử dụng cách đếm tham chiếu đơn giản, nhưng nó chỉ hoạt động đối với các đối tượng bất biến đơn giản. Mọi thứ trở nên phức tạp hơn với các đối tượng phức tạp tham chiếu đến các đối tượng khác. Xem xét đoạn mã sau


myvar = someobject.a

Điều gì xảy ra ở đây với việc đếm tham chiếu là


________số 8_______

Bây giờ ở giữa "local_var_1 = dereference[someobject]->a" và "dereference[local_var_1]->increment_reference_count" chúng ta không thể chắc chắn "someobject. a" không thay đổi. Nó có thể đã bị xóa, do đó số lượng tham chiếu giảm đi và đối tượng được giải phóng. Vì vậy, chúng tôi cần hoãn việc thu hồi bộ nhớ cho đến khi các ô được hủy đăng ký không được sử dụng 100%. Và bạn có thể đã đoán được khi điều này xảy ra — khi giao dịch kết thúc. Chúng ta chỉ cần chờ kết thúc tất cả các giao dịch có thể thấy tham chiếu. Nó có thể được thực hiện với bất kỳ nhân viên nào không giao dịch hoặc sử dụng một nhân viên riêng biệt dành riêng cho nhiệm vụ cụ thể này. Như tôi đã tìm thấy theo thời gian, nó được gọi là "khai hoang dựa trên kỷ nguyên" và nó đã được sử dụng. g. trong nhân Linux để cải tạo bộ nhớ trong cấu trúc dữ liệu RCU, mặc dù nhân Linux sử dụng cơ chế khác để xác định đường viền kỷ nguyên


Cũng có một lợi thế nhỏ của quá trình cải tạo này. chúng ta có thể nhóm các khối để có ít truy cập thường xuyên hơn trên các đống liên kết luồng, đây là nguồn tranh chấp chính khi việc khai hoang được thực hiện ngay khi số lượng tham chiếu giảm xuống 0


Bạn có thể nói. tại sao lại tính tham chiếu ngay từ đầu mà không theo dõi bộ thu gom rác [GC]? . Thật không may, hiện tại tôi không thể thấy bất kỳ cách khả thi nào để tổ chức GC theo dõi mà không làm hỏng hiệu suất hệ thống. Có, JVM, CLR, Go, V8 có GC đồng thời. Tuy nhiên, chúng thường thực thi mã được quản lý chứa đầy các rào cản để thông báo cho một GC đồng thời về các tham chiếu đã sửa đổi được truy tìm lại một lần nữa trong các điều kiện dừng trên thế giới. Vì vậy, tất cả chúng đều yêu cầu dừng mọi luồng để thực hiện các hành động cuối cùng. Tuy nhiên, việc dừng tất cả các công nhân python có thể mất vĩnh viễn vì một số trong số chúng bị kẹt trong mã gốc bên ngoài GIL, ngoài tầm với của chúng tôi


Ngoài ra, việc khai hoang dựa trên kỷ nguyên cho phép chúng tôi hoãn sửa đổi số lượng tham chiếu cho đến khi kết thúc giao dịch, bởi vì không thể hủy đối tượng nào nếu nó được nhìn thấy trước khi bắt đầu giao dịch


Bất chấp tất cả những điều đó, tôi không muốn nói rằng việc thu gom rác là không thể ở đây - nó vẫn có thể thực hiện được bằng cách này hay cách khác


Ứng dụng khả thi

Các phương thức hợp tác cơ bản của công nhân mà tôi thấy là. nhà sản xuất đến tay nhiều người tiêu dùng; . Ba cái đầu tiên có thể được hợp nhất thành một nguyên thủy "kênh" duy nhất. Chúng có thể được sử dụng ngay lập tức cho các trang web và dịch vụ HTTP để triển khai một hệ thống hoàn chỉnh với trình trung gian thông báo bằng Python thuần túy và để triển khai bộ đệm cho Django/Pyramid hoặc thậm chí là cơ sở dữ liệu trong bộ nhớ chính thức với độ bền được đảm bảo bởi sự tồn tại của nhiều


Có một nỗ lực để giới thiệu hỗ trợ nhiều trình thông dịch trong CPython
https. //www. con trăn. org/dev/peps/pep-0554/
Điều này sẽ mang lại nhiều không gian hơn bằng cách quay trở lại không gian địa chỉ và các đối tượng nhân của một quy trình, do đó tự động giải quyết các vấn đề như dịch con trỏ và sao chép các đối tượng nhân giữa các quy trình


Tôi thực sự rất vui khi thấy nhiều Python hơn trong phát triển GUI, bởi vì đó là thứ tôi đã viết nhiều nhất trong nhà cung cấp dịch vụ của mình. Thật không may, các thư viện GUI cổ điển như GTK+, Qt, WPF, trình bao bọc Win32 gốc [ATL, MFC, VCL] đều là các luồng đơn. ý tôi là. bạn có thể có nhiều luồng, nhưng bạn không bao giờ được chạm vào bất kỳ đối tượng GUI nào bên ngoài luồng chính. Sẽ hơi lạ nếu bạn chỉ nghĩ về việc có bao nhiêu thứ phụ trợ trong GUI có thể hoạt động đồng thời và logic chính của ứng dụng thường nhỏ đến mức nào


Hiện trạng triển khai

Tôi có một số triển khai cơ bản của các thùng chứa danh sách và từ điển [bản đồ băm] có hỗ trợ khôi phục, cũng như các loại bất biến cơ bản. chuỗi, số nguyên, boolean. Tôi thực sự ước mình có thể triển khai nhiều hơn, nhưng tôi làm điều đó trong thời gian rảnh rỗi và những điều này cũng yêu cầu tôi triển khai một số nội dung cơ bản trước. trình quản lý bộ nhớ dùng chung, luồng phục hồi bộ nhớ, các đối tượng đồng bộ hóa [bộ nhớ dùng chung mutex, hai loại sự kiện] và thứ phức tạp nhất là khóa đọc-ghi số nhiều. Đặc biệt, việc triển khai kênh vẫn còn thiếu do thiếu triển khai hàng đợi giao dịch [tôi không thể đơn giản sử dụng thứ gì đó như std. deque vì nó không hỗ trợ rollback trực tiếp]


Nó giống như 11 nghìn dòng mã C, với khoảng 3 nghìn dòng nhận xét và quy trình gỡ lỗi. Tại thời điểm hiện tại, tôi đang cố gắng thực hiện một số loại phát hành trước alpha-preview càng sớm càng tốt để có thể kiểm tra nó bên ngoài IDE của tôi. Tuy nhiên, trước khi phát hành mã, tôi thực sự rất vui khi nghe một số ý tưởng, đề xuất và phê bình của bạn


Đặc biệt tôi muốn đề cập đến vấn đề ổn định hệ thống. Bộ nhớ dùng chung bị hỏng rất có thể dẫn đến sự cố của toàn bộ hệ thống. Ví dụ: để giải quyết vấn đề này, LMDB sử dụng ánh xạ tệp chỉ đọc và sửa đổi bộ nhớ dùng chung bằng cách sử dụng các thao tác I/O tệp, do đó sử dụng bộ đệm đệm hợp nhất trong HĐH. Để giải quyết vấn đề hiện tại, tôi hạn chế số lượng mã được phép truy cập trực tiếp vào bộ nhớ dùng chung. Ngoài ra, tôi đang suy nghĩ về một số loại phát hiện cho các công nhân bị treo/bị treo, nếu không, điều này sẽ khiến công việc của bộ thu hồi bộ nhớ bị đình trệ và sự cố là một điều phổ biến trong quá trình thử nghiệm

Đa luồng có đạt được trong Python không?

Làm cách nào để đạt được đa luồng trong python? . Bạn cần nhập nó. Trước khi sử dụng mô-đun này, chúng tôi cũng cài đặt môi trường anaconda bằng lệnh cài đặt sau

Tại sao Python không phải là luồng

Python không an toàn theo luồng và ban đầu được thiết kế với một thứ gọi là GIL hoặc Khóa phiên dịch toàn cầu, đảm bảo các quy trình được thực thi tuần tự trên CPU của máy tính. On the surface, this means Python programs cannot support multiprocessing.

Vấn đề với đa luồng trong Python là gì?

Nhược điểm. Khi chuyển đổi ngữ cảnh xảy ra, nó chặn tiến trình, vì tiến trình đang duy trì các luồng nên các luồng cũng chặn. Ứng dụng đa luồng không thể tận dụng đa xử lý .

Chủ Đề