Đồng tiền trong Python - Hướng dẫn nhanh
Trong chương này, chúng ta sẽ hiểu khái niệm đồng thời trong Python và tìm hiểu về các luồng và quy trình khác nhau.
Concurrency là gì?
Nói một cách dễ hiểu, đồng thời là sự xuất hiện của hai hoặc nhiều sự kiện cùng một lúc. Đồng thời là một hiện tượng tự nhiên vì nhiều sự kiện xảy ra đồng thời tại bất kỳ thời điểm nào.
Về mặt lập trình, đồng thời là khi hai tác vụ chồng chéo lên nhau trong quá trình thực thi. Với lập trình đồng thời, hiệu suất của các ứng dụng và hệ thống phần mềm của chúng tôi có thể được cải thiện bởi vì chúng tôi có thể đồng thời giải quyết các yêu cầu thay vì chờ đợi một yêu cầu trước đó được hoàn thành.
Đánh giá lịch sử của đồng tiền
Những điểm sau đây sẽ cung cấp cho chúng ta đánh giá lịch sử ngắn gọn về đồng thời -
Từ khái niệm về đường sắt
Đồng thời có quan hệ mật thiết với khái niệm đường sắt. Với đường sắt, cần phải xử lý nhiều đoàn tàu trên cùng một hệ thống đường sắt để mọi đoàn tàu đều đến đích một cách an toàn.
Máy tính đồng thời trong học viện
Mối quan tâm đến đồng thời trong khoa học máy tính bắt đầu từ bài báo nghiên cứu được xuất bản bởi Edsger W. Dijkstra vào năm 1965. Trong bài báo này, ông đã xác định và giải quyết vấn đề loại trừ lẫn nhau, tính chất của điều khiển đồng thời.
Nguyên thủy đồng thời cấp cao
Trong thời gian gần đây, các lập trình viên đang nhận được các giải pháp đồng thời được cải thiện vì sự ra đời của các nguyên mẫu đồng thời cấp cao.
Cải thiện tính đồng thời với các ngôn ngữ lập trình
Các ngôn ngữ lập trình như Golang, Rust và Python của Google đã có những bước phát triển đáng kinh ngạc trong các lĩnh vực giúp chúng tôi có được các giải pháp đồng thời tốt hơn.
Luồng & đa luồng là gì?
Threadlà đơn vị thực thi nhỏ nhất có thể được thực hiện trong một hệ điều hành. Bản thân nó không phải là một chương trình mà chạy trong một chương trình. Nói cách khác, các chủ đề không độc lập với nhau. Mỗi luồng chia sẻ phần mã, phần dữ liệu, v.v. với các luồng khác. Chúng còn được gọi là các quy trình nhẹ.
Một chuỗi bao gồm các thành phần sau:
Bộ đếm chương trình bao gồm địa chỉ của lệnh thực thi tiếp theo
Stack
Tập hợp các thanh ghi
Một id duy nhất
MultithreadingMặt khác, là khả năng của CPU để quản lý việc sử dụng hệ điều hành bằng cách thực thi nhiều luồng đồng thời. Ý tưởng chính của đa luồng là đạt được sự song song bằng cách chia một quy trình thành nhiều luồng. Có thể hiểu khái niệm đa luồng với sự trợ giúp của ví dụ sau.
Thí dụ
Giả sử chúng ta đang chạy một quy trình cụ thể, trong đó chúng ta mở MS Word để nhập nội dung vào đó. Một chuỗi sẽ được chỉ định để mở MS Word và một chuỗi khác sẽ được yêu cầu để nhập nội dung vào đó. Và bây giờ, nếu chúng ta muốn chỉnh sửa hiện có thì một luồng khác sẽ được yêu cầu để thực hiện tác vụ chỉnh sửa, v.v.
Quá trình & đa xử lý là gì?
Aprocessđược định nghĩa như một thực thể, đại diện cho đơn vị công việc cơ bản được thực hiện trong hệ thống. Nói một cách dễ hiểu, chúng ta viết các chương trình máy tính của mình trong một tệp văn bản và khi chúng ta thực thi chương trình này, nó sẽ trở thành một quá trình thực hiện tất cả các tác vụ được đề cập trong chương trình. Trong suốt vòng đời của quy trình, nó trải qua các giai đoạn khác nhau - Bắt đầu, Sẵn sàng, Chạy, Chờ đợi và Kết thúc.
Sơ đồ sau đây cho thấy các giai đoạn khác nhau của một quy trình:
Một tiến trình có thể chỉ có một luồng, được gọi là luồng chính, hoặc nhiều luồng có bộ thanh ghi, bộ đếm chương trình và ngăn xếp riêng của chúng. Sơ đồ sau sẽ cho chúng ta thấy sự khác biệt -
Multiprocessing,mặt khác, là việc sử dụng hai hoặc nhiều đơn vị CPU trong một hệ thống máy tính duy nhất. Mục tiêu chính của chúng tôi là khai thác toàn bộ tiềm năng từ phần cứng của mình. Để đạt được điều này, chúng ta cần sử dụng đầy đủ số lõi CPU có sẵn trong hệ thống máy tính của mình. Đa xử lý là cách tốt nhất để làm như vậy.
Python là một trong những ngôn ngữ lập trình phổ biến nhất. Tiếp theo là một số lý do khiến nó phù hợp với các ứng dụng đồng thời -
Cú pháp đặc biệt
Đường cú pháp là cú pháp trong một ngôn ngữ lập trình được thiết kế để giúp mọi thứ dễ đọc hoặc dễ diễn đạt hơn. Nó làm cho ngôn ngữ trở nên “ngọt ngào hơn” đối với con người: mọi thứ có thể được diễn đạt rõ ràng hơn, ngắn gọn hơn hoặc theo một phong cách khác tùy theo sở thích. Python đi kèm với các phương thức Magic, có thể được định nghĩa để hoạt động trên các đối tượng. Các phương pháp Magic này được sử dụng như một đường cú pháp và gắn với các từ khóa dễ hiểu hơn.
Cộng đồng lớn
Ngôn ngữ Python đã chứng kiến một tỷ lệ chấp nhận lớn giữa các nhà khoa học dữ liệu và toán học, làm việc trong lĩnh vực AI, học máy, học sâu và phân tích định lượng.
Các API hữu ích để lập trình đồng thời
Python 2 và 3 có số lượng lớn các API dành riêng cho lập trình song song / đồng thời. Phổ biến nhất trong số họ làthreading, concurrent.features, multiprocessing, asyncio, gevent and greenlets, Vân vân.
Hạn chế của Python trong việc triển khai các ứng dụng đồng thời
Python đi kèm với một giới hạn cho các ứng dụng đồng thời. Hạn chế này được gọi làGIL (Global Interpreter Lock)có mặt trong Python. GIL không bao giờ cho phép chúng tôi sử dụng nhiều lõi của CPU và do đó chúng tôi có thể nói rằng không có luồng thực sự trong Python. Chúng ta có thể hiểu khái niệm GIL như sau:
GIL (Khóa thông dịch viên toàn cầu)
Nó là một trong những chủ đề gây tranh cãi nhất trong thế giới Python. Trong CPython, GIL là mutex - khóa loại trừ lẫn nhau, giúp mọi thứ diễn ra an toàn. Nói cách khác, chúng ta có thể nói rằng GIL ngăn nhiều luồng thực thi mã Python song song. Khóa chỉ có thể được giữ bởi một luồng tại một thời điểm và nếu chúng ta muốn thực thi một luồng thì trước tiên nó phải có được khóa. Sơ đồ dưới đây sẽ giúp bạn hiểu hoạt động của GIL.
Tuy nhiên, có một số thư viện và triển khai trong Python như Numpy, Jpython và IronPytbhon. Các thư viện này hoạt động mà không có bất kỳ tương tác nào với GIL.
Cả hai phương thức đồng thời và song song đều được sử dụng liên quan đến các chương trình đa luồng nhưng có rất nhiều sự nhầm lẫn về sự giống và khác nhau giữa chúng. Câu hỏi lớn về vấn đề này: có phải là song song đồng thời hay không? Mặc dù cả hai thuật ngữ xuất hiện khá giống nhau nhưng câu trả lời cho câu hỏi trên là KHÔNG, đồng thời và song song không giống nhau. Bây giờ, nếu chúng không giống nhau thì sự khác biệt cơ bản giữa chúng là gì?
Nói một cách dễ hiểu, đồng thời đề cập đến việc quản lý quyền truy cập vào trạng thái chia sẻ từ các luồng khác nhau và mặt khác, song song đề cập đến việc sử dụng nhiều CPU hoặc lõi của nó để cải thiện hiệu suất của phần cứng.
Chi tiết về đồng tiền
Đồng thời là khi hai nhiệm vụ chồng chéo lên nhau trong quá trình thực thi. Đó có thể là một tình huống trong đó một ứng dụng đang thực hiện nhiều nhiệm vụ cùng một lúc. Chúng ta có thể hiểu nó theo sơ đồ; nhiều nhiệm vụ đang tiến hành cùng một lúc, như sau:
Mức độ tương tranh
Trong phần này, chúng ta sẽ thảo luận về ba cấp độ quan trọng của đồng thời về mặt lập trình -
Đồng tiền cấp thấp
Trong mức độ đồng thời này, có sử dụng rõ ràng các phép toán nguyên tử. Chúng tôi không thể sử dụng loại đồng thời như vậy để xây dựng ứng dụng, vì nó rất dễ xảy ra lỗi và khó gỡ lỗi. Ngay cả Python cũng không hỗ trợ loại đồng thời như vậy.
Đồng tiền cấp trung
Trong sự đồng thời này, không sử dụng các phép toán nguyên tử rõ ràng. Nó sử dụng các khóa rõ ràng. Python và các ngôn ngữ lập trình khác hỗ trợ loại đồng thời như vậy. Hầu hết các lập trình viên ứng dụng sử dụng đồng thời này.
Đồng tiền cấp cao
Trong sự đồng thời này, không sử dụng các phép toán nguyên tử rõ ràng cũng như các khóa rõ ràng. Python cóconcurrent.futures mô-đun để hỗ trợ loại đồng thời như vậy.
Thuộc tính của hệ thống đồng thời
Đối với một chương trình hoặc hệ thống đồng thời là chính xác, một số thuộc tính phải được thỏa mãn bởi nó. Các thuộc tính liên quan đến việc chấm dứt hệ thống như sau:
Thuộc tính độ đúng
Thuộc tính độ đúng có nghĩa là chương trình hoặc hệ thống phải cung cấp câu trả lời đúng mong muốn. Để đơn giản, chúng ta có thể nói rằng hệ thống phải ánh xạ trạng thái chương trình bắt đầu sang trạng thái cuối cùng một cách chính xác.
Tài sản an toàn
Thuộc tính an toàn có nghĩa là chương trình hoặc hệ thống phải duy trì trong một “good” hoặc là “safe” trạng thái và không bao giờ làm bất cứ điều gì “bad”.
Tài sản sống
Thuộc tính này có nghĩa là một chương trình hoặc hệ thống phải “make progress” và nó sẽ đạt đến một số trạng thái mong muốn.
Tác nhân của hệ thống đồng thời
Đây là một thuộc tính chung của hệ thống đồng thời, trong đó có thể có nhiều quy trình và luồng chạy cùng một lúc để tạo tiến độ cho các tác vụ của riêng chúng. Các tiến trình và luồng này được gọi là các tác nhân của hệ thống đồng thời.
Tài nguyên của các hệ thống đồng thời
Các tác nhân phải sử dụng các tài nguyên như bộ nhớ, đĩa, máy in, v.v. để thực hiện nhiệm vụ của họ.
Một số quy tắc
Mọi hệ thống đồng thời phải có một bộ quy tắc để xác định loại nhiệm vụ được thực hiện bởi các tác nhân và thời gian cho mỗi loại. Các nhiệm vụ có thể là lấy khóa, chia sẻ bộ nhớ, sửa đổi trạng thái, v.v.
Rào cản của các hệ thống đồng thời
Chia sẻ dữ liệu
Một vấn đề quan trọng trong khi triển khai các hệ thống đồng thời là việc chia sẻ dữ liệu giữa nhiều luồng hoặc quy trình. Trên thực tế, lập trình viên phải đảm bảo rằng các khóa bảo vệ dữ liệu được chia sẻ để tất cả các quyền truy cập vào nó được tuần tự hóa và chỉ một luồng hoặc quy trình có thể truy cập dữ liệu được chia sẻ tại một thời điểm. Trong trường hợp, khi nhiều luồng hoặc quy trình đều đang cố gắng truy cập vào cùng một dữ liệu được chia sẻ thì không phải tất cả nhưng ít nhất một trong số chúng sẽ bị chặn và sẽ không hoạt động. Nói cách khác, chúng ta có thể nói rằng chúng ta chỉ có thể sử dụng một quá trình hoặc một luồng tại một thời điểm khi khóa có hiệu lực. Có thể có một số giải pháp đơn giản để loại bỏ các rào cản nêu trên -
Hạn chế chia sẻ dữ liệu
Giải pháp đơn giản nhất là không chia sẻ bất kỳ dữ liệu có thể thay đổi nào. Trong trường hợp này, chúng ta không cần sử dụng khóa rõ ràng và rào cản đồng thời do dữ liệu lẫn nhau sẽ được giải quyết.
Hỗ trợ cấu trúc dữ liệu
Nhiều khi các quy trình đồng thời cần truy cập cùng một dữ liệu cùng một lúc. Một giải pháp khác, ngoài việc sử dụng các khóa rõ ràng, là sử dụng cấu trúc dữ liệu hỗ trợ truy cập đồng thời. Ví dụ, chúng ta có thể sử dụngqueuemô-đun, cung cấp hàng đợi an toàn theo luồng. Chúng tôi cũng có thể sử dụngmultiprocessing.JoinableQueue các lớp cho đồng thời dựa trên đa xử lý.
Truyền dữ liệu bất biến
Đôi khi, cấu trúc dữ liệu mà chúng ta đang sử dụng, chẳng hạn như hàng đợi đồng thời, không phù hợp thì chúng ta có thể chuyển dữ liệu bất biến mà không cần khóa nó.
Truyền dữ liệu có thể thay đổi
Tiếp tục giải pháp trên, giả sử nếu yêu cầu chỉ truyền dữ liệu có thể thay đổi thay vì dữ liệu bất biến, thì chúng ta có thể chuyển dữ liệu có thể thay đổi được chỉ đọc.
Chia sẻ tài nguyên I / O
Một vấn đề quan trọng khác trong việc triển khai các hệ thống đồng thời là việc sử dụng các tài nguyên I / O theo các luồng hoặc quy trình. Sự cố phát sinh khi một luồng hoặc quá trình sử dụng I / O trong một thời gian dài như vậy và luồng khác đang ở chế độ chờ. Chúng ta có thể thấy loại rào cản như vậy khi làm việc với một ứng dụng nặng I / O. Nó có thể được hiểu với sự trợ giúp của một ví dụ, yêu cầu các trang từ trình duyệt web. Nó là một ứng dụng nặng. Ở đây, nếu tốc độ dữ liệu được yêu cầu chậm hơn tốc độ dữ liệu được sử dụng thì chúng ta có rào cản I / O trong hệ thống đồng thời của mình.
Tập lệnh Python sau đây là để yêu cầu một trang web và lấy thời gian mạng của chúng tôi lấy trang được yêu cầu -
import urllib.request
import time
ts = time.time()
req = urllib.request.urlopen('http://www.tutorialspoint.com')
pageHtml = req.read()
te = time.time()
print("Page Fetching Time : {} Seconds".format (te-ts))
Sau khi thực hiện đoạn script trên, chúng ta có thể có được thời gian tìm nạp trang như hình bên dưới.
Đầu ra
Page Fetching Time: 1.0991398811340332 Seconds
Chúng ta có thể thấy rằng thời gian tìm nạp trang là hơn một giây. Bây giờ điều gì xảy ra nếu chúng tôi muốn tìm nạp hàng nghìn trang web khác nhau, bạn có thể hiểu mạng của chúng tôi sẽ mất bao nhiêu thời gian.
Song song là gì?
Song song có thể được định nghĩa là nghệ thuật tách các nhiệm vụ thành các nhiệm vụ con có thể được xử lý đồng thời. Nó đối lập với đồng thời, như đã thảo luận ở trên, trong đó hai hoặc nhiều sự kiện xảy ra cùng một lúc. Chúng ta có thể hiểu nó theo sơ đồ; một nhiệm vụ được chia thành một số nhiệm vụ con có thể được xử lý song song, như sau:
Để có thêm ý tưởng về sự phân biệt giữa đồng thời và song song, hãy xem xét các điểm sau:
Đồng thời nhưng không song song
Một ứng dụng có thể đồng thời nhưng không song song có nghĩa là nó xử lý nhiều hơn một tác vụ cùng một lúc nhưng các tác vụ không được chia nhỏ thành các nhiệm vụ con.
Song song nhưng không đồng thời
Một ứng dụng có thể song song nhưng không đồng thời có nghĩa là nó chỉ hoạt động trên một nhiệm vụ tại một thời điểm và các tác vụ được chia thành các nhiệm vụ con có thể được xử lý song song.
Không song song cũng không đồng thời
Một ứng dụng có thể không song song hoặc đồng thời. Điều này có nghĩa là nó chỉ hoạt động trên một nhiệm vụ tại một thời điểm và nhiệm vụ không bao giờ được chia thành các nhiệm vụ con.
Cả song song và đồng thời
Một ứng dụng có thể vừa song song vừa đồng thời có nghĩa là nó vừa hoạt động trên nhiều tác vụ cùng một lúc và tác vụ được chia thành các nhiệm vụ con để thực hiện chúng song song.
Sự cần thiết của thuyết song song
Chúng ta có thể đạt được tính song song bằng cách phân phối các nhiệm vụ con giữa các lõi khác nhau của một CPU hoặc giữa nhiều máy tính được kết nối trong một mạng.
Hãy xem xét những điểm quan trọng sau để hiểu tại sao cần phải đạt được tính song song -
Thực thi mã hiệu quả
Với sự trợ giúp của song song, chúng tôi có thể chạy mã của mình một cách hiệu quả. Nó sẽ tiết kiệm thời gian của chúng tôi vì cùng một đoạn mã trong các phần đang chạy song song.
Nhanh hơn tính toán tuần tự
Tính toán tuần tự bị hạn chế bởi các yếu tố vật lý và thực tế do đó không thể có kết quả tính toán nhanh hơn. Mặt khác, vấn đề này được giải quyết bằng tính toán song song và cho chúng ta kết quả tính toán nhanh hơn so với tính toán tuần tự.
Thời gian thực hiện ít hơn
Xử lý song song làm giảm thời gian thực thi mã chương trình.
Nếu chúng ta nói về ví dụ thực tế về tính song song, thì card đồ họa của máy tính là ví dụ làm nổi bật sức mạnh thực sự của xử lý song song vì nó có hàng trăm lõi xử lý riêng lẻ hoạt động độc lập và có thể thực hiện cùng một lúc. Vì lý do này, chúng tôi cũng có thể chạy các ứng dụng và trò chơi cao cấp.
Hiểu biết về các bộ xử lý để thực hiện
Chúng ta biết về tính đồng thời, tính song song và sự khác biệt giữa chúng nhưng còn về hệ thống mà nó sẽ được triển khai. Rất cần có sự hiểu biết về hệ thống mà chúng ta sẽ triển khai, bởi vì nó mang lại cho chúng ta lợi ích để đưa ra quyết định sáng suốt trong khi thiết kế phần mềm. Chúng tôi có hai loại bộ xử lý sau:
Bộ xử lý lõi đơn
Bộ xử lý lõi đơn có khả năng thực thi một luồng tại bất kỳ thời điểm nào. Các bộ xử lý này sử dụngcontext switchingđể lưu trữ tất cả các thông tin cần thiết cho một chuỗi tại một thời điểm cụ thể và sau đó khôi phục thông tin sau đó. Cơ chế chuyển đổi ngữ cảnh giúp chúng tôi đạt được tiến bộ trên một số luồng trong một giây nhất định và có vẻ như hệ thống đang làm việc trên nhiều thứ.
Bộ xử lý lõi đơn có nhiều ưu điểm. Các bộ xử lý này yêu cầu ít năng lượng hơn và không có giao thức giao tiếp phức tạp giữa nhiều lõi. Mặt khác, tốc độ của bộ vi xử lý lõi đơn bị hạn chế và nó không thích hợp cho các ứng dụng lớn hơn.
Bộ vi xử lý đa lõi
Bộ xử lý đa lõi có nhiều đơn vị xử lý độc lập còn được gọi là cores.
Các bộ xử lý như vậy không cần cơ chế chuyển đổi ngữ cảnh vì mỗi lõi chứa mọi thứ nó cần để thực hiện một chuỗi các lệnh được lưu trữ.
Chu trình tìm nạp-giải mã-thực thi
Các lõi của bộ xử lý đa lõi tuân theo một chu kỳ để thực thi. Chu kỳ này được gọi làFetch-Decode-Executeđi xe đạp. Nó bao gồm các bước sau:
Tìm nạp
Đây là bước đầu tiên của chu trình, liên quan đến việc tìm nạp các lệnh từ bộ nhớ chương trình.
Giải mã
Các hướng dẫn được tải gần đây sẽ được chuyển đổi thành một loạt các tín hiệu sẽ kích hoạt các bộ phận khác của CPU.
Hành hình
Đây là bước cuối cùng trong đó các lệnh được tìm nạp và giải mã sẽ được thực thi. Kết quả thực thi sẽ được lưu trữ trong thanh ghi CPU.
Một lợi thế ở đây là việc thực thi trong bộ xử lý đa lõi nhanh hơn so với bộ xử lý đơn lõi. Nó phù hợp cho các ứng dụng lớn hơn. Mặt khác, giao thức giao tiếp phức tạp giữa nhiều lõi là một vấn đề. Nhiều lõi yêu cầu nhiều năng lượng hơn bộ xử lý đơn lõi.
Có nhiều kiểu kiến trúc hệ thống và bộ nhớ khác nhau cần được xem xét khi thiết kế chương trình hoặc hệ thống đồng thời. Nó rất cần thiết vì một hệ thống và kiểu bộ nhớ có thể phù hợp với một tác vụ nhưng có thể dễ bị lỗi với tác vụ khác.
Kiến trúc hệ thống máy tính hỗ trợ đồng thời
Michael Flynn năm 1972 đã đưa ra phân loại học để phân loại các kiểu kiến trúc hệ thống máy tính khác nhau. Phân loại này xác định bốn kiểu khác nhau như sau:
- Luồng lệnh đơn, luồng dữ liệu đơn (SISD)
- Luồng lệnh đơn, nhiều luồng dữ liệu (SIMD)
- Nhiều luồng lệnh, một luồng dữ liệu (MISD)
- Nhiều luồng lệnh, nhiều luồng dữ liệu (MIMD).
Luồng lệnh đơn, luồng dữ liệu đơn (SISD)
Như tên cho thấy, loại hệ thống như vậy sẽ có một luồng dữ liệu đến tuần tự và một đơn vị xử lý duy nhất để thực thi luồng dữ liệu. Chúng giống như các hệ thống đơn xử lý có kiến trúc tính toán song song. Sau đây là kiến trúc của SISD -
Ưu điểm của SISD
Những ưu điểm của kiến trúc SISD như sau:
- Nó đòi hỏi ít điện năng hơn.
- Không có vấn đề về giao thức giao tiếp phức tạp giữa nhiều lõi.
Nhược điểm của SISD
Những nhược điểm của kiến trúc SISD như sau:
- Tốc độ của kiến trúc SISD bị giới hạn giống như các bộ xử lý lõi đơn.
- Nó không thích hợp cho các ứng dụng lớn hơn.
Luồng lệnh đơn, nhiều luồng dữ liệu (SIMD)
Như tên cho thấy, loại hệ thống như vậy sẽ có nhiều luồng dữ liệu đến và số lượng đơn vị xử lý có thể hoạt động trên một lệnh duy nhất tại bất kỳ thời điểm nào. Chúng giống như các hệ thống đa xử lý có kiến trúc tính toán song song. Sau đây là kiến trúc của SIMD -
Ví dụ tốt nhất cho SIMD là các card đồ họa. Các thẻ này có hàng trăm đơn vị xử lý riêng lẻ. Nếu chúng ta nói về sự khác biệt tính toán giữa SISD và SIMD thì đối với mảng thêm[5, 15, 20] và [15, 25, 10],Kiến trúc SISD sẽ phải thực hiện ba thao tác thêm khác nhau. Mặt khác, với kiến trúc SIMD, chúng ta có thể thêm thì chỉ trong một thao tác thêm.
Ưu điểm của SIMD
Những ưu điểm của kiến trúc SIMD như sau:
Chỉ có thể thực hiện cùng một thao tác trên nhiều phần tử bằng một lệnh.
Thông lượng của hệ thống có thể được tăng lên bằng cách tăng số lõi của bộ xử lý.
Tốc độ xử lý cao hơn kiến trúc SISD.
Nhược điểm của SIMD
Những nhược điểm của kiến trúc SIMD như sau:
- Có sự giao tiếp phức tạp giữa số lượng lõi của bộ xử lý.
- Chi phí cao hơn kiến trúc SISD.
Luồng dữ liệu đơn nhiều lệnh (MISD)
Hệ thống với luồng MISD có số lượng đơn vị xử lý thực hiện các hoạt động khác nhau bằng cách thực hiện các lệnh khác nhau trên cùng một tập dữ liệu. Sau đây là kiến trúc của MISD -
Các đại diện của kiến trúc MISD chưa tồn tại về mặt thương mại.
Luồng nhiều hướng dẫn nhiều dữ liệu (MIMD)
Trong hệ thống sử dụng kiến trúc MIMD, mỗi bộ xử lý trong hệ thống đa xử lý có thể thực hiện song song các tập lệnh khác nhau trên tập dữ liệu khác nhau. Nó đối lập với kiến trúc SIMD trong đó hoạt động đơn lẻ được thực hiện trên nhiều tập dữ liệu. Sau đây là kiến trúc của MIMD -
Một bộ đa xử lý thông thường sử dụng kiến trúc MIMD. Các kiến trúc này về cơ bản được sử dụng trong một số lĩnh vực ứng dụng như thiết kế có sự hỗ trợ của máy tính / sản xuất có sự hỗ trợ của máy tính, mô phỏng, mô hình hóa, chuyển mạch truyền thông, v.v.
Kiến trúc bộ nhớ hỗ trợ đồng thời
Trong khi làm việc với các khái niệm như đồng thời và song song, luôn cần phải tăng tốc các chương trình. Một giải pháp được các nhà thiết kế máy tính tìm ra là tạo ra nhiều máy tính dùng chung bộ nhớ, tức là các máy tính có không gian địa chỉ vật lý duy nhất, được truy cập bởi tất cả các lõi mà bộ xử lý đang có. Trong trường hợp này, có thể có một số phong cách kiến trúc khác nhau nhưng sau đây là ba phong cách kiến trúc quan trọng:
UMA (Truy cập bộ nhớ đồng nhất)
Trong mô hình này, tất cả các bộ xử lý chia sẻ bộ nhớ vật lý một cách đồng nhất. Tất cả các bộ xử lý có thời gian truy cập bằng nhau đến tất cả các từ bộ nhớ. Mỗi bộ xử lý có thể có một bộ nhớ đệm riêng. Các thiết bị ngoại vi tuân theo một bộ quy tắc.
Khi tất cả các bộ xử lý có quyền truy cập như nhau vào tất cả các thiết bị ngoại vi, hệ thống được gọi là symmetric multiprocessor. Khi chỉ một hoặc một vài bộ xử lý có thể truy cập vào các thiết bị ngoại vi, hệ thống được gọi làasymmetric multiprocessor.
Quyền truy cập bộ nhớ không đồng nhất (NUMA)
Trong mô hình đa xử lý NUMA, thời gian truy cập thay đổi theo vị trí của từ bộ nhớ. Tại đây, bộ nhớ dùng chung được phân phối vật lý giữa tất cả các bộ xử lý, được gọi là bộ nhớ cục bộ. Tập hợp tất cả các bộ nhớ cục bộ tạo thành một không gian địa chỉ chung mà tất cả các bộ xử lý có thể truy cập.
Kiến trúc bộ nhớ chỉ bộ nhớ đệm (COMA)
Mô hình COMA là một phiên bản chuyên biệt của mô hình NUMA. Tại đây, tất cả các bộ nhớ chính được phân phối được chuyển đổi thành bộ nhớ đệm.
Nói chung, như chúng ta biết rằng chỉ là một sợi dây xoắn rất mảnh thường bằng vải bông hoặc vải lụa và được sử dụng để may quần áo và những thứ như vậy. Cụm từ tương tự cũng được sử dụng trong thế giới lập trình máy tính. Bây giờ, chúng ta liên hệ sợi chỉ dùng để may quần áo và sợi chỉ dùng để lập trình máy tính như thế nào? Các vai trò được thực hiện bởi hai luồng tương tự ở đây. Trong quần áo, chỉ giữ vải với nhau và ở mặt khác, trong lập trình máy tính, chỉ giữ chương trình máy tính và cho phép chương trình thực hiện các hành động tuần tự hoặc nhiều hành động cùng một lúc.
Threadlà đơn vị thực thi nhỏ nhất trong hệ điều hành. Bản thân nó không phải là một chương trình mà chạy trong một chương trình. Nói cách khác, các luồng không độc lập với nhau và chia sẻ phần mã, phần dữ liệu, v.v. với các luồng khác. Các luồng này còn được gọi là các quy trình nhẹ.
Kỳ của chủ đề
Để hiểu sâu về chức năng của các luồng, chúng ta cần tìm hiểu về vòng đời của các luồng hoặc các trạng thái khác nhau của luồng. Thông thường, một chuỗi có thể tồn tại ở năm trạng thái riêng biệt. Các trạng thái khác nhau được hiển thị bên dưới -
Chủ đề mới
Một luồng mới bắt đầu vòng đời của nó ở trạng thái mới. Tuy nhiên, ở giai đoạn này, nó vẫn chưa bắt đầu và nó chưa được phân bổ bất kỳ tài nguyên nào. Chúng ta có thể nói rằng nó chỉ là một thể hiện của một đối tượng.
Runnable
Khi luồng mới sinh được bắt đầu, luồng trở nên có thể chạy được tức là đang chờ chạy. Ở trạng thái này, nó có tất cả các tài nguyên nhưng trình lập lịch tác vụ vẫn chưa lập lịch để nó chạy.
Đang chạy
Ở trạng thái này, luồng tạo tiến trình và thực thi tác vụ, tác vụ đã được bộ lập lịch tác vụ chọn để chạy. Bây giờ, luồng có thể chuyển sang trạng thái chết hoặc trạng thái không thể chạy / chờ.
Không chạy / đang chờ
Trong trạng thái này, luồng bị tạm dừng vì nó đang chờ phản hồi của một số yêu cầu I / O hoặc chờ hoàn thành việc thực thi luồng khác.
Đã chết
Một luồng có thể chạy đi vào trạng thái kết thúc khi nó hoàn thành nhiệm vụ của mình hoặc kết thúc bằng cách khác.
Sơ đồ sau đây cho thấy toàn bộ vòng đời của một luồng:
Các loại chủ đề
Trong phần này, chúng ta sẽ thấy các loại ren khác nhau. Các loại được mô tả bên dưới -
Chủ đề cấp độ người dùng
Đây là những chủ đề do người dùng quản lý.
Trong trường hợp này, nhân quản lý luồng không nhận thức được sự tồn tại của các luồng. Thư viện luồng chứa mã để tạo và hủy luồng, để truyền thông điệp và dữ liệu giữa các luồng, để lập lịch thực thi luồng và lưu và khôi phục ngữ cảnh luồng. Ứng dụng bắt đầu với một luồng duy nhất.
Các ví dụ về chủ đề cấp người dùng là:
- Chủ đề Java
- Chuỗi POSIX
Ưu điểm của Chủ đề cấp độ người dùng
Sau đây là những ưu điểm khác nhau của luồng cấp người dùng:
- Chuyển đổi luồng không yêu cầu đặc quyền của chế độ Kernel.
- Chủ đề cấp người dùng có thể chạy trên bất kỳ hệ điều hành nào.
- Lập lịch có thể là ứng dụng cụ thể trong luồng cấp người dùng.
- Chủ đề cấp độ người dùng được tạo và quản lý nhanh chóng.
Nhược điểm của Chủ đề cấp độ người dùng
Sau đây là những nhược điểm khác nhau của các luồng cấp người dùng:
- Trong một hệ điều hành điển hình, hầu hết các cuộc gọi hệ thống đều bị chặn.
- Ứng dụng đa luồng không thể tận dụng đa xử lý.
Chủ đề cấp nhân
Các luồng do hệ điều hành quản lý hoạt động trên kernel, là lõi của hệ điều hành.
Trong trường hợp này, Kernel thực hiện quản lý luồng. Không có mã quản lý luồng trong khu vực ứng dụng. Các luồng nhân được hỗ trợ trực tiếp bởi hệ điều hành. Bất kỳ ứng dụng nào cũng có thể được lập trình để chạy đa luồng. Tất cả các chuỗi trong một ứng dụng được hỗ trợ trong một quy trình duy nhất.
Kernel duy trì thông tin ngữ cảnh cho toàn bộ quá trình và cho các luồng riêng lẻ trong quá trình. Lập lịch bởi Kernel được thực hiện trên cơ sở luồng. Kernel thực hiện việc tạo, lập lịch và quản lý luồng trong không gian Kernel. Các luồng nhân thường chậm tạo và quản lý hơn các luồng người dùng. Các ví dụ về luồng cấp nhân là Windows, Solaris.
Ưu điểm của luồng cấp nhân
Sau đây là những ưu điểm khác nhau của luồng cấp nhân:
Kernel có thể lên lịch đồng thời cho nhiều luồng từ cùng một tiến trình trên nhiều tiến trình.
Nếu một luồng trong một tiến trình bị chặn, Kernel có thể lên lịch cho một luồng khác của cùng một tiến trình.
Bản thân các quy trình hạt nhân có thể được đa luồng.
Nhược điểm của luồng cấp nhân
Các luồng nhân thường chậm tạo và quản lý hơn các luồng người dùng.
Việc chuyển quyền điều khiển từ luồng này sang luồng khác trong cùng một quy trình yêu cầu chuyển chế độ sang Kernel.
Khối điều khiển luồng - TCB
Khối điều khiển luồng (TCB) có thể được định nghĩa là cấu trúc dữ liệu trong nhân của hệ điều hành chủ yếu chứa thông tin về luồng. Thông tin cụ thể về chuỗi được lưu trữ trong TCB sẽ nêu bật một số thông tin quan trọng về mỗi quy trình.
Hãy xem xét các điểm sau liên quan đến các chủ đề có trong TCB:
Thread identification - Nó là id luồng duy nhất (tid) được gán cho mọi luồng mới.
Thread state - Nó chứa các thông tin liên quan đến trạng thái (Running, Runnable, Non-Running, Dead) của luồng.
Program Counter (PC) - Nó trỏ đến lệnh chương trình hiện tại của luồng.
Register set - Nó chứa các giá trị thanh ghi của luồng được gán cho chúng để tính toán.
Stack Pointer- Nó trỏ đến ngăn xếp của luồng trong tiến trình. Nó chứa các biến cục bộ trong phạm vi của luồng.
Pointer to PCB - Nó chứa con trỏ tới tiến trình đã tạo ra luồng đó.
Mối quan hệ giữa quy trình và luồng
Trong đa luồng, quy trình và luồng là hai thuật ngữ có liên quan chặt chẽ với nhau, có cùng mục tiêu là giúp máy tính có thể làm nhiều việc cùng một lúc. Một tiến trình có thể chứa một hoặc nhiều luồng nhưng ngược lại, luồng không thể chứa một tiến trình. Tuy nhiên, cả hai vẫn là hai đơn vị thực thi cơ bản. Một chương trình, thực hiện một loạt các lệnh, bắt đầu quá trình và phân luồng cả hai.
Bảng sau đây cho thấy sự so sánh giữa quy trình và luồng:
Quá trình | Chủ đề |
---|---|
Quy trình có trọng lượng lớn hoặc sử dụng nhiều tài nguyên. | Thread nhẹ, chiếm ít tài nguyên hơn một quy trình. |
Quá trình chuyển đổi cần tương tác với hệ điều hành. | Chuyển đổi luồng không cần phải tương tác với hệ điều hành. |
Trong nhiều môi trường xử lý, mỗi tiến trình thực thi cùng một đoạn mã nhưng có bộ nhớ và tài nguyên tệp riêng. | Tất cả các luồng có thể chia sẻ cùng một tập hợp các tệp đang mở, các quy trình con. |
Nếu một quá trình bị chặn, thì không quá trình nào khác có thể thực thi cho đến khi quá trình đầu tiên được bỏ chặn. | Trong khi một luồng bị chặn và đang chờ, một luồng thứ hai trong cùng một tác vụ có thể chạy. |
Nhiều quy trình mà không sử dụng luồng sử dụng nhiều tài nguyên hơn. | Nhiều quy trình phân luồng sử dụng ít tài nguyên hơn. |
Trong nhiều quy trình, mỗi quy trình hoạt động độc lập với các quy trình khác. | Một luồng có thể đọc, ghi hoặc thay đổi dữ liệu của luồng khác. |
Nếu có bất kỳ thay đổi nào trong quy trình mẹ thì nó không ảnh hưởng đến các quy trình con. | Nếu có bất kỳ thay đổi nào trong luồng chính thì nó có thể ảnh hưởng đến hoạt động của các luồng khác trong quá trình đó. |
Để giao tiếp với các quy trình anh em, các quy trình phải sử dụng giao tiếp giữa các quy trình. | Các luồng có thể giao tiếp trực tiếp với các luồng khác của quá trình đó. |
Khái niệm về đa luồng
Như chúng ta đã thảo luận trước đó rằng Đa luồng là khả năng của CPU quản lý việc sử dụng hệ điều hành bằng cách thực thi nhiều luồng đồng thời. Ý tưởng chính của đa luồng là đạt được sự song song bằng cách chia một quy trình thành nhiều luồng. Nói một cách đơn giản hơn, chúng ta có thể nói rằng đa luồng là cách để đạt được đa nhiệm bằng cách sử dụng khái niệm luồng.
Có thể hiểu khái niệm đa luồng với sự trợ giúp của ví dụ sau.
Thí dụ
Giả sử chúng ta đang chạy một tiến trình. Quá trình này có thể là để mở MS word để viết một cái gì đó. Trong quá trình này, một luồng sẽ được chỉ định để mở MS word và một luồng khác sẽ được yêu cầu để viết. Bây giờ, giả sử nếu chúng ta muốn chỉnh sửa một cái gì đó thì một luồng khác sẽ được yêu cầu để thực hiện tác vụ chỉnh sửa, v.v.
Sơ đồ sau đây giúp chúng ta hiểu cách nhiều luồng tồn tại trong bộ nhớ -
Chúng ta có thể thấy trong sơ đồ trên rằng nhiều luồng có thể tồn tại trong một tiến trình mà mỗi luồng chứa tập đăng ký và các biến cục bộ của riêng nó. Ngoài ra, tất cả các luồng trong một quy trình đều chia sẻ các biến toàn cục.
Ưu điểm của Đa luồng
Bây giờ chúng ta hãy xem một vài lợi thế của đa luồng. Những ưu điểm như sau:
Speed of communication - Đa luồng cải thiện tốc độ tính toán vì mỗi lõi hoặc bộ xử lý xử lý đồng thời các luồng riêng biệt.
Program remains responsive - Nó cho phép một chương trình vẫn đáp ứng vì một luồng đợi đầu vào và luồng khác chạy GUI cùng một lúc.
Access to global variables - Trong đa luồng, tất cả các luồng của một tiến trình cụ thể có thể truy cập các biến toàn cục và nếu có bất kỳ thay đổi nào trong biến toàn cục thì nó cũng hiển thị cho các luồng khác.
Utilization of resources - Chạy một số luồng trong mỗi chương trình giúp sử dụng CPU tốt hơn và thời gian nhàn rỗi của CPU trở nên ít hơn.
Sharing of data - Không yêu cầu thêm dung lượng cho mỗi luồng vì các luồng trong chương trình có thể chia sẻ cùng một dữ liệu.
Nhược điểm của Đa luồng
Bây giờ chúng ta hãy xem một vài nhược điểm của đa luồng. Những bất lợi như sau:
Not suitable for single processor system - Đa luồng khó đạt được hiệu suất về tốc độ tính toán trên hệ thống một bộ xử lý so với hiệu suất trên hệ thống nhiều bộ xử lý.
Issue of security - Như chúng ta biết rằng tất cả các luồng trong một chương trình chia sẻ cùng một dữ liệu, do đó luôn có vấn đề về bảo mật vì bất kỳ luồng không xác định nào cũng có thể thay đổi dữ liệu.
Increase in complexity - Đa luồng có thể làm tăng độ phức tạp của chương trình và việc gỡ lỗi trở nên khó khăn.
Lead to deadlock state - Đa luồng có thể dẫn chương trình đến nguy cơ tiềm ẩn đạt được trạng thái bế tắc.
Synchronization required- Cần có sự đồng bộ để tránh loại trừ lẫn nhau. Điều này dẫn đến việc sử dụng nhiều bộ nhớ và CPU hơn.
Trong chương này, chúng ta sẽ học cách triển khai các luồng trong Python.
Mô-đun Python để triển khai chuỗi
Các luồng trong Python đôi khi được gọi là các quy trình nhẹ vì các luồng chiếm ít bộ nhớ hơn nhiều so với các quy trình. Chủ đề cho phép thực hiện nhiều tác vụ cùng một lúc. Trong Python, chúng ta có hai mô-đun sau đây triển khai các luồng trong một chương trình:
<_thread>module
<threading>module
Sự khác biệt chính giữa hai mô-đun này là <_thread> mô-đun coi một luồng như một hàm trong khi, <threading>mô-đun coi mọi luồng như một đối tượng và thực hiện nó theo cách hướng đối tượng. Hơn nữa,<_thread>mô-đun hiệu quả trong phân luồng cấp thấp và có ít khả năng hơn <threading> mô-đun.
mô-đun <_thread>
Trong phiên bản Python trước đó, chúng tôi đã có <thread>nhưng nó đã được coi là "không được chấp nhận" trong một thời gian khá dài. Người dùng đã được khuyến khích sử dụng<threading>mô-đun thay thế. Do đó, trong Python 3, "luồng" mô-đun không còn khả dụng nữa. Nó đã được đổi tên thành "<_thread>"cho sự không tương thích ngược trong Python3.
Để tạo chuỗi mới với sự trợ giúp của <_thread> mô-đun, chúng ta cần gọi start_new_threadphương pháp của nó. Hoạt động của phương pháp này có thể được hiểu với sự trợ giúp của cú pháp sau:
_thread.start_new_thread ( function, args[, kwargs] )
Đây -
args là một loạt các đối số
kwargs là một từ điển tùy chọn của các đối số từ khóa
Nếu chúng ta muốn gọi hàm mà không truyền đối số thì chúng ta cần sử dụng một bộ đối số trống trong args.
Lệnh gọi phương thức này trả về ngay lập tức, luồng con bắt đầu và gọi hàm với danh sách args đã truyền, nếu có, nếu có. Luồng kết thúc khi và khi hàm trả về.
Thí dụ
Sau đây là một ví dụ để tạo chuỗi mới bằng cách sử dụng <_thread>mô-đun. Chúng tôi đang sử dụng phương thức start_new_thread () ở đây.
import _thread
import time
def print_time( threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print ("%s: %s" % ( threadName, time.ctime(time.time()) ))
try:
_thread.start_new_thread( print_time, ("Thread-1", 2, ) )
_thread.start_new_thread( print_time, ("Thread-2", 4, ) )
except:
print ("Error: unable to start thread")
while 1:
pass
Đầu ra
Kết quả sau đây sẽ giúp chúng ta hiểu được thế hệ của các luồng mới với sự trợ giúp của <_thread> mô-đun.
Thread-1: Mon Apr 23 10:03:33 2018
Thread-2: Mon Apr 23 10:03:35 2018
Thread-1: Mon Apr 23 10:03:35 2018
Thread-1: Mon Apr 23 10:03:37 2018
Thread-2: Mon Apr 23 10:03:39 2018
Thread-1: Mon Apr 23 10:03:39 2018
Thread-1: Mon Apr 23 10:03:41 2018
Thread-2: Mon Apr 23 10:03:43 2018
Thread-2: Mon Apr 23 10:03:47 2018
Thread-2: Mon Apr 23 10:03:51 2018
mô-đun <threading>
Các <threading>mô-đun thực thi theo cách hướng đối tượng và coi mọi luồng như một đối tượng. Do đó, nó cung cấp hỗ trợ cấp cao, mạnh mẽ hơn cho các luồng so với mô-đun <_thread>. Mô-đun này được bao gồm với Python 2.4.
Các phương thức bổ sung trong mô-đun <threading>
Các <threading> mô-đun bao gồm tất cả các phương thức của <_thread>nhưng nó cũng cung cấp các phương pháp bổ sung. Các phương pháp bổ sung như sau:
threading.activeCount() - Phương thức này trả về số lượng đối tượng luồng đang hoạt động
threading.currentThread() - Phương thức này trả về số đối tượng luồng trong điều khiển luồng của người gọi.
threading.enumerate() - Phương thức này trả về danh sách tất cả các đối tượng luồng hiện đang hoạt động.
run() - Phương thức run () là điểm vào của một luồng.
start() - Phương thức start () bắt đầu một luồng bằng cách gọi phương thức run.
join([time]) - Tham gia () đợi luồng kết thúc.
isAlive() - Phương thức isAlive () kiểm tra xem một luồng có còn đang thực thi hay không.
getName() - Phương thức getName () trả về tên của một luồng.
setName() - Phương thức setName () đặt tên của một luồng.
Để triển khai luồng, <threading> mô-đun có Thread lớp cung cấp các phương thức sau:
Làm cách nào để tạo luồng bằng mô-đun <threading>?
Trong phần này, chúng ta sẽ học cách tạo chuỗi bằng cách sử dụng <threading>mô-đun. Làm theo các bước sau để tạo một chuỗi mới bằng mô-đun <threading> -
Step 1 - Trong bước này, chúng ta cần xác định một lớp con mới của Thread lớp học.
Step 2 - Sau đó, để thêm các đối số bổ sung, chúng ta cần ghi đè __init__(self [,args]) phương pháp.
Step 3 - Ở bước này, chúng ta cần ghi đè phương thức run (self [, args]) để thực thi những gì mà luồng sẽ làm khi bắt đầu.
Bây giờ, sau khi tạo mới Thread lớp con, chúng ta có thể tạo một thể hiện của nó và sau đó bắt đầu một luồng mới bằng cách gọi start(), lần lượt gọi là run() phương pháp.
Thí dụ
Hãy xem xét ví dụ này để tìm hiểu cách tạo một chuỗi mới bằng cách sử dụng <threading> mô-đun.
import threading
import time
exitFlag = 0
class myThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print ("Starting " + self.name)
print_time(self.name, self.counter, 5)
print ("Exiting " + self.name)
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
threadName.exit()
time.sleep(delay)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("Exiting Main Thread")
Starting Thread-1
Starting Thread-2
Đầu ra
Bây giờ, hãy xem xét kết quả sau:
Thread-1: Mon Apr 23 10:52:09 2018
Thread-1: Mon Apr 23 10:52:10 2018
Thread-2: Mon Apr 23 10:52:10 2018
Thread-1: Mon Apr 23 10:52:11 2018
Thread-1: Mon Apr 23 10:52:12 2018
Thread-2: Mon Apr 23 10:52:12 2018
Thread-1: Mon Apr 23 10:52:13 2018
Exiting Thread-1
Thread-2: Mon Apr 23 10:52:14 2018
Thread-2: Mon Apr 23 10:52:16 2018
Thread-2: Mon Apr 23 10:52:18 2018
Exiting Thread-2
Exiting Main Thread
Chương trình Python cho các trạng thái luồng khác nhau
Có năm trạng thái luồng - mới, chạy được, đang chạy, đang chờ và đã chết. Trong số năm Trong số năm trạng thái này, chúng tôi sẽ chủ yếu tập trung vào ba trạng thái - đang chạy, chờ đợi và chết. Một luồng nhận tài nguyên của nó ở trạng thái đang chạy, đợi tài nguyên ở trạng thái chờ; bản phát hành cuối cùng của tài nguyên, nếu việc thực thi và thu được ở trạng thái chết.
Chương trình Python sau đây với sự trợ giúp của các phương thức start (), sleep () và join () sẽ hiển thị cách một luồng được nhập vào trạng thái chạy, chờ và chết tương ứng.
Step 1 - Nhập các mô-đun cần thiết, <threading> và <time>
import threading
import time
Step 2 - Định nghĩa một hàm sẽ được gọi trong khi tạo một luồng.
def thread_states():
print("Thread entered in running state")
Step 3 - Chúng tôi đang sử dụng phương thức sleep () của mô-đun thời gian để làm cho chuỗi của chúng tôi đợi trong 2 giây.
time.sleep(2)
Step 4 - Bây giờ, chúng ta đang tạo một luồng có tên là T1, luồng này lấy đối số của hàm đã định nghĩa ở trên.
T1 = threading.Thread(target=thread_states)
Step 5- Bây giờ, với sự trợ giúp của hàm start (), chúng ta có thể bắt đầu luồng của mình. Nó sẽ tạo ra thông báo, được thiết lập bởi chúng tôi trong khi xác định chức năng.
T1.start()
Thread entered in running state
Step 6 - Bây giờ, cuối cùng chúng ta có thể giết luồng bằng phương thức join () sau khi nó kết thúc quá trình thực thi.
T1.join()
Bắt đầu một chuỗi bằng Python
Trong python, chúng ta có thể bắt đầu một luồng mới bằng nhiều cách khác nhau nhưng cách dễ nhất trong số đó là xác định nó như một hàm duy nhất. Sau khi xác định hàm, chúng ta có thể chuyển nó làm mục tiêu cho mộtthreading.Threadđối tượng và như vậy. Thực thi mã Python sau để hiểu cách hoạt động của hàm:
import threading
import time
import random
def Thread_execution(i):
print("Execution of Thread {} started\n".format(i))
sleepTime = random.randint(1,4)
time.sleep(sleepTime)
print("Execution of Thread {} finished".format(i))
for i in range(4):
thread = threading.Thread(target=Thread_execution, args=(i,))
thread.start()
print("Active Threads:" , threading.enumerate())
Đầu ra
Execution of Thread 0 started
Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>]
Execution of Thread 1 started
Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>,
<Thread(Thread-3577, started 3080)>]
Execution of Thread 2 started
Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>,
<Thread(Thread-3577, started 3080)>,
<Thread(Thread-3578, started 2268)>]
Execution of Thread 3 started
Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>,
<Thread(Thread-3577, started 3080)>,
<Thread(Thread-3578, started 2268)>,
<Thread(Thread-3579, started 4520)>]
Execution of Thread 0 finished
Execution of Thread 1 finished
Execution of Thread 2 finished
Execution of Thread 3 finished
Chuỗi Daemon trong Python
Trước khi triển khai các luồng daemon trong Python, chúng ta cần biết về các luồng daemon và cách sử dụng chúng. Về mặt máy tính, daemon là một tiến trình nền xử lý các yêu cầu cho các dịch vụ khác nhau như gửi dữ liệu, truyền tệp, v.v. Nó sẽ không hoạt động nếu nó không được yêu cầu nữa. Tác vụ tương tự cũng có thể được thực hiện với sự trợ giúp của các luồng không phải daemon. Tuy nhiên, trong trường hợp này, luồng chính phải theo dõi các luồng không phải daemon theo cách thủ công. Mặt khác, nếu chúng ta đang sử dụng các luồng daemon thì luồng chính hoàn toàn có thể quên điều này và nó sẽ bị giết khi luồng chính thoát ra. Một điểm quan trọng khác về các luồng daemon là chúng ta có thể chọn chỉ sử dụng chúng cho các tác vụ không thiết yếu sẽ không ảnh hưởng đến chúng ta nếu nó không hoàn thành hoặc bị chết giữa chừng. Sau đây là cách triển khai các luồng daemon trong python:
import threading
import time
def nondaemonThread():
print("starting my thread")
time.sleep(8)
print("ending my thread")
def daemonThread():
while True:
print("Hello")
time.sleep(2)
if __name__ == '__main__':
nondaemonThread = threading.Thread(target = nondaemonThread)
daemonThread = threading.Thread(target = daemonThread)
daemonThread.setDaemon(True)
daemonThread.start()
nondaemonThread.start()
Trong đoạn mã trên, có hai chức năng cụ thể là >nondaemonThread() và >daemonThread(). Hàm đầu tiên in trạng thái của nó và ngủ sau 8 giây trong khi hàm deamonThread () in Hello sau mỗi 2 giây vô thời hạn. Chúng ta có thể hiểu sự khác biệt giữa các luồng nondaemon và daemon với sự trợ giúp của đầu ra sau:
Hello
starting my thread
Hello
Hello
Hello
Hello
ending my thread
Hello
Hello
Hello
Hello
Hello
Đồng bộ hóa luồng có thể được định nghĩa là một phương pháp với sự trợ giúp của chúng ta có thể yên tâm rằng hai hoặc nhiều luồng đồng thời không truy cập đồng thời vào phân đoạn chương trình được gọi là phần quan trọng. Mặt khác, như chúng ta biết rằng phần quan trọng là phần của chương trình nơi tài nguyên được chia sẻ được truy cập. Do đó, chúng ta có thể nói rằng đồng bộ hóa là quá trình đảm bảo rằng hai hoặc nhiều luồng không giao tiếp với nhau bằng cách truy cập các tài nguyên cùng một lúc. Biểu đồ dưới đây cho thấy bốn luồng đang cố gắng truy cập vào phần quan trọng của một chương trình cùng một lúc.
Để làm cho nó rõ ràng hơn, giả sử hai hoặc nhiều luồng cố gắng thêm đối tượng vào danh sách cùng một lúc. Hành động này không thể dẫn đến một kết thúc thành công vì nó sẽ làm rơi một hoặc tất cả các đối tượng hoặc nó sẽ làm hỏng hoàn toàn trạng thái của danh sách. Ở đây, vai trò của đồng bộ hóa là chỉ một luồng tại một thời điểm có thể truy cập vào danh sách.
Sự cố trong đồng bộ hóa chuỗi
Chúng tôi có thể gặp sự cố khi triển khai lập trình đồng thời hoặc áp dụng đồng bộ hóa nguyên thủy. Trong phần này, chúng ta sẽ thảo luận về hai vấn đề lớn. Các vấn đề là -
- Deadlock
- Điều kiện của cuộc đua
Điều kiện của cuộc đua
Đây là một trong những vấn đề lớn trong lập trình đồng thời. Truy cập đồng thời vào các tài nguyên được chia sẻ có thể dẫn đến tình trạng chủng tộc. Điều kiện chủng tộc có thể được định nghĩa là sự xuất hiện của một điều kiện khi hai hoặc nhiều luồng có thể truy cập dữ liệu được chia sẻ và sau đó cố gắng thay đổi giá trị của nó cùng một lúc. Do đó, giá trị của các biến có thể không thể đoán trước và thay đổi tùy thuộc vào thời gian chuyển đổi ngữ cảnh của các quy trình.
Thí dụ
Hãy xem xét ví dụ này để hiểu khái niệm về tình trạng chủng tộc -
Step 1 - Trong bước này, chúng ta cần nhập mô-đun luồng -
import threading
Step 2 - Bây giờ, hãy xác định một biến toàn cục, chẳng hạn như x, cùng với giá trị của nó là 0 -
x = 0
Step 3 - Bây giờ, chúng ta cần xác định increment_global() hàm, sẽ thực hiện tăng 1 trong hàm tổng thể x -
def increment_global():
global x
x += 1
Step 4 - Trong bước này, chúng tôi sẽ xác định taskofThread()hàm, sẽ gọi hàm increment_global () trong một số lần cụ thể; ví dụ của chúng tôi là 50000 lần -
def taskofThread():
for _ in range(50000):
increment_global()
Step 5- Bây giờ, hãy xác định hàm main () trong đó các luồng t1 và t2 được tạo. Cả hai sẽ được bắt đầu với sự trợ giúp của hàm start () và đợi cho đến khi chúng hoàn thành công việc của mình với sự trợ giúp của hàm join ().
def main():
global x
x = 0
t1 = threading.Thread(target= taskofThread)
t2 = threading.Thread(target= taskofThread)
t1.start()
t2.start()
t1.join()
t2.join()
Step 6- Bây giờ, chúng ta cần cung cấp phạm vi như trong bao nhiêu lần lặp chúng ta muốn gọi hàm main (). Ở đây, chúng tôi đang gọi nó trong 5 lần.
if __name__ == "__main__":
for i in range(5):
main()
print("x = {1} after Iteration {0}".format(i,x))
Trong kết quả hiển thị bên dưới, chúng ta có thể thấy ảnh hưởng của điều kiện chủng tộc vì giá trị của x sau mỗi lần lặp được mong đợi là 100000. Tuy nhiên, có rất nhiều sự thay đổi trong giá trị. Điều này là do sự truy cập đồng thời của các luồng tới biến chung x.
Đầu ra
x = 100000 after Iteration 0
x = 54034 after Iteration 1
x = 80230 after Iteration 2
x = 93602 after Iteration 3
x = 93289 after Iteration 4
Đối phó với tình trạng cuộc đua bằng cách sử dụng khóa
Như chúng ta đã thấy ảnh hưởng của điều kiện chủng tộc trong chương trình trên, chúng ta cần một công cụ đồng bộ hóa, có thể xử lý tình trạng chủng tộc giữa nhiều luồng. Trong Python,<threading>mô-đun cung cấp lớp Khóa để đối phó với tình trạng chủng tộc. Hơn nữa,Locklớp cung cấp các phương thức khác nhau với sự trợ giúp của chúng ta có thể xử lý tình trạng chủng tộc giữa nhiều luồng. Các phương pháp được mô tả dưới đây:
phương thức get ()
Phương pháp này được sử dụng để lấy, tức là chặn một khóa. Khóa có thể chặn hoặc không chặn tùy thuộc vào giá trị đúng hoặc sai sau:
With value set to True - Nếu phương thức get () được gọi với True, là đối số mặc định, thì việc thực thi luồng sẽ bị chặn cho đến khi khóa được mở khóa.
With value set to False - Nếu phương thức get () được gọi với False, không phải là đối số mặc định, thì việc thực thi luồng sẽ không bị chặn cho đến khi nó được đặt thành true, tức là cho đến khi nó bị khóa.
phương thức release ()
Phương pháp này được sử dụng để phát hành một khóa. Sau đây là một số nhiệm vụ quan trọng liên quan đến phương pháp này:
Nếu khóa bị khóa, thì release()phương pháp sẽ mở khóa nó. Công việc của nó là cho phép chính xác một luồng tiến hành nếu nhiều luồng bị chặn và chờ khóa được mở khóa.
Nó sẽ nâng cao một ThreadError nếu khóa đã được mở khóa.
Bây giờ, chúng ta có thể viết lại chương trình trên với lớp khóa và các phương thức của nó để tránh điều kiện đua. Chúng ta cần xác định phương thức taskofThread () với đối số lock và sau đó cần sử dụng các phương thức get () và release () để chặn và không chặn các khóa để tránh tình trạng đua.
Thí dụ
Sau đây là ví dụ về chương trình python để hiểu khái niệm khóa để xử lý điều kiện chủng tộc -
import threading
x = 0
def increment_global():
global x
x += 1
def taskofThread(lock):
for _ in range(50000):
lock.acquire()
increment_global()
lock.release()
def main():
global x
x = 0
lock = threading.Lock()
t1 = threading.Thread(target = taskofThread, args = (lock,))
t2 = threading.Thread(target = taskofThread, args = (lock,))
t1.start()
t2.start()
t1.join()
t2.join()
if __name__ == "__main__":
for i in range(5):
main()
print("x = {1} after Iteration {0}".format(i,x))
Kết quả sau đây cho thấy ảnh hưởng của điều kiện chủng tộc bị bỏ qua; vì giá trị của x, sau mỗi & mỗi lần lặp, bây giờ là 100000, đúng như kỳ vọng của chương trình này.
Đầu ra
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4
Bế tắc - Vấn đề của các triết gia ăn uống
Bế tắc là một vấn đề rắc rối mà người ta có thể gặp phải khi thiết kế các hệ thống đồng thời. Chúng ta có thể minh họa vấn đề này với sự trợ giúp của vấn đề triết học ăn uống như sau:
Edsger Dijkstra ban đầu đã giới thiệu vấn đề triết học ăn uống, một trong những minh họa nổi tiếng về một trong những vấn đề lớn nhất của hệ thống đồng thời được gọi là bế tắc.
Trong bài toán này, có năm triết gia nổi tiếng đang ngồi ở một bàn tròn ăn một số thức ăn từ bát của họ. Năm triết gia có thể dùng nĩa để ăn thức ăn của họ. Tuy nhiên, các triết gia quyết định sử dụng hai nĩa cùng một lúc để ăn thức ăn của họ.
Bây giờ, có hai điều kiện chính cho các triết gia. Thứ nhất, mỗi triết gia có thể đang ăn hoặc đang trong trạng thái suy nghĩ và thứ hai, trước tiên họ phải có được cả hai cái nĩa, tức là trái và phải. Vấn đề nảy sinh khi mỗi người trong số năm nhà triết học chọn ngã rẽ trái cùng một lúc. Bây giờ tất cả họ đang chờ đợi một chiếc nĩa phù hợp được miễn phí nhưng họ sẽ không bao giờ từ bỏ chiếc nĩa của mình cho đến khi họ đã ăn thức ăn của mình và chiếc nĩa phù hợp sẽ không bao giờ có sẵn. Do đó, sẽ có một trạng thái bế tắc tại bàn ăn.
Bế tắc trong hệ thống đồng thời
Bây giờ nếu chúng ta thấy, vấn đề tương tự cũng có thể phát sinh trong các hệ thống đồng thời của chúng ta. Các nhánh trong ví dụ trên sẽ là tài nguyên của hệ thống và mỗi nhà triết học có thể đại diện cho quy trình đang cạnh tranh để có được tài nguyên.
Giải pháp với chương trình Python
Giải pháp của vấn đề này có thể được tìm thấy bằng cách chia các triết gia thành hai loại - greedy philosophers và generous philosophers. Chủ yếu là một triết gia tham lam sẽ cố gắng nhặt cái ngã ba bên trái và đợi cho đến khi nó ở đó. Sau đó, anh ta sẽ đợi đến đúng cái nĩa ở đó, nhặt nó lên, ăn và sau đó đặt nó xuống. Mặt khác, một triết gia hào phóng sẽ cố gắng nhặt ngã ba bên trái và nếu nó không ở đó, anh ta sẽ đợi và thử lại sau một thời gian. Nếu họ nhận được ngã ba bên trái thì họ sẽ cố gắng đến bên phải. Nếu chúng cũng lấy đúng nĩa thì chúng sẽ ăn và nhả cả hai nĩa. Tuy nhiên, nếu họ không nhận được ngã ba bên phải thì họ sẽ phát hành ngã ba bên trái.
Thí dụ
Chương trình Python sau đây sẽ giúp chúng ta tìm ra giải pháp cho vấn đề triết học ăn uống -
import threading
import random
import time
class DiningPhilosopher(threading.Thread):
running = True
def __init__(self, xname, Leftfork, Rightfork):
threading.Thread.__init__(self)
self.name = xname
self.Leftfork = Leftfork
self.Rightfork = Rightfork
def run(self):
while(self.running):
time.sleep( random.uniform(3,13))
print ('%s is hungry.' % self.name)
self.dine()
def dine(self):
fork1, fork2 = self.Leftfork, self.Rightfork
while self.running:
fork1.acquire(True)
locked = fork2.acquire(False)
if locked: break
fork1.release()
print ('%s swaps forks' % self.name)
fork1, fork2 = fork2, fork1
else:
return
self.dining()
fork2.release()
fork1.release()
def dining(self):
print ('%s starts eating '% self.name)
time.sleep(random.uniform(1,10))
print ('%s finishes eating and now thinking.' % self.name)
def Dining_Philosophers():
forks = [threading.Lock() for n in range(5)]
philosopherNames = ('1st','2nd','3rd','4th', '5th')
philosophers= [DiningPhilosopher(philosopherNames[i], forks[i%5], forks[(i+1)%5]) \
for i in range(5)]
random.seed()
DiningPhilosopher.running = True
for p in philosophers: p.start()
time.sleep(30)
DiningPhilosopher.running = False
print (" It is finishing.")
Dining_Philosophers()
Chương trình trên sử dụng khái niệm triết gia tham lam và hào phóng. Chương trình cũng đã sử dụngacquire() và release() phương pháp của Lock lớp của <threading>mô-đun. Chúng ta có thể thấy giải pháp trong đầu ra sau:
Đầu ra
4th is hungry.
4th starts eating
1st is hungry.
1st starts eating
2nd is hungry.
5th is hungry.
3rd is hungry.
1st finishes eating and now thinking.3rd swaps forks
2nd starts eating
4th finishes eating and now thinking.
3rd swaps forks5th starts eating
5th finishes eating and now thinking.
4th is hungry.
4th starts eating
2nd finishes eating and now thinking.
3rd swaps forks
1st is hungry.
1st starts eating
4th finishes eating and now thinking.
3rd starts eating
5th is hungry.
5th swaps forks
1st finishes eating and now thinking.
5th starts eating
2nd is hungry.
2nd swaps forks
4th is hungry.
5th finishes eating and now thinking.
3rd finishes eating and now thinking.
2nd starts eating 4th starts eating
It is finishing.
Trong cuộc sống thực, nếu một nhóm người đang thực hiện một nhiệm vụ chung thì cần có sự giao tiếp giữa họ để hoàn thành nhiệm vụ đúng cách. Điều tương tự cũng được áp dụng cho các chủ đề. Trong lập trình, để giảm thời gian lý tưởng của bộ xử lý chúng ta tạo nhiều luồng và gán các nhiệm vụ con khác nhau cho mỗi luồng. Do đó, cần phải có một phương tiện liên lạc và chúng phải tương tác với nhau để hoàn thành công việc một cách đồng bộ.
Hãy xem xét các điểm quan trọng sau đây liên quan đến thông tin liên lạc chuỗi -
No performance gain - Nếu chúng ta không thể đạt được giao tiếp thích hợp giữa các luồng và các quy trình thì hiệu suất đạt được từ sự đồng thời và song song sẽ không có ích gì.
Accomplish task properly - Không có cơ chế liên lạc thích hợp giữa các luồng, nhiệm vụ được giao không thể hoàn thành đúng cách.
More efficient than inter-process communication - Giao tiếp giữa các luồng hiệu quả hơn và dễ sử dụng hơn so với giao tiếp giữa các tiến trình vì tất cả các luồng trong một tiến trình chia sẻ cùng một không gian địa chỉ và chúng không cần sử dụng bộ nhớ dùng chung.
Cấu trúc dữ liệu Python cho giao tiếp an toàn theo luồng
Mã đa luồng dẫn đến vấn đề truyền thông tin từ luồng này sang luồng khác. Các nguyên thủy giao tiếp tiêu chuẩn không giải quyết được vấn đề này. Do đó, chúng ta cần triển khai đối tượng tổng hợp của riêng mình để chia sẻ các đối tượng giữa các luồng để làm cho luồng giao tiếp an toàn. Sau đây là một số cấu trúc dữ liệu cung cấp giao tiếp an toàn theo chuỗi sau khi thực hiện một số thay đổi trong chúng -
Bộ
Để sử dụng cấu trúc dữ liệu tập hợp theo cách an toàn luồng, chúng ta cần mở rộng lớp tập hợp để triển khai cơ chế khóa của riêng chúng ta.
Thí dụ
Đây là một ví dụ Python về việc mở rộng lớp -
class extend_class(set):
def __init__(self, *args, **kwargs):
self._lock = Lock()
super(extend_class, self).__init__(*args, **kwargs)
def add(self, elem):
self._lock.acquire()
try:
super(extend_class, self).add(elem)
finally:
self._lock.release()
def delete(self, elem):
self._lock.acquire()
try:
super(extend_class, self).delete(elem)
finally:
self._lock.release()
Trong ví dụ trên, một đối tượng lớp có tên extend_class đã được xác định và được kế thừa từ Python set class. Một đối tượng khóa được tạo bên trong hàm tạo của lớp này. Bây giờ, có hai chức năng -add() và delete(). Các chức năng này được xác định và an toàn theo luồng. Cả hai đều dựa vàosuper chức năng lớp với một ngoại lệ chính.
Người trang trí
Đây là một phương pháp quan trọng khác để truyền thông an toàn theo luồng là sử dụng trình trang trí.
Thí dụ
Hãy xem xét một ví dụ Python cho thấy cách sử dụng trình trang trí & mminus;
def lock_decorator(method):
def new_deco_method(self, *args, **kwargs):
with self._lock:
return method(self, *args, **kwargs)
return new_deco_method
class Decorator_class(set):
def __init__(self, *args, **kwargs):
self._lock = Lock()
super(Decorator_class, self).__init__(*args, **kwargs)
@lock_decorator
def add(self, *args, **kwargs):
return super(Decorator_class, self).add(elem)
@lock_decorator
def delete(self, *args, **kwargs):
return super(Decorator_class, self).delete(elem)
Trong ví dụ trên, một phương thức decorator có tên lock_decorator đã được định nghĩa, phương thức này sẽ được kế thừa từ lớp phương thức Python. Sau đó, một đối tượng khóa được tạo bên trong hàm tạo của lớp này. Bây giờ, có hai chức năng - thêm () và xóa (). Các chức năng này được xác định và an toàn theo luồng. Cả hai đều dựa trên chức năng siêu cấp với một ngoại lệ chính.
Danh sách
Cấu trúc dữ liệu danh sách an toàn theo luồng, nhanh chóng cũng như cấu trúc dễ dàng để lưu trữ tạm thời trong bộ nhớ. Trong Cpython, GIL bảo vệ chống lại việc truy cập đồng thời vào chúng. Như chúng ta đã biết rằng danh sách là an toàn theo chuỗi nhưng dữ liệu nằm trong đó thì sao. Trên thực tế, dữ liệu của danh sách không được bảo vệ. Ví dụ,L.append(x)không đảm bảo trả về kết quả mong đợi nếu một luồng khác đang cố gắng làm điều tương tự. Điều này là do, mặc dùappend() là một hoạt động nguyên tử và an toàn cho luồng nhưng luồng khác đang cố gắng sửa đổi dữ liệu của danh sách theo cách đồng thời, do đó chúng ta có thể thấy các tác dụng phụ của điều kiện chủng tộc trên đầu ra.
Để giải quyết loại vấn đề này và sửa đổi dữ liệu một cách an toàn, chúng ta phải triển khai cơ chế khóa thích hợp, điều này đảm bảo hơn nữa rằng nhiều luồng không thể có khả năng chạy vào điều kiện đua. Để thực hiện cơ chế khóa thích hợp, chúng ta có thể mở rộng lớp như chúng ta đã làm trong các ví dụ trước.
Một số hoạt động nguyên tử khác trên danh sách như sau:
L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()
Đây -
- L, L1, L2 tất cả đều là danh sách
- D, D1, D2 là các con số
- x, y là các đối tượng
- tôi, j là int
Hàng đợi
Nếu dữ liệu của danh sách không được bảo vệ, chúng tôi có thể phải đối mặt với hậu quả. Chúng tôi có thể lấy hoặc xóa mục dữ liệu sai, về điều kiện chủng tộc. Đó là lý do tại sao nên sử dụng cấu trúc dữ liệu hàng đợi. Ví dụ trong thế giới thực về hàng đợi có thể là đường một chiều có một làn xe, nơi xe đi vào trước, ra trước. Có thể thấy nhiều ví dụ thực tế hơn về việc xếp hàng tại cửa sổ soát vé và trạm dừng xe buýt.
Hàng đợi là cấu trúc dữ liệu an toàn theo mặc định và chúng ta không cần lo lắng về việc triển khai cơ chế khóa phức tạp. Python cung cấp cho chúng ta
Các loại hàng đợi
Trong phần này, chúng ta sẽ tìm hiểu về các loại hàng đợi khác nhau. Python cung cấp ba tùy chọn hàng đợi để sử dụng từ<queue> mô-đun -
- Hàng đợi bình thường (FIFO, First in First out)
- LIFO, Lần xuất trước
- Priority
Chúng ta sẽ tìm hiểu về các hàng đợi khác nhau trong các phần tiếp theo.
Hàng đợi bình thường (FIFO, First in First out)
Đây là cách triển khai hàng đợi được sử dụng phổ biến nhất do Python cung cấp. Trong cơ chế xếp hàng này, ai đến trước sẽ nhận được dịch vụ trước. FIFO còn được gọi là hàng đợi bình thường. Hàng đợi FIFO có thể được biểu diễn như sau:
Triển khai Python của Hàng đợi FIFO
Trong python, hàng đợi FIFO có thể được thực hiện với luồng đơn cũng như đa luồng.
Hàng đợi FIFO với luồng đơn
Để triển khai hàng đợi FIFO với một luồng, Queuelớp sẽ triển khai một container cơ bản nhập trước, xuất trước. Các phần tử sẽ được thêm vào một "cuối" của chuỗi bằng cách sử dụngput()và bị xóa khỏi đầu kia bằng cách sử dụng get().
Thí dụ
Sau đây là một chương trình Python để triển khai hàng đợi FIFO với một luồng:
import queue
q = queue.Queue()
for i in range(8):
q.put("item-" + str(i))
while not q.empty():
print (q.get(), end = " ")
Đầu ra
item-0 item-1 item-2 item-3 item-4 item-5 item-6 item-7
Kết quả đầu ra cho thấy rằng chương trình trên sử dụng một luồng duy nhất để minh họa rằng các phần tử được xóa khỏi hàng đợi theo thứ tự mà chúng được chèn vào.
Hàng đợi FIFO với nhiều chủ đề
Để triển khai FIFO với nhiều luồng, chúng ta cần xác định hàm myqueue (), được mở rộng từ mô-đun hàng đợi. Hoạt động của các phương thức get () và put () giống như đã thảo luận ở trên trong khi triển khai hàng đợi FIFO với một luồng. Sau đó, để làm cho nó đa luồng, chúng ta cần khai báo và khởi tạo các luồng. Các luồng này sẽ sử dụng hàng đợi theo cách FIFO.
Thí dụ
Sau đây là một chương trình Python để triển khai hàng đợi FIFO với nhiều luồng
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(2)
q = queue.Queue()
for i in range(5):
q.put(i)
threads = []
for i in range(4):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Đầu ra
<Thread(Thread-3654, started 5044)> removed 0 from the queue
<Thread(Thread-3655, started 3144)> removed 1 from the queue
<Thread(Thread-3656, started 6996)> removed 2 from the queue
<Thread(Thread-3657, started 2672)> removed 3 from the queue
<Thread(Thread-3654, started 5044)> removed 4 from the queue
LIFO, Cuối cùng trong hàng đợi Đầu tiên
Hàng đợi này sử dụng phép loại suy hoàn toàn trái ngược với hàng đợi FIFO (First in First Out). Trong cơ chế xếp hàng này, ai đến sau cùng sẽ nhận được dịch vụ trước. Điều này tương tự để thực hiện cấu trúc dữ liệu ngăn xếp. Hàng đợi LIFO tỏ ra hữu ích trong khi triển khai tìm kiếm Theo độ sâu giống như các thuật toán của trí tuệ nhân tạo.
Triển khai Python của hàng đợi LIFO
Trong python, hàng đợi LIFO có thể được thực hiện với luồng đơn cũng như đa luồng.
Hàng đợi LIFO với một chuỗi đơn
Để triển khai hàng đợi LIFO với một luồng đơn, Queue lớp sẽ triển khai một vùng chứa cuối cùng vào, ra trước cơ bản bằng cách sử dụng cấu trúc Queue.LifoQueue. Bây giờ, đang gọiput(), các phần tử được thêm vào phần đầu của vùng chứa và được loại bỏ khỏi phần đầu cũng khi sử dụng get().
Thí dụ
Sau đây là một chương trình Python để triển khai hàng đợi LIFO với một luồng đơn:
import queue
q = queue.LifoQueue()
for i in range(8):
q.put("item-" + str(i))
while not q.empty():
print (q.get(), end=" ")
Output:
item-7 item-6 item-5 item-4 item-3 item-2 item-1 item-0
Kết quả đầu ra cho thấy rằng chương trình trên sử dụng một luồng duy nhất để minh họa rằng các phần tử bị xóa khỏi hàng đợi theo thứ tự ngược lại mà chúng được chèn vào.
Hàng đợi LIFO với nhiều chủ đề
Việc triển khai tương tự như chúng ta đã thực hiện việc triển khai hàng đợi FIFO với nhiều luồng. Sự khác biệt duy nhất là chúng ta cần sử dụngQueue lớp sẽ triển khai một vùng chứa cuối cùng vào, ra trước cơ bản bằng cách sử dụng cấu trúc Queue.LifoQueue.
Thí dụ
Sau đây là một chương trình Python để triển khai hàng đợi LIFO với nhiều luồng:
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(2)
q = queue.LifoQueue()
for i in range(5):
q.put(i)
threads = []
for i in range(4):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Đầu ra
<Thread(Thread-3882, started 4928)> removed 4 from the queue
<Thread(Thread-3883, started 4364)> removed 3 from the queue
<Thread(Thread-3884, started 6908)> removed 2 from the queue
<Thread(Thread-3885, started 3584)> removed 1 from the queue
<Thread(Thread-3882, started 4928)> removed 0 from the queue
Hàng đợi ưu tiên
Trong hàng đợi FIFO và LIFO, thứ tự của các mục có liên quan đến thứ tự chèn. Tuy nhiên, có nhiều trường hợp mức độ ưu tiên quan trọng hơn thứ tự chèn. Chúng ta hãy xem xét một ví dụ trong thế giới thực. Giả sử an ninh ở sân bay đang kiểm tra những người thuộc các hạng mục khác nhau. Những người thuộc VVIP, nhân viên hàng không, nhân viên hải quan, các loại có thể được ưu tiên kiểm tra thay vì được kiểm tra trên cơ sở khi đến như đối với dân thường.
Một khía cạnh quan trọng khác cần được xem xét cho hàng đợi ưu tiên là cách phát triển bộ lập lịch tác vụ. Một thiết kế phổ biến là phục vụ nhiệm vụ tác nhân nhất trên cơ sở ưu tiên trong hàng đợi. Cấu trúc dữ liệu này có thể được sử dụng để chọn các mục từ hàng đợi dựa trên giá trị ưu tiên của chúng.
Triển khai hàng đợi ưu tiên trong Python
Trong python, hàng đợi ưu tiên có thể được thực hiện với luồng đơn cũng như đa luồng.
Hàng đợi ưu tiên với một chuỗi đơn
Để triển khai hàng đợi ưu tiên với một luồng, Queue lớp sẽ triển khai một tác vụ trên vùng chứa ưu tiên bằng cách sử dụng cấu trúc Queue.Hàng đợi ưu tiên. Bây giờ, đang gọiput(), các phần tử được thêm vào với một giá trị trong đó giá trị thấp nhất sẽ có mức ưu tiên cao nhất và do đó được truy xuất đầu tiên bằng cách sử dụng get().
Thí dụ
Hãy xem xét chương trình Python sau để triển khai hàng đợi Ưu tiên với một luồng:
import queue as Q
p_queue = Q.PriorityQueue()
p_queue.put((2, 'Urgent'))
p_queue.put((1, 'Most Urgent'))
p_queue.put((10, 'Nothing important'))
prio_queue.put((5, 'Important'))
while not p_queue.empty():
item = p_queue.get()
print('%s - %s' % item)
Đầu ra
1 – Most Urgent
2 - Urgent
5 - Important
10 – Nothing important
Trong đầu ra ở trên, chúng ta có thể thấy rằng hàng đợi đã lưu trữ các mục dựa trên mức độ ưu tiên - giá trị ít hơn có mức độ ưu tiên cao.
Hàng đợi ưu tiên với nhiều chủ đề
Việc triển khai tương tự như việc thực hiện các hàng đợi FIFO và LIFO với nhiều luồng. Sự khác biệt duy nhất là chúng ta cần sử dụngQueue lớp để khởi tạo mức độ ưu tiên bằng cách sử dụng cấu trúc Queue.PriorityQueue. Một sự khác biệt khác là cách tạo hàng đợi. Trong ví dụ dưới đây, nó sẽ được tạo bằng hai tập dữ liệu giống nhau.
Thí dụ
Chương trình Python sau giúp thực hiện hàng đợi ưu tiên với nhiều luồng:
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(1)
q = queue.PriorityQueue()
for i in range(5):
q.put(i,1)
for i in range(5):
q.put(i,1)
threads = []
for i in range(2):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Đầu ra
<Thread(Thread-4939, started 2420)> removed 0 from the queue
<Thread(Thread-4940, started 3284)> removed 0 from the queue
<Thread(Thread-4939, started 2420)> removed 1 from the queue
<Thread(Thread-4940, started 3284)> removed 1 from the queue
<Thread(Thread-4939, started 2420)> removed 2 from the queue
<Thread(Thread-4940, started 3284)> removed 2 from the queue
<Thread(Thread-4939, started 2420)> removed 3 from the queue
<Thread(Thread-4940, started 3284)> removed 3 from the queue
<Thread(Thread-4939, started 2420)> removed 4 from the queue
<Thread(Thread-4940, started 3284)> removed 4 from the queue
Trong chương này, chúng ta sẽ tìm hiểu về kiểm thử các ứng dụng luồng. Chúng ta cũng sẽ tìm hiểu tầm quan trọng của thử nghiệm.
Tại sao phải kiểm tra?
Trước khi đi sâu vào thảo luận về tầm quan trọng của thử nghiệm, chúng ta cần biết thử nghiệm là gì. Nói chung, thử nghiệm là một kỹ thuật tìm hiểu xem một thứ gì đó đang hoạt động tốt như thế nào. Mặt khác, cụ thể nếu chúng ta nói về chương trình máy tính hoặc phần mềm thì kiểm thử là kỹ thuật truy cập chức năng của một chương trình phần mềm.
Trong phần này, chúng ta sẽ thảo luận về tầm quan trọng của kiểm thử phần mềm. Trong phát triển phần mềm, phải kiểm tra kỹ trước khi phát hành phần mềm cho khách hàng. Đó là lý do tại sao việc kiểm tra phần mềm bởi đội ngũ kiểm thử có kinh nghiệm là rất quan trọng. Hãy xem xét các điểm sau để hiểu tầm quan trọng của kiểm thử phần mềm -
Cải thiện chất lượng phần mềm
Chắc chắn, không công ty nào muốn cung cấp phần mềm chất lượng thấp và không khách hàng nào muốn mua phần mềm chất lượng thấp. Kiểm tra cải thiện chất lượng của phần mềm bằng cách tìm và sửa các lỗi trong đó.
Sự hài lòng của khách hàng
Phần quan trọng nhất của bất kỳ doanh nghiệp nào là sự hài lòng của khách hàng. Bằng cách cung cấp phần mềm không có lỗi và chất lượng tốt, các công ty có thể đạt được sự hài lòng của khách hàng.
Giảm bớt tác động của các tính năng mới
Giả sử chúng ta đã tạo ra một hệ thống phần mềm gồm 10000 dòng và chúng ta cần thêm một tính năng mới thì nhóm phát triển sẽ lo ngại về tác động của tính năng mới này đối với toàn bộ phần mềm. Ở đây, thử nghiệm cũng đóng một vai trò quan trọng vì nếu nhóm thử nghiệm đã tạo ra một bộ thử nghiệm tốt thì nó có thể giúp chúng ta tránh khỏi bất kỳ sự cố thảm khốc nào có thể xảy ra.
Kinh nghiệm người dùng
Một phần quan trọng nhất khác của bất kỳ doanh nghiệp nào là trải nghiệm của người dùng sản phẩm đó. Chỉ có thử nghiệm mới có thể đảm bảo rằng người dùng cuối cảm thấy đơn giản và dễ sử dụng sản phẩm.
Cắt giảm chi phí
Thử nghiệm có thể cắt giảm tổng chi phí của phần mềm bằng cách tìm và sửa các lỗi trong giai đoạn thử nghiệm của quá trình phát triển phần mềm hơn là sửa sau khi giao hàng. Nếu có một lỗi lớn sau khi phân phối phần mềm thì nó sẽ làm tăng chi phí hữu hình nói về chi phí và chi phí vô hình nói về sự không hài lòng của khách hàng, danh tiếng tiêu cực của công ty, v.v.
Kiểm tra những gì?
Bạn nên có kiến thức thích hợp về những gì sẽ được kiểm tra. Trong phần này, trước tiên chúng ta sẽ hiểu động cơ chính của tester khi kiểm thử bất kỳ phần mềm nào. Phạm vi của mã, tức là, nên tránh bao nhiêu dòng mã mà bộ thử nghiệm của chúng tôi truy cập trong khi thử nghiệm. Đó là bởi vì, trong khi thử nghiệm, chỉ tập trung vào số lượng dòng mã không tạo thêm giá trị thực cho hệ thống của chúng tôi. Có thể vẫn còn một số lỗi, lỗi này sẽ phản ánh muộn hơn ở giai đoạn sau ngay cả sau khi triển khai.
Hãy xem xét những điểm quan trọng sau liên quan đến những gì cần kiểm tra -
Chúng ta cần tập trung vào việc kiểm tra chức năng của mã hơn là độ phủ của mã.
Chúng ta cần kiểm tra những phần quan trọng nhất của mã trước và sau đó chuyển sang những phần ít quan trọng hơn của mã. Nó chắc chắn sẽ tiết kiệm thời gian.
Người thử nghiệm phải có vô số thử nghiệm khác nhau để có thể đẩy phần mềm lên đến giới hạn của nó.
Các cách tiếp cận để kiểm tra các chương trình phần mềm đồng thời
Do khả năng sử dụng khả năng thực sự của kiến trúc đa lõi, các hệ thống phần mềm đồng thời đang thay thế các hệ thống tuần tự. Trong thời gian gần đây, các chương trình hệ thống đồng thời đang được sử dụng trong mọi thứ, từ điện thoại di động đến máy giặt, từ ô tô đến máy bay, v.v. Chúng ta cần phải cẩn thận hơn khi kiểm tra các chương trình phần mềm đồng thời vì nếu chúng ta đã thêm nhiều luồng vào ứng dụng một luồng có đã là một lỗi, thì chúng ta sẽ có nhiều lỗi.
Các kỹ thuật kiểm tra cho các chương trình phần mềm đồng thời đang tập trung rộng rãi vào việc lựa chọn xen kẽ các mô hình có thể gây hại như điều kiện chủng tộc, bế tắc và vi phạm tính nguyên tử. Sau đây là hai cách tiếp cận để kiểm tra các chương trình phần mềm đồng thời:
Khám phá có hệ thống
Cách tiếp cận này nhằm mục đích khám phá không gian của sự đan xen càng rộng càng tốt. Những cách tiếp cận như vậy có thể áp dụng kỹ thuật brute-force và những cách khác áp dụng kỹ thuật giảm thứ tự từng phần hoặc kỹ thuật heuristic để khám phá không gian của sự đan xen.
Hướng về tài sản
Các phương pháp tiếp cận theo hướng thuộc tính dựa trên quan sát rằng các lỗi đồng thời có nhiều khả năng xảy ra dưới sự đan xen làm lộ các thuộc tính cụ thể như kiểu truy cập bộ nhớ đáng ngờ. Các phương pháp tiếp cận theo hướng thuộc tính khác nhau nhắm vào các lỗi khác nhau như điều kiện chủng tộc, bế tắc và vi phạm tính nguyên tử, điều này còn phụ thuộc vào một hoặc các thuộc tính cụ thể khác.
Các chiến lược kiểm tra
Chiến lược kiểm tra còn được gọi là cách tiếp cận kiểm tra. Chiến lược xác định cách thức kiểm tra sẽ được thực hiện. Cách tiếp cận kiểm tra có hai kỹ thuật -
Chủ động
Một cách tiếp cận trong đó quá trình thiết kế thử nghiệm được bắt đầu sớm nhất có thể để tìm và sửa chữa các khiếm khuyết trước khi tạo bản dựng.
Phản ứng
Một cách tiếp cận trong đó thử nghiệm không bắt đầu cho đến khi hoàn thành quá trình phát triển.
Trước khi áp dụng bất kỳ chiến lược hoặc phương pháp kiểm tra nào trên chương trình python, chúng ta phải có ý tưởng cơ bản về loại lỗi mà chương trình phần mềm có thể mắc phải. Các lỗi như sau:
Lỗi cú pháp
Trong quá trình phát triển chương trình, có thể mắc nhiều lỗi nhỏ. Các lỗi phần lớn là do lỗi đánh máy. Ví dụ, thiếu dấu hai chấm hoặc sai chính tả của từ khóa, vv Những lỗi như vậy là do sai cú pháp chương trình và không logic. Do đó, những lỗi này được gọi là lỗi cú pháp.
Lỗi ngữ nghĩa
Các lỗi ngữ nghĩa còn được gọi là lỗi lôgic. Nếu có lỗi logic hoặc ngữ nghĩa trong chương trình phần mềm thì câu lệnh sẽ biên dịch và chạy chính xác nhưng nó sẽ không đưa ra kết quả mong muốn vì logic không đúng.
Kiểm tra đơn vị
Đây là một trong những chiến lược kiểm thử được sử dụng nhiều nhất để kiểm tra các chương trình python. Chiến lược này được sử dụng để kiểm tra các đơn vị hoặc thành phần của mã. Theo đơn vị hoặc thành phần, chúng tôi có nghĩa là các lớp hoặc chức năng của mã. Kiểm thử đơn vị đơn giản hóa việc kiểm tra các hệ thống lập trình lớn bằng cách kiểm tra các đơn vị “nhỏ”. Với sự trợ giúp của khái niệm trên, kiểm thử đơn vị có thể được định nghĩa là một phương pháp trong đó các đơn vị mã nguồn riêng lẻ được kiểm tra để xác định xem chúng có trả lại đầu ra mong muốn hay không.
Trong các phần tiếp theo, chúng ta sẽ tìm hiểu về các mô-đun Python khác nhau để kiểm tra đơn vị.
mô-đun đơn nhất
Mô-đun đầu tiên cho kiểm thử đơn vị là mô-đun đơn vị nhất. Nó được lấy cảm hứng từ JUnit và theo mặc định được bao gồm trong Python3.6. Nó hỗ trợ tự động hóa thử nghiệm, chia sẻ mã thiết lập và tắt cho các thử nghiệm, tổng hợp các thử nghiệm thành bộ sưu tập và tính độc lập của các thử nghiệm khỏi khung báo cáo.
Sau đây là một vài khái niệm quan trọng được hỗ trợ bởi mô-đun đơn nhất
Văn bản cố định
Nó được sử dụng để thiết lập một bài kiểm tra để nó có thể được chạy trước khi bắt đầu bài kiểm tra và hủy bỏ sau khi kết thúc bài kiểm tra. Nó có thể liên quan đến việc tạo cơ sở dữ liệu tạm thời, thư mục, v.v. cần thiết trước khi bắt đầu kiểm tra.
Trường hợp thử nghiệm
Trường hợp kiểm thử kiểm tra xem phản hồi bắt buộc có phải đến từ một tập hợp đầu vào cụ thể hay không. Mô-đun đơn nhất bao gồm một lớp cơ sở có tên là TestCase có thể được sử dụng để tạo các trường hợp thử nghiệm mới. Nó bao gồm hai phương pháp mặc định:
setUp()- một phương pháp móc để thiết lập bộ cố định thử nghiệm trước khi thực hiện nó. Điều này được gọi trước khi gọi các phương pháp thử nghiệm đã triển khai.
tearDown( - một phương thức hook để giải cấu trúc lớp cố định sau khi chạy tất cả các bài kiểm tra trong lớp.
Bộ thử nghiệm
Nó là một tập hợp các bộ thử nghiệm, các trường hợp thử nghiệm hoặc cả hai.
Người chạy thử nghiệm
Nó kiểm soát việc chạy các trường hợp hoặc bộ thử nghiệm và cung cấp kết quả cho người dùng. Nó có thể sử dụng GUI hoặc giao diện văn bản đơn giản để cung cấp kết quả.
Example
Chương trình Python sau sử dụng mô-đun đơn nhất để kiểm tra mô-đun có tên Fibonacci. Chương trình giúp tính toán chuỗi Fibonacci của một số. Trong ví dụ này, chúng tôi đã tạo một lớp có tên Fibo_test, để xác định các trường hợp thử nghiệm bằng cách sử dụng các phương pháp khác nhau. Các phương thức này được kế thừa từ unittest.TestCase. Chúng tôi đang sử dụng hai phương thức mặc định - setUp () và ráchDown (). Chúng tôi cũng xác định phương pháp testfibocal. Tên của bài kiểm tra phải được bắt đầu bằng bài kiểm tra chữ cái. Trong khối cuối cùng, unittest.main () cung cấp một giao diện dòng lệnh cho tập lệnh thử nghiệm.
import unittest
def fibonacci(n):
a, b = 0, 1
for i in range(n):
a, b = b, a + b
return a
class Fibo_Test(unittest.TestCase):
def setUp(self):
print("This is run before our tests would be executed")
def tearDown(self):
print("This is run after the completion of execution of our tests")
def testfibocal(self):
self.assertEqual(fib(0), 0)
self.assertEqual(fib(1), 1)
self.assertEqual(fib(5), 5)
self.assertEqual(fib(10), 55)
self.assertEqual(fib(20), 6765)
if __name__ == "__main__":
unittest.main()
Khi chạy từ dòng lệnh, tập lệnh trên tạo ra kết quả giống như sau:
Đầu ra
This runs before our tests would be executed.
This runs after the completion of execution of our tests.
.
----------------------------------------------------------------------
Ran 1 test in 0.006s
OK
Bây giờ, để làm rõ hơn, chúng tôi đang thay đổi mã của mình, điều này đã giúp xác định mô-đun Fibonacci.
Hãy xem xét khối mã sau làm ví dụ:
def fibonacci(n):
a, b = 0, 1
for i in range(n):
a, b = b, a + b
return a
Một số thay đổi đối với khối mã được thực hiện như hình dưới đây -
def fibonacci(n):
a, b = 1, 1
for i in range(n):
a, b = b, a + b
return a
Bây giờ, sau khi chạy tập lệnh với mã đã thay đổi, chúng ta sẽ nhận được kết quả sau:
This runs before our tests would be executed.
This runs after the completion of execution of our tests.
F
======================================================================
FAIL: testCalculation (__main__.Fibo_Test)
----------------------------------------------------------------------
Traceback (most recent call last):
File "unitg.py", line 15, in testCalculation
self.assertEqual(fib(0), 0)
AssertionError: 1 != 0
----------------------------------------------------------------------
Ran 1 test in 0.007s
FAILED (failures = 1)
Kết quả trên cho thấy rằng mô-đun đã không cung cấp đầu ra mong muốn.
Mô-đun Docktest
Mô-đun docktest cũng giúp kiểm tra đơn vị. Nó cũng được đóng gói sẵn với python. Nó dễ sử dụng hơn mô-đun đơn nhất. Mô-đun đơn nhất phù hợp hơn cho các bài kiểm tra phức tạp. Để sử dụng mô-đun học thuyết, chúng ta cần nhập nó. Chuỗi doc của hàm tương ứng phải có phiên python tương tác cùng với đầu ra của chúng.
Nếu mọi thứ đều ổn trong mã của chúng tôi thì sẽ không có đầu ra từ mô-đun docktest; nếu không, nó sẽ cung cấp đầu ra.
Thí dụ
Ví dụ Python sau đây sử dụng mô-đun docktest để kiểm tra một mô-đun có tên là Fibonacci, giúp tính toán chuỗi Fibonacci của một số.
import doctest
def fibonacci(n):
"""
Calculates the Fibonacci number
>>> fibonacci(0)
0
>>> fibonacci(1)
1
>>> fibonacci(10)
55
>>> fibonacci(20)
6765
>>>
"""
a, b = 1, 1
for i in range(n):
a, b = b, a + b
return a
if __name__ == "__main__":
doctest.testmod()
Chúng ta có thể thấy rằng chuỗi doc của hàm tương ứng có tên fib có phiên python tương tác cùng với các kết quả đầu ra. Nếu mã của chúng tôi ổn thì sẽ không có đầu ra từ mô-đun học thuyết. Nhưng để xem nó hoạt động như thế nào, chúng ta có thể chạy nó với tùy chọn –v.
(base) D:\ProgramData>python dock_test.py -v
Trying:
fibonacci(0)
Expecting:
0
ok
Trying:
fibonacci(1)
Expecting:
1
ok
Trying:
fibonacci(10)
Expecting:
55
ok
Trying:
fibonacci(20)
Expecting:
6765
ok
1 items had no tests:
__main__
1 items passed all tests:
4 tests in __main__.fibonacci
4 tests in 2 items.
4 passed and 0 failed.
Test passed.
Bây giờ, chúng tôi sẽ thay đổi mã đã giúp xác định mô-đun Fibonacci
Hãy xem xét khối mã sau làm ví dụ:
def fibonacci(n):
a, b = 0, 1
for i in range(n):
a, b = b, a + b
return a
Khối mã sau giúp thực hiện các thay đổi:
def fibonacci(n):
a, b = 1, 1
for i in range(n):
a, b = b, a + b
return a
Sau khi chạy script ngay cả khi không có tùy chọn –v, với đoạn mã đã thay đổi, chúng ta sẽ nhận được kết quả như hình dưới đây.
Đầu ra
(base) D:\ProgramData>python dock_test.py
**********************************************************************
File "unitg.py", line 6, in __main__.fibonacci
Failed example:
fibonacci(0)
Expected:
0
Got:
1
**********************************************************************
File "unitg.py", line 10, in __main__.fibonacci
Failed example:
fibonacci(10)
Expected:
55
Got:
89
**********************************************************************
File "unitg.py", line 12, in __main__.fibonacci
Failed example:
fibonacci(20)
Expected:
6765
Got:
10946
**********************************************************************
1 items had failures:
3 of 4 in __main__.fibonacci
***Test Failed*** 3 failures.
Chúng ta có thể thấy trong đầu ra ở trên rằng ba bài kiểm tra đã thất bại.
Trong chương này, chúng ta sẽ học cách gỡ lỗi các ứng dụng luồng. Chúng ta cũng sẽ tìm hiểu tầm quan trọng của việc gỡ lỗi.
Gỡ lỗi là gì?
Trong lập trình máy tính, gỡ lỗi là quá trình tìm và loại bỏ các lỗi, lỗi và bất thường khỏi chương trình máy tính. Quá trình này bắt đầu ngay sau khi mã được viết và tiếp tục trong các giai đoạn liên tiếp khi mã được kết hợp với các đơn vị lập trình khác để tạo thành một sản phẩm phần mềm. Gỡ lỗi là một phần của quy trình kiểm thử phần mềm và là một phần không thể thiếu trong toàn bộ vòng đời phát triển phần mềm.
Trình gỡ lỗi Python
Trình gỡ lỗi Python hoặc pdblà một phần của thư viện chuẩn Python. Nó là một công cụ dự phòng tốt để theo dõi các lỗi khó tìm và cho phép chúng tôi sửa mã bị lỗi một cách nhanh chóng và đáng tin cậy. Tiếp theo là hai nhiệm vụ quan trọng nhất củapdp trình gỡ lỗi -
- Nó cho phép chúng ta kiểm tra giá trị của các biến trong thời gian chạy.
- Chúng ta cũng có thể bước qua mã và đặt các điểm ngắt.
Chúng ta có thể làm việc với pdb theo hai cách sau:
- Thông qua dòng lệnh; điều này cũng được gọi là gỡ lỗi sau khi chết.
- Bằng cách chạy pdb tương tác.
Làm việc với pdb
Để làm việc với trình gỡ lỗi Python, chúng ta cần sử dụng mã sau tại vị trí mà chúng ta muốn xâm nhập vào trình gỡ lỗi:
import pdb;
pdb.set_trace()
Hãy xem xét các lệnh sau để làm việc với pdb thông qua dòng lệnh.
- h(help)
- d(down)
- u(up)
- b(break)
- cl(clear)
- l(list))
- n(next))
- c(continue)
- s(step)
- r(return))
- b(break)
Sau đây là bản trình diễn lệnh h (help) của trình gỡ lỗi Python:
import pdb
pdb.set_trace()
--Call--
>d:\programdata\lib\site-packages\ipython\core\displayhook.py(247)__call__()
-> def __call__(self, result = None):
(Pdb) h
Documented commands (type help <topic>):
========================================
EOF c d h list q rv undisplay
a cl debug help ll quit s unt
alias clear disable ignore longlist r source until
args commands display interact n restart step up
b condition down j next return tbreak w
break cont enable jump p retval u whatis
bt continue exit l pp run unalias where
Miscellaneous help topics:
==========================
exec pdb
Thí dụ
Trong khi làm việc với trình gỡ lỗi Python, chúng ta có thể đặt điểm ngắt ở bất kỳ đâu trong tập lệnh bằng cách sử dụng các dòng sau:
import pdb;
pdb.set_trace()
Sau khi thiết lập điểm ngắt, chúng ta có thể chạy script bình thường. Tập lệnh sẽ thực thi cho đến một thời điểm nhất định; cho đến khi một dòng đã được thiết lập. Hãy xem xét ví dụ sau, nơi chúng tôi sẽ chạy tập lệnh bằng cách sử dụng các dòng được đề cập ở trên tại các vị trí khác nhau trong tập lệnh -
import pdb;
a = "aaa"
pdb.set_trace()
b = "bbb"
c = "ccc"
final = a + b + c
print (final)
Khi tập lệnh trên được chạy, nó sẽ thực thi chương trình cho đến khi a = “aaa”, chúng ta có thể kiểm tra điều này trong kết quả sau.
Đầu ra
--Return--
> <ipython-input-7-8a7d1b5cc854>(3)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
*** NameError: name 'b' is not defined
(Pdb) p c
*** NameError: name 'c' is not defined
Sau khi sử dụng lệnh 'p (print)' trong pdb, tập lệnh này chỉ in 'aaa'. Tiếp theo là lỗi vì chúng tôi đã đặt điểm ngắt cho đến a = "aaa".
Tương tự, chúng ta có thể chạy script bằng cách thay đổi các điểm ngắt và thấy sự khác biệt trong đầu ra -
import pdb
a = "aaa"
b = "bbb"
c = "ccc"
pdb.set_trace()
final = a + b + c
print (final)
Đầu ra
--Return--
> <ipython-input-9-a59ef5caf723>(5)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
'bbb'
(Pdb) p c
'ccc'
(Pdb) p final
*** NameError: name 'final' is not defined
(Pdb) exit
Trong tập lệnh sau, chúng tôi đang đặt điểm ngắt ở dòng cuối cùng của chương trình:
import pdb
a = "aaa"
b = "bbb"
c = "ccc"
final = a + b + c
pdb.set_trace()
print (final)
Kết quả như sau:
--Return--
> <ipython-input-11-8019b029997d>(6)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
'bbb'
(Pdb) p c
'ccc'
(Pdb) p final
'aaabbbccc'
(Pdb)
Trong chương này, chúng ta sẽ tìm hiểu cách điểm chuẩn và lập hồ sơ giúp giải quyết các vấn đề về hiệu suất.
Giả sử chúng ta đã viết một đoạn mã và nó cũng cho kết quả mong muốn nhưng điều gì sẽ xảy ra nếu chúng ta muốn chạy đoạn mã này nhanh hơn một chút vì nhu cầu đã thay đổi. Trong trường hợp này, chúng ta cần tìm hiểu những phần mã của chúng ta đang làm chậm toàn bộ chương trình. Trong trường hợp này, điểm chuẩn và lập hồ sơ có thể hữu ích.
Điểm chuẩn là gì?
Đo điểm chuẩn nhằm mục đích đánh giá một cái gì đó bằng cách so sánh với một tiêu chuẩn. Tuy nhiên, câu hỏi đặt ra ở đây là điểm chuẩn sẽ là gì và tại sao chúng ta cần nó trong trường hợp lập trình phần mềm. Đo điểm chuẩn cho mã có nghĩa là mã đang thực thi nhanh như thế nào và tắc nghẽn ở đâu. Một lý do chính cho điểm chuẩn là nó tối ưu hóa mã.
Điểm chuẩn hoạt động như thế nào?
Nếu chúng ta nói về hoạt động của điểm chuẩn, chúng ta cần bắt đầu bằng cách đo điểm chuẩn của toàn bộ chương trình như một trạng thái hiện tại, sau đó chúng ta có thể kết hợp các điểm chuẩn vi mô và sau đó phân tách một chương trình thành các chương trình nhỏ hơn. Để tìm ra những điểm nghẽn trong chương trình của chúng tôi và tối ưu hóa nó. Nói cách khác, chúng ta có thể hiểu nó là chia bài toán lớn và hóc búa thành hàng loạt bài toán nhỏ hơn và dễ dàng hơn một chút để tối ưu hóa chúng.
Mô-đun Python để đo điểm chuẩn
Trong Python, chúng tôi có một mô-đun mặc định để đo điểm chuẩn được gọi là timeit. Với sự giúp đỡ củatimeit mô-đun, chúng tôi có thể đo hiệu suất của một đoạn mã Python nhỏ trong chương trình chính của chúng tôi.
Thí dụ
Trong tập lệnh Python sau, chúng tôi đang nhập timeit mô-đun, đo lường thêm thời gian cần thiết để thực thi hai chức năng - functionA và functionB -
import timeit
import time
def functionA():
print("Function A starts the execution:")
print("Function A completes the execution:")
def functionB():
print("Function B starts the execution")
print("Function B completes the execution")
start_time = timeit.default_timer()
functionA()
print(timeit.default_timer() - start_time)
start_time = timeit.default_timer()
functionB()
print(timeit.default_timer() - start_time)
Sau khi chạy đoạn script trên, chúng ta sẽ có được thời gian thực thi của cả 2 hàm như hình bên dưới.
Đầu ra
Function A starts the execution:
Function A completes the execution:
0.0014599495514175942
Function B starts the execution
Function B completes the execution
0.0017024724827479076
Viết bộ đếm thời gian của riêng chúng tôi bằng cách sử dụng chức năng trang trí
Trong Python, chúng ta có thể tạo bộ đếm thời gian của riêng mình, nó sẽ hoạt động giống như timeitmô-đun. Nó có thể được thực hiện với sự trợ giúp củadecoratorchức năng. Sau đây là một ví dụ về bộ hẹn giờ tùy chỉnh -
import random
import time
def timer_func(func):
def function_timer(*args, **kwargs):
start = time.time()
value = func(*args, **kwargs)
end = time.time()
runtime = end - start
msg = "{func} took {time} seconds to complete its execution."
print(msg.format(func = func.__name__,time = runtime))
return value
return function_timer
@timer_func
def Myfunction():
for x in range(5):
sleep_time = random.choice(range(1,3))
time.sleep(sleep_time)
if __name__ == '__main__':
Myfunction()
Tập lệnh python ở trên giúp nhập các mô-đun thời gian ngẫu nhiên. Chúng tôi đã tạo hàm trang trí timer_func (). Điều này có hàm function_timer () bên trong nó. Bây giờ, hàm lồng nhau sẽ lấy thời gian trước khi gọi hàm được truyền vào. Sau đó, nó đợi hàm trả về và lấy thời gian kết thúc. Bằng cách này, cuối cùng chúng ta có thể làm cho tập lệnh python in thời gian thực thi. Tập lệnh sẽ tạo ra kết quả như hình dưới đây.
Đầu ra
Myfunction took 8.000457763671875 seconds to complete its execution.
Hồ sơ là gì?
Đôi khi lập trình viên muốn đo lường một số thuộc tính như việc sử dụng bộ nhớ, độ phức tạp thời gian hoặc sử dụng các lệnh cụ thể về chương trình để đo khả năng thực sự của chương trình đó. Loại đo lường như vậy về chương trình được gọi là lược tả. Hồ sơ sử dụng phân tích chương trình động để thực hiện việc đo lường như vậy.
Trong các phần tiếp theo, chúng ta sẽ tìm hiểu về các Mô-đun Python khác nhau để lập hồ sơ.
cProfile - mô-đun có sẵn
cProfilelà một mô-đun tích hợp sẵn trong Python để lập hồ sơ. Mô-đun là một phần mở rộng C với chi phí hợp lý làm cho nó phù hợp để lập hồ sơ các chương trình chạy dài. Sau khi chạy, nó ghi lại tất cả các chức năng và thời gian thực thi. Nó rất mạnh mẽ nhưng đôi khi hơi khó giải thích và hành động. Trong ví dụ sau, chúng tôi đang sử dụng cProfile trên đoạn mã bên dưới:
Thí dụ
def increment_global():
global x
x += 1
def taskofThread(lock):
for _ in range(50000):
lock.acquire()
increment_global()
lock.release()
def main():
global x
x = 0
lock = threading.Lock()
t1 = threading.Thread(target=taskofThread, args=(lock,))
t2 = threading.Thread(target= taskofThread, args=(lock,))
t1.start()
t2.start()
t1.join()
t2.join()
if __name__ == "__main__":
for i in range(5):
main()
print("x = {1} after Iteration {0}".format(i,x))
Đoạn mã trên được lưu trong thread_increment.pytập tin. Bây giờ, hãy thực thi mã với cProfile trên dòng lệnh như sau:
(base) D:\ProgramData>python -m cProfile thread_increment.py
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4
3577 function calls (3522 primitive calls) in 1.688 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:103(release)
5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:143(__init__)
5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:147(__enter__)
… … … …
Từ kết quả trên, rõ ràng là cProfile in ra tất cả 3577 hàm được gọi, với thời gian dành cho mỗi và số lần chúng đã được gọi. Tiếp theo là các cột chúng tôi có trong đầu ra -
ncalls - Đó là số lượng cuộc gọi được thực hiện.
tottime - Là tổng thời gian dành cho một hàm đã cho.
percall - Nó đề cập đến thương số của tổng thời gian chia cho ncalls.
cumtime- Đó là thời gian tích lũy dành cho chức năng này và tất cả các chức năng con. Nó thậm chí còn chính xác đối với các hàm đệ quy.
percall - Là thương số của cumtime chia cho các lần gọi nguyên thủy.
filename:lineno(function) - Về cơ bản, nó cung cấp dữ liệu tương ứng của từng chức năng.
Giả sử chúng ta phải tạo một số lượng lớn các luồng cho các tác vụ đa luồng của mình. Nó sẽ đắt nhất về mặt tính toán vì có thể có nhiều vấn đề về hiệu suất, do quá nhiều luồng. Một vấn đề lớn có thể là thông lượng bị hạn chế. Chúng ta có thể giải quyết vấn đề này bằng cách tạo một nhóm các chủ đề. Nhóm luồng có thể được định nghĩa là nhóm các luồng được khởi tạo trước và không hoạt động, sẵn sàng được giao công việc. Tạo nhóm luồng được ưu tiên hơn là tạo luồng mới cho mọi tác vụ khi chúng ta cần thực hiện một số lượng lớn tác vụ. Nhóm luồng có thể quản lý việc thực thi đồng thời số lượng lớn các luồng như sau:
Nếu một luồng trong nhóm luồng hoàn thành việc thực thi thì luồng đó có thể được sử dụng lại.
Nếu một luồng bị kết thúc, một luồng khác sẽ được tạo để thay thế luồng đó.
Mô-đun Python - Concurrent.futures
Thư viện chuẩn Python bao gồm concurrent.futuresmô-đun. Mô-đun này đã được thêm vào trong Python 3.2 để cung cấp cho các nhà phát triển một giao diện cấp cao để khởi chạy các tác vụ không đồng bộ. Nó là một lớp trừu tượng trên đầu các mô-đun phân luồng và đa xử lý của Python để cung cấp giao diện chạy các tác vụ bằng cách sử dụng nhóm luồng hoặc quy trình.
Trong các phần tiếp theo, chúng ta sẽ tìm hiểu về các lớp khác nhau của mô-đun concurrent.futures.
Lớp thừa hành viên
Executorlà một lớp trừu tượng của concurrent.futuresMô-đun Python. Nó không thể được sử dụng trực tiếp và chúng ta cần sử dụng một trong các lớp con bê tông sau:
- ThreadPoolExecutor
- ProcessPoolExecutor
ThreadPoolExecutor - Một lớp con Concrete
Nó là một trong những lớp con cụ thể của lớp Executor. Lớp con sử dụng đa luồng và chúng tôi nhận được một nhóm luồng để gửi các tác vụ. Nhóm này chỉ định các nhiệm vụ cho các luồng có sẵn và lên lịch chúng chạy.
Làm thế nào để tạo một ThreadPoolExecutor?
Với sự giúp đỡ của concurrent.futures mô-đun và lớp con cụ thể của nó Executor, chúng ta có thể dễ dàng tạo một nhóm các chủ đề. Đối với điều này, chúng ta cần xây dựng mộtThreadPoolExecutorvới số lượng chủ đề chúng tôi muốn trong nhóm. Theo mặc định, con số là 5. Sau đó, chúng tôi có thể gửi một nhiệm vụ đến nhóm luồng. Khi nào chúng tasubmit() một nhiệm vụ, chúng tôi nhận lại một Future. Đối tượng Tương lai có một phương thức được gọi làdone(), cho biết liệu tương lai có được giải quyết hay không. Với điều này, một giá trị đã được đặt cho đối tượng tương lai cụ thể đó. Khi một tác vụ kết thúc, trình thực thi nhóm luồng sẽ đặt giá trị cho đối tượng tương lai.
Thí dụ
from concurrent.futures import ThreadPoolExecutor
from time import sleep
def task(message):
sleep(2)
return message
def main():
executor = ThreadPoolExecutor(5)
future = executor.submit(task, ("Completed"))
print(future.done())
sleep(2)
print(future.done())
print(future.result())
if __name__ == '__main__':
main()
Đầu ra
False
True
Completed
Trong ví dụ trên, ThreadPoolExecutorđã được xây dựng với 5 chủ đề. Sau đó, một tác vụ, sẽ đợi trong 2 giây trước khi đưa ra thông báo, sẽ được gửi cho người thực thi nhóm luồng. Như đã thấy từ đầu ra, nhiệm vụ không hoàn thành cho đến 2 giây, vì vậy cuộc gọi đầu tiên đếndone()sẽ trả về Sai. Sau 2 giây, nhiệm vụ được hoàn thành và chúng tôi nhận được kết quả của tương lai bằng cách gọiresult() phương pháp trên đó.
Instantiating ThreadPoolExecutor - Trình quản lý ngữ cảnh
Một cách khác để khởi tạo ThreadPoolExecutorlà với sự trợ giúp của trình quản lý ngữ cảnh. Nó hoạt động tương tự như phương pháp được sử dụng trong ví dụ trên. Ưu điểm chính của việc sử dụng trình quản lý ngữ cảnh là nó trông tốt về mặt cú pháp. Việc khởi tạo có thể được thực hiện với sự trợ giúp của đoạn mã sau:
with ThreadPoolExecutor(max_workers = 5) as executor
Thí dụ
Ví dụ sau được mượn từ tài liệu Python. Trong ví dụ này, trước hếtconcurrent.futuresmô-đun phải được nhập. Sau đó, một hàm có tênload_url()được tạo sẽ tải url được yêu cầu. Sau đó, hàm tạoThreadPoolExecutorvới 5 chủ đề trong hồ bơi. CácThreadPoolExecutorđã được sử dụng làm trình quản lý ngữ cảnh. Chúng ta có thể nhận được kết quả của tương lai bằng cách gọiresult() phương pháp trên đó.
import concurrent.futures
import urllib.request
URLS = ['http://www.foxnews.com/',
'http://www.cnn.com/',
'http://europe.wsj.com/',
'http://www.bbc.co.uk/',
'http://some-made-up-domain.com/']
def load_url(url, timeout):
with urllib.request.urlopen(url, timeout = timeout) as conn:
return conn.read()
with concurrent.futures.ThreadPoolExecutor(max_workers = 5) as executor:
future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
except Exception as exc:
print('%r generated an exception: %s' % (url, exc))
else:
print('%r page is %d bytes' % (url, len(data)))
Đầu ra
Sau đây sẽ là đầu ra của tập lệnh Python ở trên:
'http://some-made-up-domain.com/' generated an exception: <urlopen error [Errno 11004] getaddrinfo failed>
'http://www.foxnews.com/' page is 229313 bytes
'http://www.cnn.com/' page is 168933 bytes
'http://www.bbc.co.uk/' page is 283893 bytes
'http://europe.wsj.com/' page is 938109 bytes
Sử dụng hàm Executor.map ()
Con trăn map()chức năng được sử dụng rộng rãi trong một số nhiệm vụ. Một trong những nhiệm vụ như vậy là áp dụng một hàm nhất định cho mọi phần tử trong các vòng lặp. Tương tự, chúng ta có thể ánh xạ tất cả các phần tử của một trình vòng lặp tới một hàm và gửi chúng dưới dạng các công việc độc lậpThreadPoolExecutor. Hãy xem xét ví dụ sau về tập lệnh Python để hiểu cách hoạt động của hàm.
Thí dụ
Trong ví dụ dưới đây, hàm bản đồ được sử dụng để áp dụng square() hàm cho mọi giá trị trong mảng giá trị.
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import as_completed
values = [2,3,4,5]
def square(n):
return n * n
def main():
with ThreadPoolExecutor(max_workers = 3) as executor:
results = executor.map(square, values)
for result in results:
print(result)
if __name__ == '__main__':
main()
Đầu ra
Tập lệnh Python ở trên tạo ra kết quả sau:
4
9
16
25
Nhóm quy trình có thể được tạo và sử dụng giống như cách chúng ta đã tạo và sử dụng nhóm quy trình. Nhóm quy trình có thể được định nghĩa là nhóm các quy trình khởi tạo trước và không hoạt động, sẵn sàng để được giao công việc. Tạo nhóm quy trình được ưu tiên hơn là tạo quy trình mới cho mọi tác vụ khi chúng ta cần thực hiện một số lượng lớn tác vụ.
Mô-đun Python - Concurrent.futures
Thư viện chuẩn Python có một mô-đun được gọi là concurrent.futures. Mô-đun này đã được thêm vào trong Python 3.2 để cung cấp cho các nhà phát triển một giao diện cấp cao để khởi chạy các tác vụ không đồng bộ. Nó là một lớp trừu tượng trên đầu các mô-đun phân luồng và đa xử lý của Python để cung cấp giao diện chạy các tác vụ bằng cách sử dụng nhóm luồng hoặc quy trình.
Trong các phần tiếp theo, chúng ta sẽ xem xét các lớp con khác nhau của mô-đun concurrent.futures.
Lớp thừa hành viên
Executor là một lớp trừu tượng của concurrent.futuresMô-đun Python. Nó không thể được sử dụng trực tiếp và chúng ta cần sử dụng một trong các lớp con bê tông sau:
- ThreadPoolExecutor
- ProcessPoolExecutor
ProcessPoolExecutor - Một lớp con cụ thể
Nó là một trong những lớp con cụ thể của lớp Executor. Nó sử dụng đa xử lý và chúng tôi nhận được một nhóm các quy trình để gửi các nhiệm vụ. Nhóm này chỉ định nhiệm vụ cho các quy trình có sẵn và lên lịch để chúng chạy.
Làm cách nào để tạo ProcessPoolExecutor?
Với sự giúp đỡ của concurrent.futures mô-đun và lớp con cụ thể của nó Executor, chúng ta có thể dễ dàng tạo một nhóm quy trình. Đối với điều này, chúng ta cần xây dựng mộtProcessPoolExecutorvới số lượng quy trình chúng tôi muốn trong nhóm. Theo mặc định, số là 5. Tiếp theo là gửi một nhiệm vụ đến nhóm quy trình.
Thí dụ
Bây giờ chúng ta sẽ xem xét cùng một ví dụ mà chúng ta đã sử dụng khi tạo nhóm luồng, điểm khác biệt duy nhất là bây giờ chúng ta sẽ sử dụng ProcessPoolExecutor thay vì ThreadPoolExecutor .
from concurrent.futures import ProcessPoolExecutor
from time import sleep
def task(message):
sleep(2)
return message
def main():
executor = ProcessPoolExecutor(5)
future = executor.submit(task, ("Completed"))
print(future.done())
sleep(2)
print(future.done())
print(future.result())
if __name__ == '__main__':
main()
Đầu ra
False
False
Completed
Trong ví dụ trên, một Quy trìnhPoolExecutorđã được xây dựng với 5 chủ đề. Sau đó, một nhiệm vụ, sẽ đợi trong 2 giây trước khi đưa ra thông báo, sẽ được gửi cho người thực thi nhóm tiến trình. Như đã thấy từ đầu ra, nhiệm vụ không hoàn thành cho đến 2 giây, vì vậy cuộc gọi đầu tiên đếndone()sẽ trả về Sai. Sau 2 giây, nhiệm vụ được hoàn thành và chúng tôi nhận được kết quả của tương lai bằng cách gọiresult() phương pháp trên đó.
Instantiating ProcessPoolExecutor - Trình quản lý ngữ cảnh
Một cách khác để khởi tạo ProcessPoolExecutor là với sự trợ giúp của trình quản lý ngữ cảnh. Nó hoạt động tương tự như phương pháp được sử dụng trong ví dụ trên. Ưu điểm chính của việc sử dụng trình quản lý ngữ cảnh là nó trông tốt về mặt cú pháp. Việc khởi tạo có thể được thực hiện với sự trợ giúp của đoạn mã sau:
with ProcessPoolExecutor(max_workers = 5) as executor
Thí dụ
Để hiểu rõ hơn, chúng tôi đang lấy ví dụ tương tự như được sử dụng trong khi tạo nhóm luồng. Trong ví dụ này, chúng ta cần bắt đầu bằng cách nhậpconcurrent.futuresmô-đun. Sau đó, một hàm có tênload_url()được tạo sẽ tải url được yêu cầu. CácProcessPoolExecutorsau đó được tạo với 5 số luồng trong nhóm. Quá trìnhPoolExecutorđã được sử dụng làm trình quản lý ngữ cảnh. Chúng ta có thể nhận được kết quả của tương lai bằng cách gọiresult() phương pháp trên đó.
import concurrent.futures
from concurrent.futures import ProcessPoolExecutor
import urllib.request
URLS = ['http://www.foxnews.com/',
'http://www.cnn.com/',
'http://europe.wsj.com/',
'http://www.bbc.co.uk/',
'http://some-made-up-domain.com/']
def load_url(url, timeout):
with urllib.request.urlopen(url, timeout = timeout) as conn:
return conn.read()
def main():
with concurrent.futures.ProcessPoolExecutor(max_workers=5) as executor:
future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
except Exception as exc:
print('%r generated an exception: %s' % (url, exc))
else:
print('%r page is %d bytes' % (url, len(data)))
if __name__ == '__main__':
main()
Đầu ra
Tập lệnh Python ở trên sẽ tạo ra kết quả sau:
'http://some-made-up-domain.com/' generated an exception: <urlopen error [Errno 11004] getaddrinfo failed>
'http://www.foxnews.com/' page is 229476 bytes
'http://www.cnn.com/' page is 165323 bytes
'http://www.bbc.co.uk/' page is 284981 bytes
'http://europe.wsj.com/' page is 967575 bytes
Sử dụng hàm Executor.map ()
Con trăn map()chức năng được sử dụng rộng rãi để thực hiện một số nhiệm vụ. Một trong những nhiệm vụ như vậy là áp dụng một hàm nhất định cho mọi phần tử trong các vòng lặp. Tương tự, chúng ta có thể ánh xạ tất cả các phần tử của một trình vòng lặp tới một hàm và gửi chúng dưới dạng các công việc độc lập choProcessPoolExecutor. Hãy xem xét ví dụ sau về tập lệnh Python để hiểu điều này.
Thí dụ
Chúng tôi sẽ xem xét cùng một ví dụ mà chúng tôi đã sử dụng khi tạo nhóm luồng bằng cách sử dụng Executor.map()chức năng. Trong ví dụ dưới đây, hàm bản đồ được sử dụng để áp dụngsquare() hàm cho mọi giá trị trong mảng giá trị.
from concurrent.futures import ProcessPoolExecutor
from concurrent.futures import as_completed
values = [2,3,4,5]
def square(n):
return n * n
def main():
with ProcessPoolExecutor(max_workers = 3) as executor:
results = executor.map(square, values)
for result in results:
print(result)
if __name__ == '__main__':
main()
Đầu ra
Tập lệnh Python ở trên sẽ tạo ra kết quả sau
4
9
16
25
Khi nào sử dụng ProcessPoolExecutor và ThreadPoolExecutor?
Bây giờ chúng ta đã nghiên cứu về cả hai lớp Executor - ThreadPoolExecutor và ProcessPoolExecutor, chúng ta cần biết khi nào sử dụng trình thực thi nào. Chúng ta cần chọn ProcessPoolExecutor trong trường hợp khối lượng công việc ràng buộc CPU và ThreadPoolExecutor trong trường hợp khối lượng công việc ràng buộc I / O.
Nếu chúng ta sử dụng ProcessPoolExecutor, thì chúng ta không cần phải lo lắng về GIL vì nó sử dụng đa xử lý. Hơn nữa, thời gian thực hiện sẽ ít hơn khi so sánh vớiThreadPoolExecution. Hãy xem xét ví dụ tập lệnh Python sau để hiểu điều này.
Thí dụ
import time
import concurrent.futures
value = [8000000, 7000000]
def counting(n):
start = time.time()
while n > 0:
n -= 1
return time.time() - start
def main():
start = time.time()
with concurrent.futures.ProcessPoolExecutor() as executor:
for number, time_taken in zip(value, executor.map(counting, value)):
print('Start: {} Time taken: {}'.format(number, time_taken))
print('Total time taken: {}'.format(time.time() - start))
if __name__ == '__main__':
main()
Đầu ra
Start: 8000000 Time taken: 1.5509998798370361
Start: 7000000 Time taken: 1.3259999752044678
Total time taken: 2.0840001106262207
Example- Python script with ThreadPoolExecutor:
import time
import concurrent.futures
value = [8000000, 7000000]
def counting(n):
start = time.time()
while n > 0:
n -= 1
return time.time() - start
def main():
start = time.time()
with concurrent.futures.ThreadPoolExecutor() as executor:
for number, time_taken in zip(value, executor.map(counting, value)):
print('Start: {} Time taken: {}'.format(number, time_taken))
print('Total time taken: {}'.format(time.time() - start))
if __name__ == '__main__':
main()
Đầu ra
Start: 8000000 Time taken: 3.8420000076293945
Start: 7000000 Time taken: 3.6010000705718994
Total time taken: 3.8480000495910645
Từ kết quả đầu ra của cả hai chương trình trên, chúng ta có thể thấy sự khác biệt về thời gian thực thi khi sử dụng ProcessPoolExecutor và ThreadPoolExecutor.
Trong chương này, chúng tôi sẽ tập trung nhiều hơn vào sự so sánh giữa đa xử lý và đa luồng.
Đa xử lý
Nó là việc sử dụng hai hoặc nhiều đơn vị CPU trong một hệ thống máy tính. Đây là cách tốt nhất để khai thác toàn bộ tiềm năng từ phần cứng của chúng tôi bằng cách sử dụng đầy đủ số lõi CPU có sẵn trong hệ thống máy tính của chúng tôi.
Đa luồng
Nó là khả năng của CPU để quản lý việc sử dụng hệ điều hành bằng cách thực thi nhiều luồng đồng thời. Ý tưởng chính của đa luồng là đạt được sự song song bằng cách chia một quy trình thành nhiều luồng.
Bảng sau đây cho thấy một số khác biệt quan trọng giữa chúng:
Đa xử lý | Đa chương trình |
---|---|
Đa xử lý đề cập đến việc xử lý nhiều quá trình cùng một lúc bởi nhiều CPU. | Đa chương trình lưu giữ một số chương trình trong bộ nhớ chính cùng một lúc và thực thi chúng đồng thời bằng cách sử dụng một CPU. |
Nó sử dụng nhiều CPU. | Nó sử dụng một CPU. |
Nó cho phép xử lý song song. | Quá trình chuyển đổi ngữ cảnh diễn ra. |
Ít thời gian hơn để xử lý công việc. | Thêm thời gian để xử lý công việc. |
Nó tạo điều kiện cho việc sử dụng hiệu quả các thiết bị của hệ thống máy tính. | Kém hiệu quả hơn đa xử lý. |
Thường đắt hơn. | Hệ thống như vậy ít tốn kém hơn. |
Loại bỏ tác động của khóa thông dịch toàn cầu (GIL)
Trong khi làm việc với các ứng dụng đồng thời, có một hạn chế trong Python được gọi là GIL (Global Interpreter Lock). GIL không bao giờ cho phép chúng tôi sử dụng nhiều lõi của CPU và do đó chúng tôi có thể nói rằng không có luồng thực sự trong Python. GIL là mutex - khóa loại trừ lẫn nhau, giúp mọi thứ an toàn. Nói cách khác, chúng ta có thể nói rằng GIL ngăn nhiều luồng thực thi mã Python song song. Khóa chỉ có thể được giữ bởi một luồng tại một thời điểm và nếu chúng ta muốn thực thi một luồng thì trước tiên nó phải có được khóa.
Với việc sử dụng đa xử lý, chúng ta có thể bỏ qua hạn chế do GIL gây ra một cách hiệu quả -
Bằng cách sử dụng đa xử lý, chúng tôi đang sử dụng khả năng của nhiều quy trình và do đó chúng tôi đang sử dụng nhiều phiên bản GIL.
Do đó, không có giới hạn nào về việc thực thi bytecode của một luồng trong các chương trình của chúng tôi tại bất kỳ thời điểm nào.
Các quy trình bắt đầu bằng Python
Ba phương pháp sau có thể được sử dụng để bắt đầu một quy trình bằng Python trong mô-đun đa xử lý:
- Fork
- Spawn
- Forkserver
Tạo quy trình với Fork
Lệnh Fork là một lệnh tiêu chuẩn được tìm thấy trong UNIX. Nó được sử dụng để tạo các quy trình mới được gọi là quy trình con. Tiến trình con này chạy đồng thời với tiến trình được gọi là tiến trình mẹ. Các quy trình con này cũng giống với các quy trình mẹ của chúng và kế thừa tất cả các tài nguyên có sẵn cho quy trình mẹ. Các lệnh gọi hệ thống sau được sử dụng trong khi tạo quy trình với Fork:
fork()- Nó là một lời gọi hệ thống thường được thực hiện trong nhân. Nó được sử dụng để tạo một bản sao của quy trình. P>
getpid() - Lệnh gọi hệ thống này trả về ID tiến trình (PID) của tiến trình gọi.
Thí dụ
Ví dụ tập lệnh Python sau đây sẽ giúp bạn hiểu rõ cách tạo một quy trình con mới và lấy PID của các quy trình con và quy trình mẹ -
import os
def child():
n = os.fork()
if n > 0:
print("PID of Parent process is : ", os.getpid())
else:
print("PID of Child process is : ", os.getpid())
child()
Đầu ra
PID of Parent process is : 25989
PID of Child process is : 25990
Tạo quy trình với Spawn
Spawn có nghĩa là bắt đầu một cái gì đó mới. Do đó, sinh ra một quy trình có nghĩa là một quy trình mẹ tạo ra một quy trình mới. Tiến trình mẹ tiếp tục thực thi của nó một cách không đồng bộ hoặc đợi cho đến khi tiến trình con kết thúc quá trình thực thi của nó. Làm theo các bước sau để tạo ra một quy trình -
Nhập mô-đun đa xử lý.
Tạo tiến trình đối tượng.
Bắt đầu hoạt động của quy trình bằng cách gọi start() phương pháp.
Chờ cho đến khi quá trình kết thúc và thoát ra bằng cách gọi join() phương pháp.
Thí dụ
Ví dụ sau về tập lệnh Python giúp tạo ra ba quy trình
import multiprocessing
def spawn_process(i):
print ('This is process: %s' %i)
return
if __name__ == '__main__':
Process_jobs = []
for i in range(3):
p = multiprocessing.Process(target = spawn_process, args = (i,))
Process_jobs.append(p)
p.start()
p.join()
Đầu ra
This is process: 0
This is process: 1
This is process: 2
Tạo quy trình với Forkserver
Cơ chế Forkserver chỉ khả dụng trên các nền tảng UNIX được chọn hỗ trợ chuyển các bộ mô tả tệp qua Unix Pipes. Hãy xem xét các điểm sau để hiểu hoạt động của cơ chế Forkserver:
Máy chủ được khởi tạo khi sử dụng cơ chế Forkserver để bắt đầu quy trình mới.
Sau đó máy chủ nhận lệnh và xử lý tất cả các yêu cầu tạo quy trình mới.
Để tạo một quy trình mới, chương trình python của chúng tôi sẽ gửi một yêu cầu đến Forkserver và nó sẽ tạo một quy trình cho chúng tôi.
Cuối cùng, chúng tôi có thể sử dụng quy trình được tạo mới này trong các chương trình của mình.
Quy trình daemon bằng Python
Python multiprocessingmô-đun cho phép chúng tôi có các quy trình daemon thông qua tùy chọn daemonic của nó. Các quy trình daemon hoặc các quy trình đang chạy trong nền tuân theo khái niệm tương tự như các luồng daemon. Để thực hiện quá trình trong nền, chúng ta cần đặt cờ daemonic thành true. Tiến trình daemon sẽ tiếp tục chạy miễn là tiến trình chính đang thực thi và nó sẽ kết thúc sau khi kết thúc quá trình thực thi hoặc khi chương trình chính bị giết.
Thí dụ
Ở đây, chúng ta đang sử dụng ví dụ tương tự như được sử dụng trong các luồng daemon. Sự khác biệt duy nhất là sự thay đổi của mô-đun từmultithreading đến multiprocessingvà đặt cờ daemonic thành true. Tuy nhiên, sẽ có một sự thay đổi trong đầu ra như hình dưới đây -
import multiprocessing
import time
def nondaemonProcess():
print("starting my Process")
time.sleep(8)
print("ending my Process")
def daemonProcess():
while True:
print("Hello")
time.sleep(2)
if __name__ == '__main__':
nondaemonProcess = multiprocessing.Process(target = nondaemonProcess)
daemonProcess = multiprocessing.Process(target = daemonProcess)
daemonProcess.daemon = True
nondaemonProcess.daemon = False
daemonProcess.start()
nondaemonProcess.start()
Đầu ra
starting my Process
ending my Process
Đầu ra khác khi so sánh với đầu ra được tạo bởi các luồng daemon, vì quy trình ở chế độ không có daemon có đầu ra. Do đó, quá trình daemonic tự động kết thúc sau khi các chương trình chính kết thúc để tránh các quá trình đang chạy liên tục.
Kết thúc quy trình bằng Python
Chúng tôi có thể hủy hoặc chấm dứt một quy trình ngay lập tức bằng cách sử dụng terminate()phương pháp. Chúng tôi sẽ sử dụng phương thức này để kết thúc tiến trình con, tiến trình đã được tạo với sự trợ giúp của hàm, ngay trước khi hoàn thành việc thực thi.
Thí dụ
import multiprocessing
import time
def Child_process():
print ('Starting function')
time.sleep(5)
print ('Finished function')
P = multiprocessing.Process(target = Child_process)
P.start()
print("My Process has terminated, terminating main thread")
print("Terminating Child Process")
P.terminate()
print("Child Process successfully terminated")
Đầu ra
My Process has terminated, terminating main thread
Terminating Child Process
Child Process successfully terminated
Kết quả đầu ra cho thấy chương trình kết thúc trước khi thực thi tiến trình con đã được tạo với sự trợ giúp của hàm Child_process (). Điều này ngụ ý rằng quá trình con đã được kết thúc thành công.
Xác định quy trình hiện tại bằng Python
Mọi quy trình trong hệ điều hành đều có nhận dạng quy trình được gọi là PID. Trong Python, chúng ta có thể tìm ra PID của quy trình hiện tại với sự trợ giúp của lệnh sau:
import multiprocessing
print(multiprocessing.current_process().pid)
Thí dụ
Ví dụ sau về tập lệnh Python giúp tìm ra PID của quy trình chính cũng như PID của quy trình con:
import multiprocessing
import time
def Child_process():
print("PID of Child Process is: {}".format(multiprocessing.current_process().pid))
print("PID of Main process is: {}".format(multiprocessing.current_process().pid))
P = multiprocessing.Process(target=Child_process)
P.start()
P.join()
Đầu ra
PID of Main process is: 9401
PID of Child Process is: 9402
Sử dụng một quy trình trong lớp con
Chúng ta có thể tạo các chuỗi bằng cách phân loại threading.Threadlớp học. Ngoài ra, chúng tôi cũng có thể tạo quy trình bằng cách phân loạimultiprocessing.Processlớp học. Để sử dụng một quy trình trong lớp con, chúng ta cần xem xét các điểm sau:
Chúng ta cần xác định một lớp con mới của Process lớp học.
Chúng tôi cần ghi đè _init_(self [,args] ) lớp học.
Chúng ta cần ghi đè run(self [,args] ) phương pháp để thực hiện những gì Process
Chúng ta cần bắt đầu quá trình này bằng cách gọistart() phương pháp.
Thí dụ
import multiprocessing
class MyProcess(multiprocessing.Process):
def run(self):
print ('called run method in process: %s' %self.name)
return
if __name__ == '__main__':
jobs = []
for i in range(5):
P = MyProcess()
jobs.append(P)
P.start()
P.join()
Đầu ra
called run method in process: MyProcess-1
called run method in process: MyProcess-2
called run method in process: MyProcess-3
called run method in process: MyProcess-4
called run method in process: MyProcess-5
Mô-đun đa xử lý Python - Lớp nhóm
Nếu chúng ta nói về song song đơn giản processingcác tác vụ trong các ứng dụng Python của chúng tôi, sau đó mô-đun đa xử lý cung cấp cho chúng tôi lớp Pool. Các phương pháp sau củaPool lớp có thể được sử dụng để tăng số lượng các quy trình con trong chương trình chính của chúng tôi
phương thức apply ()
Phương pháp này tương tự như.submit()phương pháp của .ThreadPoolExecutor.Nó chặn cho đến khi kết quả sẵn sàng.
phương thức apply_async ()
Khi chúng ta cần thực hiện song song các tác vụ của mình thì chúng ta cần sử dụngapply_async()phương pháp để gửi nhiệm vụ cho nhóm. Đây là một hoạt động không đồng bộ sẽ không khóa luồng chính cho đến khi tất cả các quy trình con được thực thi.
phương thức map ()
Giống như apply(), nó cũng chặn cho đến khi kết quả sẵn sàng. Nó tương đương với cài sẵnmap() chức năng chia dữ liệu có thể lặp lại thành một số phần và gửi đến nhóm quy trình dưới dạng các tác vụ riêng biệt.
phương thức map_async ()
Nó là một biến thể của map() phương pháp như apply_async() là với apply()phương pháp. Nó trả về một đối tượng kết quả. Khi kết quả sẵn sàng, một hàm có thể gọi được áp dụng cho nó. Có thể gọi phải được hoàn thành ngay lập tức; nếu không, luồng xử lý kết quả sẽ bị chặn.
Thí dụ
Ví dụ sau sẽ giúp bạn triển khai một nhóm quy trình để thực hiện thực thi song song. Một phép tính đơn giản về bình phương số đã được thực hiện bằng cách áp dụngsquare() chức năng thông qua multiprocessing.Poolphương pháp. Sau đópool.map() đã được sử dụng để gửi 5, vì đầu vào là danh sách các số nguyên từ 0 đến 4. Kết quả sẽ được lưu trữ trong p_outputs và nó được in.
def square(n):
result = n*n
return result
if __name__ == '__main__':
inputs = list(range(5))
p = multiprocessing.Pool(processes = 4)
p_outputs = pool.map(function_square, inputs)
p.close()
p.join()
print ('Pool :', p_outputs)
Đầu ra
Pool : [0, 1, 4, 9, 16]
Thông tin liên lạc giữa các quá trình có nghĩa là trao đổi dữ liệu giữa các quá trình. Cần phải trao đổi dữ liệu giữa các quy trình để phát triển ứng dụng song song. Sơ đồ sau cho thấy các cơ chế giao tiếp khác nhau để đồng bộ hóa giữa nhiều quy trình con -
Các cơ chế giao tiếp khác nhau
Trong phần này, chúng ta sẽ tìm hiểu về các cơ chế giao tiếp khác nhau. Các cơ chế được mô tả dưới đây:
Hàng đợi
Hàng đợi có thể được sử dụng với các chương trình đa quy trình. Lớp hàng đợi củamultiprocessing mô-đun tương tự như Queue.Queuelớp học. Do đó, có thể sử dụng cùng một API.Multiprocessing.Queue cung cấp cho chúng ta một cơ chế giao tiếp FIFO (nhập trước xuất trước) an toàn theo luồng và quy trình giữa các quy trình.
Thí dụ
Sau đây là một ví dụ đơn giản được lấy từ tài liệu chính thức của python về đa xử lý để hiểu khái niệm về lớp Hàng đợi của đa xử lý.
from multiprocessing import Process, Queue
import queue
import random
def f(q):
q.put([42, None, 'hello'])
def main():
q = Queue()
p = Process(target = f, args = (q,))
p.start()
print (q.get())
if __name__ == '__main__':
main()
Đầu ra
[42, None, 'hello']
Đường ống
Nó là một cấu trúc dữ liệu, được sử dụng để giao tiếp giữa các quy trình trong các chương trình đa quy trình. Hàm Pipe () trả về một cặp đối tượng kết nối được kết nối bằng một đường ống theo mặc định là song công (hai chiều). Nó hoạt động theo cách sau:
Nó trả về một cặp đối tượng kết nối đại diện cho hai đầu của đường ống.
Mọi đối tượng có hai phương thức - send() và recv(), để giao tiếp giữa các quy trình.
Thí dụ
Sau đây là một ví dụ đơn giản được lấy từ tài liệu chính thức của python về đa xử lý để hiểu khái niệm về Pipe() chức năng của đa xử lý.
from multiprocessing import Process, Pipe
def f(conn):
conn.send([42, None, 'hello'])
conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target = f, args = (child_conn,))
p.start()
print (parent_conn.recv())
p.join()
Đầu ra
[42, None, 'hello']
Giám đốc
Trình quản lý là một lớp mô-đun đa xử lý cung cấp cách điều phối thông tin được chia sẻ giữa tất cả người dùng của nó. Đối tượng người quản lý kiểm soát một quy trình máy chủ, quy trình này quản lý các đối tượng được chia sẻ và cho phép các quy trình khác thao tác chúng. Nói cách khác, người quản lý cung cấp một cách để tạo ra dữ liệu có thể được chia sẻ giữa các quy trình khác nhau. Sau đây là các thuộc tính khác nhau của đối tượng manager:
Thuộc tính chính của manager là kiểm soát một tiến trình máy chủ, quản lý các đối tượng được chia sẻ.
Một thuộc tính quan trọng khác là cập nhật tất cả các đối tượng được chia sẻ khi bất kỳ quá trình nào sửa đổi nó.
Thí dụ
Dưới đây là một ví dụ sử dụng đối tượng manager để tạo một bản ghi danh sách trong quy trình máy chủ và sau đó thêm một bản ghi mới trong danh sách đó.
import multiprocessing
def print_records(records):
for record in records:
print("Name: {0}\nScore: {1}\n".format(record[0], record[1]))
def insert_record(record, records):
records.append(record)
print("A New record is added\n")
if __name__ == '__main__':
with multiprocessing.Manager() as manager:
records = manager.list([('Computers', 1), ('Histoty', 5), ('Hindi',9)])
new_record = ('English', 3)
p1 = multiprocessing.Process(target = insert_record, args = (new_record, records))
p2 = multiprocessing.Process(target = print_records, args = (records,))
p1.start()
p1.join()
p2.start()
p2.join()
Đầu ra
A New record is added
Name: Computers
Score: 1
Name: Histoty
Score: 5
Name: Hindi
Score: 9
Name: English
Score: 3
Khái niệm về Không gian tên trong Trình quản lý
Lớp quản lý đi kèm với khái niệm không gian tên, đây là một phương pháp nhanh chóng để chia sẻ một số thuộc tính trên nhiều quy trình. Không gian tên không có bất kỳ phương thức công khai nào có thể được gọi, nhưng chúng có các thuộc tính có thể ghi.
Thí dụ
Ví dụ tập lệnh Python sau đây giúp chúng tôi sử dụng không gian tên để chia sẻ dữ liệu giữa quy trình chính và quy trình con -
import multiprocessing
def Mng_NaSp(using_ns):
using_ns.x +=5
using_ns.y *= 10
if __name__ == '__main__':
manager = multiprocessing.Manager()
using_ns = manager.Namespace()
using_ns.x = 1
using_ns.y = 1
print ('before', using_ns)
p = multiprocessing.Process(target = Mng_NaSp, args = (using_ns,))
p.start()
p.join()
print ('after', using_ns)
Đầu ra
before Namespace(x = 1, y = 1)
after Namespace(x = 6, y = 10)
Ctypes-Mảng và Giá trị
Mô-đun đa xử lý cung cấp các đối tượng Mảng và Giá trị để lưu trữ dữ liệu trong bản đồ bộ nhớ dùng chung. Array là một mảng ctypes được cấp phát từ bộ nhớ dùng chung và Value là một đối tượng ctypes được cấp phát từ bộ nhớ dùng chung.
Cùng với, nhập Quy trình, Giá trị, Mảng từ quá trình đa xử lý.
Thí dụ
Sau script Python là một ví dụ được lấy từ tài liệu python để sử dụng Ctypes Array và Value để chia sẻ một số dữ liệu giữa các quy trình.
def f(n, a):
n.value = 3.1415927
for i in range(len(a)):
a[i] = -a[i]
if __name__ == '__main__':
num = Value('d', 0.0)
arr = Array('i', range(10))
p = Process(target = f, args = (num, arr))
p.start()
p.join()
print (num.value)
print (arr[:])
Đầu ra
3.1415927
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
Giao tiếp các quy trình tuần tự (CSP)
CSP được sử dụng để minh họa sự tương tác của hệ thống với các hệ thống khác có mô hình đồng thời. CSP là một khuôn khổ để viết đồng thời hoặc chương trình thông qua việc truyền thông điệp và do đó nó có hiệu quả để mô tả đồng thời.
Thư viện Python - PyCSP
Để triển khai các nguyên thủy cốt lõi được tìm thấy trong CSP, Python có một thư viện được gọi là PyCSP. Nó giữ cho việc triển khai rất ngắn gọn và dễ đọc để có thể hiểu rất dễ dàng. Sau đây là mạng quy trình cơ bản của PyCSP -
Trong mạng quy trình PyCSP ở trên, có hai quy trình - Quy trình 1 và Quy trình 2. Các quy trình này giao tiếp bằng cách truyền thông điệp qua hai kênh - kênh 1 và kênh 2.
Cài đặt PyCSP
Với sự trợ giúp của lệnh sau, chúng ta có thể cài đặt thư viện Python PyCSP -
pip install PyCSP
Thí dụ
Tập lệnh Python sau đây là một ví dụ đơn giản để chạy hai tiến trình song song với nhau. Nó được thực hiện với sự trợ giúp của trăn PyCSP libabary -
from pycsp.parallel import *
import time
@process
def P1():
time.sleep(1)
print('P1 exiting')
@process
def P2():
time.sleep(1)
print('P2 exiting')
def main():
Parallel(P1(), P2())
print('Terminating')
if __name__ == '__main__':
main()
Trong tập lệnh trên, hai chức năng cụ thể là P1 và P2 đã được tạo ra và sau đó được trang trí bằng @process để chuyển đổi chúng thành các quy trình.
Đầu ra
P2 exiting
P1 exiting
Terminating
Lập trình theo hướng sự kiện tập trung vào các sự kiện. Cuối cùng, luồng chương trình phụ thuộc vào các sự kiện. Cho đến nay, chúng ta đang xử lý mô hình thực thi tuần tự hoặc song song nhưng mô hình có khái niệm về lập trình hướng sự kiện được gọi là mô hình không đồng bộ. Lập trình theo hướng sự kiện phụ thuộc vào một vòng lặp sự kiện luôn lắng nghe các sự kiện mới đến. Hoạt động của lập trình hướng sự kiện phụ thuộc vào các sự kiện. Khi một sự kiện lặp lại, thì các sự kiện sẽ quyết định những gì sẽ thực thi và theo thứ tự nào. Lưu đồ sau sẽ giúp bạn hiểu cách hoạt động của điều này -
Mô-đun Python - Asyncio
Mô-đun Asyncio đã được thêm vào trong Python 3.4 và nó cung cấp cơ sở hạ tầng để viết mã đồng thời đơn luồng bằng cách sử dụng đồng quy trình. Sau đây là các khái niệm khác nhau được sử dụng bởi mô-đun Asyncio:
Vòng lặp sự kiện
Vòng lặp sự kiện là một chức năng để xử lý tất cả các sự kiện trong một mã tính toán. Nó hoạt động trong suốt quá trình thực hiện toàn bộ chương trình và theo dõi các sự kiện đến và thực hiện. Mô-đun Asyncio cho phép một vòng lặp sự kiện duy nhất cho mỗi quá trình. Tiếp theo là một số phương pháp được cung cấp bởi mô-đun Asyncio để quản lý một vòng lặp sự kiện -
loop = get_event_loop() - Phương thức này sẽ cung cấp vòng lặp sự kiện cho ngữ cảnh hiện tại.
loop.call_later(time_delay,callback,argument) - Phương thức này sắp xếp cuộc gọi lại sẽ được gọi sau giây time_delay đã cho.
loop.call_soon(callback,argument)- Phương thức này sắp xếp cho một cuộc gọi lại sẽ được gọi càng sớm càng tốt. Lệnh gọi lại được gọi sau khi call_soon () trả về và khi điều khiển quay trở lại vòng lặp sự kiện.
loop.time() - Phương thức này dùng để trả về thời gian hiện tại theo đồng hồ bên trong của vòng lặp sự kiện.
asyncio.set_event_loop() - Phương thức này sẽ thiết lập vòng lặp sự kiện cho ngữ cảnh hiện tại thành vòng lặp.
asyncio.new_event_loop() - Phương thức này sẽ tạo và trả về một đối tượng vòng lặp sự kiện mới.
loop.run_forever() - Phương thức này sẽ chạy cho đến khi phương thức stop () được gọi.
Thí dụ
Ví dụ sau về vòng lặp sự kiện giúp in hello worldbằng cách sử dụng phương thức get_event_loop (). Ví dụ này được lấy từ tài liệu chính thức của Python.
import asyncio
def hello_world(loop):
print('Hello World')
loop.stop()
loop = asyncio.get_event_loop()
loop.call_soon(hello_world, loop)
loop.run_forever()
loop.close()
Đầu ra
Hello World
Hợp đồng tương lai
Điều này tương thích với lớp concurrent.futures.Future đại diện cho một tính toán chưa được hoàn thành. Có những khác biệt sau giữa asyncio.futures.Future và concurrent.futures.Future -
Các phương thức result () và exception () không nhận đối số timeout và đưa ra một ngoại lệ khi tương lai chưa được thực hiện.
Các lệnh gọi lại được đăng ký với add_done_callback () luôn được gọi thông qua call_soon () của vòng lặp sự kiện.
Lớp asyncio.futures.Future không tương thích với các hàm wait () và as_completed () trong gói concurrent.futures.
Thí dụ
Sau đây là một ví dụ sẽ giúp bạn hiểu cách sử dụng lớp asyncio.futures.future.
import asyncio
async def Myoperation(future):
await asyncio.sleep(2)
future.set_result('Future Completed')
loop = asyncio.get_event_loop()
future = asyncio.Future()
asyncio.ensure_future(Myoperation(future))
try:
loop.run_until_complete(future)
print(future.result())
finally:
loop.close()
Đầu ra
Future Completed
Coroutines
Khái niệm về coroutines trong Asyncio tương tự như khái niệm về đối tượng Thread tiêu chuẩn trong mô-đun luồng. Đây là tổng quát của khái niệm chương trình con. Một chương trình điều tra có thể bị tạm dừng trong quá trình thực thi để nó chờ xử lý bên ngoài và quay trở lại từ điểm mà nó đã dừng khi xử lý bên ngoài được thực hiện. Hai cách sau đây giúp chúng tôi triển khai các quy trình điều tra -
async def function ()
Đây là một phương pháp để thực hiện các coroutines trong mô-đun Asyncio. Sau đây là một tập lệnh Python cho cùng một:
import asyncio
async def Myoperation():
print("First Coroutine")
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(Myoperation())
finally:
loop.close()
Đầu ra
First Coroutine
@ asyncio.coroutine decorator
Một phương pháp khác để triển khai các quy trình là sử dụng các bộ tạo với trình trang trí @ asyncio.coroutine. Sau đây là một tập lệnh Python cho cùng một:
import asyncio
@asyncio.coroutine
def Myoperation():
print("First Coroutine")
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(Myoperation())
finally:
loop.close()
Đầu ra
First Coroutine
Nhiệm vụ
Lớp con của mô-đun Asyncio này chịu trách nhiệm thực thi các quy trình trong vòng lặp sự kiện theo cách song song. Sau đây script Python là một ví dụ về xử lý một số tác vụ song song.
import asyncio
import time
async def Task_ex(n):
time.sleep(1)
print("Processing {}".format(n))
async def Generator_task():
for i in range(10):
asyncio.ensure_future(Task_ex(i))
int("Tasks Completed")
asyncio.sleep(2)
loop = asyncio.get_event_loop()
loop.run_until_complete(Generator_task())
loop.close()
Đầu ra
Tasks Completed
Processing 0
Processing 1
Processing 2
Processing 3
Processing 4
Processing 5
Processing 6
Processing 7
Processing 8
Processing 9
Phương tiện giao thông
Mô-đun Asyncio cung cấp các lớp truyền tải để thực hiện các kiểu giao tiếp khác nhau. Các lớp này không an toàn theo luồng và luôn được ghép nối với một thể hiện giao thức sau khi thiết lập kênh giao tiếp.
Sau đây là các loại phương tiện giao thông khác biệt được kế thừa từ BaseTransport:
ReadTransport - Đây là một giao diện cho các phương tiện chỉ đọc.
WriteTransport - Đây là một giao diện cho các phương tiện chỉ ghi.
DatagramTransport - Đây là một giao diện để gửi dữ liệu.
BaseSubprocessTransport - Tương tự với lớp BaseTransport.
Tiếp theo là năm phương thức khác biệt của lớp BaseTransport mà sau đó là tạm thời trên bốn loại truyền tải -
close() - Nó đóng cửa phương tiện giao thông.
is_closing() - Phương thức này sẽ trả về true nếu quá trình vận chuyển đang đóng hoặc đã bị đóng.
get_extra_info(name, default = none) - Điều này sẽ cung cấp cho chúng tôi một số thông tin bổ sung về vận tải.
get_protocol() - Phương thức này sẽ trả về giao thức hiện tại.
Giao thức
Mô-đun Asyncio cung cấp các lớp cơ sở mà bạn có thể phân lớp để triển khai các giao thức mạng của mình. Các lớp đó được sử dụng cùng với các phương tiện giao thông; giao thức phân tích cú pháp dữ liệu đến và yêu cầu ghi dữ liệu đi, trong khi quá trình truyền tải chịu trách nhiệm về I / O thực tế và bộ đệm. Sau đây là ba lớp của Giao thức -
Protocol - Đây là lớp cơ sở để thực hiện các giao thức phân luồng để sử dụng với các phương tiện truyền tải TCP và SSL.
DatagramProtocol - Đây là lớp cơ sở để thực hiện các giao thức datagram để sử dụng với các phương tiện truyền tải UDP ..
SubprocessProtocol - Đây là lớp cơ sở để thực hiện các giao thức giao tiếp với các tiến trình con thông qua một tập hợp các đường ống một chiều.
Lập trình phản ứng là một mô hình lập trình xử lý các luồng dữ liệu và sự lan truyền của sự thay đổi. Có nghĩa là khi một luồng dữ liệu được phát ra bởi một thành phần, sự thay đổi sẽ được truyền đến các thành phần khác bằng thư viện lập trình phản ứng. Sự lan truyền sự thay đổi sẽ tiếp tục cho đến khi nó đến người nhận cuối cùng. Sự khác biệt giữa lập trình hướng sự kiện và lập trình phản ứng là lập trình hướng sự kiện xoay quanh các sự kiện và lập trình phản ứng xoay quanh dữ liệu.
ReactiveX hoặc RX để lập trình phản ứng
ReactiveX hoặc Raective Extension là cách triển khai nổi tiếng nhất của lập trình phản ứng. Hoạt động của ReactiveX phụ thuộc vào hai lớp sau:
Lớp quan sát được
Lớp này là nguồn của luồng dữ liệu hoặc các sự kiện và nó đóng gói dữ liệu đến để dữ liệu có thể được truyền từ luồng này sang luồng khác. Nó sẽ không cung cấp dữ liệu cho đến khi một số người quan sát đăng ký vào nó.
Lớp quan sát viên
Lớp này sử dụng luồng dữ liệu được phát ra bởi observable. Có thể có nhiều người quan sát với người quan sát được và mỗi người quan sát sẽ nhận được từng mục dữ liệu được phát ra. Người quan sát có thể nhận được ba loại sự kiện bằng cách đăng ký vào có thể quan sát -
on_next() event - Nó ngụ ý rằng có một phần tử trong luồng dữ liệu.
on_completed() event - Nó ngụ ý kết thúc phát thải và không có mặt hàng nào nữa.
on_error() event - Nó cũng ngụ ý kết thúc phát xạ nhưng trong trường hợp khi một lỗi được tạo ra bởi observable.
RxPY - Mô-đun Python cho lập trình phản ứng
RxPY là một mô-đun Python có thể được sử dụng để lập trình phản ứng. Chúng tôi cần đảm bảo rằng mô-đun đã được cài đặt. Lệnh sau có thể được sử dụng để cài đặt mô-đun RxPY:
pip install RxPY
Thí dụ
Sau đây là một tập lệnh Python, sử dụng RxPY mô-đun và các lớp của nó Observable và Observe forlập trình phản ứng. Về cơ bản có hai lớp -
get_strings() - để lấy dây từ người quan sát.
PrintObserver()- để in các chuỗi từ trình quan sát. Nó sử dụng cả ba sự kiện của lớp quan sát viên. Nó cũng sử dụng lớp subscribe ().
from rx import Observable, Observer
def get_strings(observer):
observer.on_next("Ram")
observer.on_next("Mohan")
observer.on_next("Shyam")
observer.on_completed()
class PrintObserver(Observer):
def on_next(self, value):
print("Received {0}".format(value))
def on_completed(self):
print("Finished")
def on_error(self, error):
print("Error: {0}".format(error))
source = Observable.create(get_strings)
source.subscribe(PrintObserver())
Đầu ra
Received Ram
Received Mohan
Received Shyam
Finished
Thư viện PyF Chức năng để lập trình phản ứng
PyFunctionallà một thư viện Python khác có thể được sử dụng để lập trình phản ứng. Nó cho phép chúng tôi tạo các chương trình chức năng bằng ngôn ngữ lập trình Python. Nó hữu ích vì nó cho phép chúng ta tạo các đường ống dẫn dữ liệu bằng cách sử dụng các toán tử chức năng được xâu chuỗi.
Sự khác biệt giữa RxPY và PyF Chức năng
Cả hai thư viện đều được sử dụng để lập trình phản ứng và xử lý luồng theo cách tương tự nhưng sự khác biệt chính giữa cả hai thư viện phụ thuộc vào việc xử lý dữ liệu. RxPY xử lý dữ liệu và sự kiện trong hệ thống trong khi PyFunctional tập trung vào việc chuyển đổi dữ liệu bằng cách sử dụng các mô hình lập trình chức năng.
Cài đặt mô-đun PyF Chức năng
Chúng ta cần cài đặt mô-đun này trước khi sử dụng nó. Nó có thể được cài đặt với sự trợ giúp của lệnh pip như sau:
pip install pyfunctional
Thí dụ
Ví dụ sau sử dụng the PyFunctional mô-đun và nó seqlớp đóng vai trò là đối tượng luồng mà chúng ta có thể lặp lại và thao tác. Trong chương trình này, nó ánh xạ chuỗi bằng cách sử dụng hàm lamda nhân đôi mọi giá trị, sau đó lọc giá trị trong đó x lớn hơn 4 và cuối cùng nó giảm chuỗi thành tổng của tất cả các giá trị còn lại.
from functional import seq
result = seq(1,2,3).map(lambda x: x*2).filter(lambda x: x > 4).reduce(lambda x, y: x + y)
print ("Result: {}".format(result))
Đầu ra
Result: 6