Tại sao luồng Python chậm?

Trong hướng dẫn này, bạn sẽ khám phá ra anti-pattern khi sử dụng ThreadPool và cách tránh nó trong các dự án của bạn.

Bắt đầu nào

Mục lục

  • ThreadPool có thể chậm hơn vòng lặp For
  • ThreadPool có thể chậm hơn đối với các tác vụ liên quan đến CPU
  • Ví dụ về ThreadPool chậm hơn vòng lặp For
    • Nhiệm vụ giới hạn CPU trong vòng lặp For
    • Nhiệm vụ giới hạn CPU trong ThreadPool
  • Tại sao The ThreadPool không chậm hơn nhiều?
  • Câu hỏi thường gặp
    • Điều gì xảy ra nếu chúng ta sử dụng nhiều chủ đề hơn trong ThreadPool?
    • Điều gì xảy ra nếu chúng ta sử dụng một luồng trong ThreadPool?
    • Worker Threads trong ThreadPool có chạy trên các lõi khác nhau không?
    • Việc sử dụng nhóm đa xử lý sẽ tăng tốc ví dụ này chứ?
  • mang đi

ThreadPool có thể chậm hơn vòng lặp For

đa xử lý. hồ bơi. ThreadPool trong Python cung cấp một nhóm các luồng có thể tái sử dụng để thực hiện các tác vụ đặc biệt

Một đối tượng nhóm luồng kiểm soát nhóm luồng công nhân mà công việc có thể được gửi tới

— đa xử lý — Song song dựa trên quy trình

Lớp ThreadPool mở rộng lớp Pool. Lớp Pool cung cấp một nhóm các quy trình công nhân cho đồng thời dựa trên quy trình

Mặc dù lớp ThreadPool nằm trong mô-đun đa xử lý nhưng lớp này cung cấp tính đồng thời dựa trên luồng và phù hợp nhất với các tác vụ liên kết với IO, chẳng hạn như đọc hoặc ghi từ ổ cắm hoặc tệp

Một ThreadPool có thể được cấu hình khi nó được tạo, nó sẽ chuẩn bị các luồng mới

Chúng ta có thể đưa ra các tác vụ một lần cho ThreadPool bằng các phương thức như apply[] hoặc chúng ta có thể áp dụng chức năng tương tự cho một mục có thể lặp lại bằng các phương thức như map[]

Sau đó, kết quả cho các tác vụ đã phát hành có thể được truy xuất đồng bộ hoặc chúng tôi có thể truy xuất kết quả của các tác vụ sau này bằng cách sử dụng các phiên bản không đồng bộ của các phương thức, chẳng hạn như apply_async[] và map_async[]

ThreadPool được thiết kế để tăng tốc chương trình của bạn bằng cách thực hiện đồng thời các tác vụ

Tuy nhiên, trong một số trường hợp sử dụng, việc sử dụng ThreadPool có thể khiến chương trình của bạn chậm hơn. Đôi khi chậm hơn đáng kể so với việc thực hiện cùng một tác vụ trong vòng lặp for

Làm thế nào ThreadPool có thể làm cho chương trình của bạn chậm hơn?

Chạy các vòng lặp của bạn bằng cách sử dụng tất cả các CPU, tải xuống cuốn sách MIỄN PHÍ của tôi để tìm hiểu cách thực hiện

ThreadPool có thể chậm hơn đối với các tác vụ liên quan đến CPU

Sử dụng ThreadPool cho tác vụ liên quan đến CPU có thể chậm hơn so với việc không sử dụng nó

Điều này là do các chuỗi Python bị hạn chế bởi Khóa phiên dịch toàn cầu hoặc GIL

GIL là một mẫu lập trình trong trình thông dịch Python tham chiếu [CPython] sử dụng đồng bộ hóa để đảm bảo rằng chỉ một luồng có thể thực thi các lệnh tại một thời điểm trong quy trình Python

Điều này có nghĩa là mặc dù chúng ta có thể có nhiều luồng trong nhóm luồng nhưng chỉ một luồng có thể thực thi tại một thời điểm

Điều này tốt khi các tác vụ được thực thi bởi nhóm luồng đang bị chặn, chẳng hạn như các tác vụ liên kết với IO có thể đọc từ một tệp hoặc kết nối internet

Đây là sự cố khi các tác vụ được thực thi bởi nhóm luồng bị ràng buộc bởi CPU, nghĩa là tốc độ thực thi của chúng được xác định bởi tốc độ của CPU. Các tác vụ này không bị chặn và do đó chạy nhanh nhất có thể. Do có GIL, các luồng thực thi các tác vụ này sẽ chạy lần lượt và dẫm lên nhau thông qua chuyển đổi ngữ cảnh

Chuyển đổi ngữ cảnh là một mẫu lập trình cho phép nhiều hơn một luồng thực thi chạy trên một CPU, chẳng hạn như. g. thay đổi “bối cảnh” cho CPU thực thi các lệnh. Trong một context switch, hệ điều hành sẽ lưu trữ trạng thái của luồng đang thực thi để nó có thể được tiếp tục lại sau đó và cho phép một luồng thực thi khác chạy và lưu trữ trạng thái của nó

Đây là một vấn đề với các tác vụ liên quan đến CPU vì chuyển ngữ cảnh là một hoạt động tương đối tốn kém. Có nhiều luồng chạy cùng lúc với cùng mức độ ưu tiên trên cùng một loại tác vụ sẽ có thể buộc hệ điều hành phải thường xuyên chuyển ngữ cảnh giữa chúng, dẫn đến chi phí tính toán không cần thiết

Kết quả là tác vụ tổng thể có thể sẽ chậm hơn khi thực thi nó với ThreadPool so với thực thi trực tiếp trong vòng lặp for

Cho rằng việc thực thi các tác vụ liên quan đến CPU với ThreadPool có thể sẽ dẫn đến hiệu suất tương tự hoặc kém hơn, chúng tôi có thể coi cách sử dụng này là một cách chống mẫu. Đó là một ThreadPool sử dụng có thể dễ dàng xác định và phải tránh, e. g. một giải pháp tồi cho vấn đề đồng thời trong Python

  • Sử dụng ThreadPool cho các Tác vụ liên kết với CPU là một Anti-pattern

đa xử lý. hồ bơi. Nhóm có lẽ nên được sử dụng thay thế cho các tác vụ liên quan đến CPU. Điều này là do nó sử dụng các quy trình thay vì các luồng và do đó, nó không bị ràng buộc bởi GIL

Bây giờ chúng ta đã biết tại sao việc sử dụng ThreadPool có thể chậm hơn vòng lặp for trong một số trường hợp, hãy xem một ví dụ đã hoạt động

Bối rối với API lớp ThreadPool?
Tải xuống bảng cheat PDF MIỄN PHÍ của tôi

Ví dụ về ThreadPool chậm hơn vòng lặp For

Hãy xem một ví dụ trong đó việc sử dụng ThreadPool có thể chậm hơn so với vòng lặp for

Nhiệm vụ giới hạn CPU trong vòng lặp For

Trước tiên, hãy xác định một tác vụ đơn giản liên kết với CPU để thực thi nhiều lần

Trong trường hợp này, chúng ta có thể bình phương một số. Nghĩa là, được cung cấp một số đầu vào, trả về giá trị bình phương

1

2

3

# thực hiện một số phép toán

def hoạt động[giá trị]:

    trả về giá trị**2

Tiếp theo, hãy thực hiện thao tác này nhiều lần, chẳng hạn như một triệu [10.000.000] lần và báo cáo một thông báo khi chúng tôi thực hiện xong. Tức là ta sẽ bình phương các số từ 0 đến 9.999.999

Chúng ta có thể sử dụng cách hiểu danh sách, đó là vòng lặp for Pythonic

1

2

3

4

.. .

# thực hiện một phép toán nhiều lần

giá trị = [hoạt động[i] for i in range[10000000]]

in['xong']

Điều này có thể dễ dàng được viết trực tiếp dưới dạng vòng lặp for;

1

2

3

4

5

6

.. .

# thực hiện một phép toán nhiều lần

giá trị = danh sách[]

cho i trong phạm vi[10000000]:

    giá trị. chắp thêm[thao tác[i]]

in['xong']

Hoặc sử dụng hàm map[], hàm này cũng có thể mang tính Pythonic hơn;

1

2

3

4

.. .

# thực hiện một phép toán nhiều lần

giá trị = danh sách[bản đồ[operation, range[10000000]]]

in['xong']

Chúng tôi sẽ gắn bó với việc hiểu danh sách. Ví dụ đầy đủ được liệt kê dưới đây

1

2

3

4

5

6

7

8

9

10

11

12

13

#Trăn Siêu Nhanh. com

# ví dụ về việc thực hiện một nhiệm vụ toán học đơn giản nhiều lần trong vòng lặp for

 

# thực hiện một số phép toán

def hoạt động[giá trị]:

    trả về giá trị**2

 

#bảo vệ điểm vào

if __name__ == '__main__'.

    # thực hiện một phép toán nhiều lần

    # giá trị = [hoạt động[i] cho tôi trong phạm vi[1000000]]

    giá trị = [hoạt động[i] for i in range[10000000]]

    in['xong']

Code chạy nhanh, hoàn thành trong khoảng 3. 2 giây trên hệ thống của tôi

Mất bao lâu để chạy trên hệ thống của bạn?
Hãy cho tôi biết trong phần nhận xét bên dưới.

Tiếp theo, hãy thực hiện đồng thời tác vụ bằng cách sử dụng ThreadPool

Nhiệm vụ giới hạn CPU trong ThreadPool

Chúng ta có thể cập nhật mã từ ví dụ trước để sử dụng ThreadPool

Đây sẽ là một phản mẫu như đã mô tả trước đây, do đó, chúng tôi hy vọng ví dụ này sẽ chạy nhanh như pr chậm hơn so với phiên bản vòng lặp for do chi phí chuyển đổi ngữ cảnh

Đầu tiên, chúng ta có thể tạo một nhóm luồng với một số luồng, trong trường hợp này là 4. Sau đó, chúng ta có thể sử dụng phương thức map[] trên ThreadPool để gửi các tác vụ vào nhóm luồng. Mỗi nhiệm vụ sẽ là bình phương một số, với các số từ 0 đến 9.999.999 được gửi vào nhóm để thực hiện

1

2

3

4

.. .

# thực hiện một phép toán nhiều lần

với Nhóm luồng[4] as pool:

    kết quả = nhóm. bản đồ[hoạt động, phạm vi[10000000]]

Ví dụ đầy đủ được liệt kê dưới đây

1

2

3

4

5

6

7

8

9

10

11

12

13

14

#Trăn Siêu Nhanh. com

# ví dụ về cách sử dụng nhóm luồng có thể chậm hơn, phản mẫu

từ đa xử lý. nhóm nhập ThreadPool

 

# thực hiện một số phép toán

def hoạt động[giá trị]:

    trả về giá trị**2

 

#bảo vệ điểm vào

if __name__ == '__main__'.

    # thực hiện một phép toán nhiều lần

    với ThreadPool[4] as pool:

        kết quả = nhóm. bản đồ[hoạt động, phạm vi[10000000]]

    in['xong']

Chạy ví dụ bình phương tất cả các số như trước

Trên hệ thống của tôi, phải mất khoảng 3. 3 giây để hoàn thành so với 3. 2 giây được thực hiện với vòng lặp for

Điều này chậm hơn một chút, chậm hơn 100 mili giây hoặc gần bằng tốc độ không mang lại lợi ích gì

Khóa học Python ThreadPool miễn phí

Tải xuống bảng cheat API ThreadPool của tôi và như một phần thưởng, bạn sẽ nhận được quyền truy cập MIỄN PHÍ vào khóa học email 7 ngày của tôi

Khám phá cách sử dụng ThreadPool bao gồm cách định cấu hình số luồng công nhân và cách thực thi các tác vụ không đồng bộ

Tìm hiểu thêm
 

Tại sao The ThreadPool không chậm hơn nhiều?

Lý do là theo mặc định, map[] sử dụng giá trị đối số "chunksize" khác 1

Nhớ lại rằng đối số "chunksize" cho phương thức map[] kiểm soát việc ánh xạ các tác vụ đã phát hành tới các tác vụ nội bộ được truyền tới các luồng công nhân để thực thi. Nó cho phép các lệnh gọi hàm đã phát hành được nhóm thành các đợt gọi là khối để thực thi, mang lại lợi ích tính toán lớn

Nếu “chunksize” được đặt thành 1, nghĩa là một tác vụ cho mỗi lệnh gọi hàm, thì ví dụ này sẽ chạy chậm hơn rất nhiều

Ví dụ

1

2

3

4

5

6

7

8

9

10

11

12

13

14

#Trăn Siêu Nhanh. com

# ví dụ về cách sử dụng nhóm luồng có thể chậm hơn, phản mẫu

từ đa xử lý. nhóm nhập ThreadPool

 

# thực hiện một số phép toán

def hoạt động[giá trị]:

    trả về giá trị**2

 

#bảo vệ điểm vào

if __name__ == '__main__'.

    # thực hiện một phép toán nhiều lần

    với ThreadPool[4] as pool:

        kết quả = nhóm. bản đồ[hoạt động, phạm vi[10000000], chunksize=1]

    in['xong']

Chạy ví dụ bình phương tất cả các số như trước, nhưng nó chậm hơn đáng kể

Trên hệ thống của tôi, phải mất khoảng 44. 3 giây để hoàn thành, so với 3. 2 giây cho phiên bản for-loop

Đó là 41. 1 giây dài hơn hoặc 13. chậm hơn 8 lần

Một lần nữa, lý do thiếu cải tiến là do tác vụ là tác vụ gắn với CPU và ThreadPool sử dụng các luồng tuân theo GIL, nghĩa là chỉ một luồng có thể thực thi tại một thời điểm và hệ điều hành sẽ chuyển đổi ngữ cảnh giữa chúng.

Choáng ngợp trước các API đồng thời của python?
Để tìm sự giải thoát, hãy tải xuống Bản đồ tư duy về đồng thời Python MIỄN PHÍ của tôi

Câu hỏi thường gặp

Phần này trả lời một số câu hỏi thường gặp về ví dụ này

Điều gì xảy ra nếu chúng ta sử dụng nhiều chủ đề hơn trong ThreadPool?

Sử dụng nhiều luồng hơn sẽ không cải thiện hiệu suất vì lý do tương tự như việc sử dụng 4 luồng không tăng tốc độ thực thi tác vụ

GIL đảm bảo chỉ có một luồng thực thi các lệnh tại một thời điểm và hệ điều hành sẽ chuyển ngữ cảnh giữa các tác vụ, bổ sung thêm chi phí đáng kể cho tác vụ tổng thể

Điều gì xảy ra nếu chúng ta sử dụng một luồng trong ThreadPool?

ThreadPool sẽ vẫn chậm hơn vòng lặp for ngay cả khi nhóm luồng có một luồng

Lý do là vì tất cả các chi phí bổ sung trong nhóm luồng để đóng gói từng tác vụ bằng cách sử dụng các lớp bên trong và các lệnh gọi hàm bổ sung khi tác vụ nảy xung quanh bên trong nhóm luồng để thực thi

Đây là lý do tại sao ngay cả khi sử dụng Nhóm đa xử lý cho một tác vụ đơn giản như vậy cũng có thể không giúp tăng tốc, ít nhất là đối với ví dụ này

Worker Threads trong ThreadPool có chạy trên các lõi khác nhau không?

Chắc là không

Hệ điều hành xác định mã nào sẽ chạy trên mỗi lõi CPU trong hệ thống của bạn

Vì chỉ có một luồng có thể thực thi tại một thời điểm trong ví dụ này nên rất có khả năng một lõi CPU sẽ được sử dụng

Việc sử dụng nhóm đa xử lý sẽ tăng tốc ví dụ này chứ?

Có lẽ

Thao tác rất đơn giản và chúng tôi đang thực hiện hàng triệu lần

Nếu chúng ta đẩy các tác vụ này vào một Nhóm đa xử lý bằng cách sử dụng map[], trước tiên chúng ta sẽ phải điều chỉnh đối số chunksize xác định ánh xạ các tác vụ mà chúng ta gửi tới các tác vụ nội bộ được tuần tự hóa và truyền đến các quy trình worker trong nhóm

Sau đó, việc thực thi các tác vụ trong nhóm quy trình sẽ thêm chi phí hoạt động, trước tiên là để gói bổ sung các tác vụ bằng cách sử dụng các đối tượng bên trong và các lời gọi hàm bổ sung cần được thực hiện, và thứ hai là cho giao tiếp giữa các quy trình cần thiết để chia sẻ các tác vụ và kết quả của chúng

Tại sao các chuỗi Python quá chậm?

Điều này là do Các chuỗi Python bị ràng buộc bởi Khóa thông dịch viên toàn cầu hoặc GIL . GIL là một mẫu lập trình trong trình thông dịch Python tham chiếu [CPython] sử dụng đồng bộ hóa để đảm bảo rằng chỉ một luồng có thể thực thi các lệnh tại một thời điểm trong quy trình Python.

Tại sao Python không tốt cho luồng?

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

Luồng Python có nhanh hơn không?

Cả đa luồng và đa xử lý đều cho phép mã Python chạy đồng thời. Chỉ đa xử lý mới cho phép mã của bạn thực sự song song. Tuy nhiên, nếu mã của bạn nặng về IO [như yêu cầu HTTP], thì đa luồng vẫn có thể tăng tốc mã của bạn .

Tại sao phân luồng chậm?

Mọi luồng đều cần một số tài nguyên hệ thống và chi phí hoạt động , do đó, nó cũng làm chậm hiệu suất. Một vấn đề khác được gọi là "sự bùng nổ luồng" khi THÊM luồng được tạo ra so với số lõi trên hệ thống. Và một số chủ đề chờ kết thúc các chủ đề khác là ý tưởng tồi tệ nhất cho đa luồng.

Chủ Đề