Hầu hết các nền tảng cho coroutines xảy ra trong thập niên 60/70 và sau đó dừng lại để ủng hộ các lựa chọn thay thế (ví dụ, chủ đề)
Có bất kỳ chất nào cho sự quan tâm đổi mới trong coroutines đã xảy ra trong python và các ngôn ngữ khác?
Hầu hết các nền tảng cho coroutines xảy ra trong thập niên 60/70 và sau đó dừng lại để ủng hộ các lựa chọn thay thế (ví dụ, chủ đề)
Có bất kỳ chất nào cho sự quan tâm đổi mới trong coroutines đã xảy ra trong python và các ngôn ngữ khác?
Câu trả lời:
Quân đoàn không bao giờ rời đi, trong khi đó họ chỉ bị lu mờ bởi những thứ khác. Sự quan tâm ngày càng tăng gần đây trong lập trình không đồng bộ và do đó, phần lớn là do ba yếu tố: tăng sự chấp nhận các kỹ thuật lập trình chức năng, các bộ công cụ hỗ trợ kém cho tính song song thực sự (JavaScript! Python!) Và quan trọng nhất là: sự đánh đổi khác nhau giữa các luồng và coroutines. Đối với một số trường hợp sử dụng, coroutines là khách quan tốt hơn.
Một trong những mô hình lập trình lớn nhất của thập niên 80, 90 và ngày nay là OOP. Nếu chúng ta nhìn vào lịch sử của OOP và đặc biệt là sự phát triển của ngôn ngữ Simula, chúng ta sẽ thấy rằng các lớp học phát triển từ các coroutines. Simula được dự định để mô phỏng các hệ thống với các sự kiện riêng biệt. Mỗi phần tử của hệ thống là một quy trình riêng biệt sẽ thực hiện theo các sự kiện trong thời gian của một bước mô phỏng, sau đó mang lại kết quả để cho các quy trình khác thực hiện công việc của chúng. Trong quá trình phát triển Simula 67, khái niệm lớp học đã được giới thiệu. Bây giờ trạng thái bền bỉ của coroutine được lưu trữ trong các thành viên đối tượng và các sự kiện được kích hoạt bằng cách gọi một phương thức. Để biết thêm chi tiết, hãy xem xét việc đọc bài viết Sự phát triển của các ngôn ngữ SIMULA của Nygaard & Dahl.
Vì vậy, trong một vòng xoắn vui nhộn, chúng tôi đã sử dụng coroutines suốt, chúng tôi chỉ gọi chúng là các đối tượng và lập trình hướng sự kiện.
Liên quan đến song song, có hai loại ngôn ngữ: những ngôn ngữ có mô hình bộ nhớ phù hợp và những ngôn ngữ không có. Một mô hình bộ nhớ thảo luận về những thứ giống như nếu tôi viết vào một biến và sau đó đọc từ biến đó trong một luồng khác, tôi có thấy giá trị cũ hoặc giá trị mới hoặc có lẽ là một giá trị không hợp lệ không? "Trước" và "sau" nghĩa là gì? Những hoạt động được đảm bảo là nguyên tử?
Tạo một mô hình bộ nhớ tốt rất khó, do đó, nỗ lực này đơn giản chưa bao giờ được thực hiện đối với hầu hết các ngôn ngữ nguồn mở động không xác định, được xác định theo triển khai: Perl, JavaScript, Python, Ruby, PHP. Tất nhiên, tất cả những ngôn ngữ đó đã phát triển vượt xa so với kịch bản ban đầu mà chúng được xây dựng cho. Chà, một số ngôn ngữ này có một số loại tài liệu mô hình bộ nhớ, nhưng những ngôn ngữ đó là không đủ. Thay vào đó, chúng tôi có hack:
Perl có thể được biên dịch với sự hỗ trợ luồng, nhưng mỗi luồng chứa một bản sao riêng biệt của trạng thái trình thông dịch hoàn chỉnh, làm cho các luồng trở nên đắt đỏ. Là lợi ích duy nhất, phương pháp không chia sẻ này tránh được các cuộc đua dữ liệu và buộc các lập trình viên chỉ giao tiếp thông qua hàng đợi / tín hiệu / IPC. Perl không có một câu chuyện mạnh mẽ để xử lý async.
JavaScript luôn có sự hỗ trợ phong phú cho lập trình chức năng, vì vậy các lập trình viên sẽ mã hóa thủ công / cuộc gọi lại theo cách thủ công trong các chương trình của họ, nơi họ cần các hoạt động không đồng bộ. Ví dụ, với các yêu cầu Ajax hoặc độ trễ hoạt hình. Vì web vốn dĩ không đồng bộ, nên có rất nhiều mã JavaScript không đồng bộ và việc quản lý tất cả các cuộc gọi lại này vô cùng đau đớn. Do đó, chúng tôi thấy nhiều nỗ lực để tổ chức các cuộc gọi lại tốt hơn (Lời hứa) hoặc loại bỏ chúng hoàn toàn.
Python có tính năng không may này được gọi là Khóa phiên dịch toàn cầu. Về cơ bản, mô hình bộ nhớ Python là tất cả các hiệu ứng xuất hiện tuần tự vì không có sự song song. Mỗi lần chỉ có một luồng sẽ chạy mã Python. Trong khi đó Python có các luồng, chúng chỉ mạnh như coroutines. [1] Python có thể mã hóa nhiều coroutines thông qua các hàm tạo yield
. Nếu được sử dụng đúng cách, điều này một mình có thể tránh được hầu hết địa ngục gọi lại từ JavaScript. Hệ thống async / await gần đây hơn từ Python 3.5 giúp cho các thành ngữ không đồng bộ thuận tiện hơn trong Python và tích hợp một vòng lặp sự kiện.
[1]: Về mặt kỹ thuật những hạn chế này chỉ áp dụng cho CPython, triển khai tham chiếu Python. Các triển khai khác như Jython cung cấp các luồng thực sự có thể thực thi song song, nhưng phải trải qua thời gian dài để thực hiện hành vi tương đương. Về cơ bản: mỗi biến hoặc thành viên đối tượng là một biến dễ bay hơi sao cho tất cả các thay đổi là nguyên tử và được nhìn thấy ngay lập tức trong tất cả các luồng. Tất nhiên, sử dụng các biến dễ bay hơi đắt hơn nhiều so với sử dụng các biến thông thường.
Tôi không biết đủ về Ruby và PHP để rang chúng đúng cách.
Tóm lại: một số ngôn ngữ này có các quyết định thiết kế cơ bản làm cho việc đa luồng không mong muốn hoặc không thể, dẫn đến sự tập trung mạnh mẽ hơn vào các lựa chọn thay thế như coroutines và về cách làm cho việc lập trình async thuận tiện hơn.
Cuối cùng, hãy nói về sự khác biệt giữa coroutines và chủ đề:
Các luồng về cơ bản giống như các tiến trình, ngoại trừ việc nhiều luồng trong một tiến trình chia sẻ một không gian bộ nhớ. Điều này có nghĩa là các chủ đề không có nghĩa là trọng lượng nhẹ ánh sáng về mặt bộ nhớ. Chủ đề được lên lịch trước bởi hệ điều hành. Điều này có nghĩa là các công tắc nhiệm vụ có chi phí hoạt động cao và có thể xảy ra vào những thời điểm bất tiện. Chi phí hoạt động này có hai thành phần: chi phí tạm dừng trạng thái của luồng và chi phí chuyển đổi giữa chế độ người dùng (đối với luồng) và chế độ lõi (đối với trình lập lịch biểu).
Nếu một quá trình lên lịch các luồng của chính nó trực tiếp và hợp tác, thì việc chuyển ngữ cảnh sang chế độ kernel là không cần thiết và việc chuyển đổi các tác vụ tương đối tốn kém cho một lệnh gọi hàm gián tiếp, như trong: khá rẻ. Những sợi có trọng lượng nhẹ này có thể được gọi là sợi màu xanh lá cây, sợi hoặc coroutines tùy thuộc vào các chi tiết khác nhau. Người dùng đáng chú ý của các sợi / sợi màu xanh lá cây là các triển khai Java ban đầu và gần đây là Goroutines ở Golang. Một lợi thế về mặt khái niệm của coroutines là việc thực hiện chúng có thể được hiểu theo dòng chảy kiểm soát một cách rõ ràng qua lại giữa các coroutines. Tuy nhiên, các coroutine này không đạt được sự song song thực sự trừ khi chúng được lên lịch trên nhiều luồng hệ điều hành.
Trường hợp giá rẻ coroutines hữu ích? Hầu hết các phần mềm không cần một luồng gazillion, vì vậy các luồng đắt tiền thông thường thường ổn. Tuy nhiên, lập trình async đôi khi có thể đơn giản hóa mã của bạn. Để được sử dụng tự do, sự trừu tượng này phải đủ rẻ.
Và sau đó là web. Như đã đề cập ở trên, web vốn không đồng bộ. Yêu cầu mạng chỉ đơn giản là mất một thời gian dài. Nhiều máy chủ web duy trì một nhóm luồng đầy đủ các luồng công nhân. Tuy nhiên, hầu hết thời gian của các luồng này sẽ không hoạt động vì chúng đang chờ một số tài nguyên, có phải là đang chờ một sự kiện I / O khi tải tệp từ đĩa, đợi cho đến khi máy khách nhận ra một phần của phản hồi hoặc đợi cho đến khi cơ sở dữ liệu truy vấn hoàn thành. NodeJS đã chứng minh một cách phi thường rằng một thiết kế máy chủ không đồng bộ và dựa trên sự kiện do đó hoạt động rất tốt. Rõ ràng JavaScript không phải là ngôn ngữ duy nhất được sử dụng cho các ứng dụng web, do đó, cũng có một sự khích lệ lớn đối với các ngôn ngữ khác (đáng chú ý trong Python và C #) để giúp lập trình web không đồng bộ dễ dàng hơn.
Coroutines từng là hữu ích vì hệ điều hành không thực hiện trước công phủ đầu lập kế hoạch. Khi họ bắt đầu cung cấp lịch trình ưu tiên, việc từ bỏ kiểm soát định kỳ trong chương trình của bạn là lâu hơn.
Khi các bộ xử lý đa lõi trở nên phổ biến hơn, các coroutine được sử dụng để đạt được tính song song của nhiệm vụ và / hoặc giữ mức độ sử dụng của hệ thống cao (khi một luồng thực thi phải chờ trên một tài nguyên, một luồng khác có thể bắt đầu chạy ở vị trí của nó).
NodeJS là một trường hợp đặc biệt, trong đó coroutines được sử dụng có quyền truy cập song song vào IO. Nghĩa là, nhiều luồng được sử dụng để phục vụ các yêu cầu IO, nhưng một luồng duy nhất được sử dụng để thực thi mã javascript. Mục đích của việc thực thi mã người dùng trong một chuỗi ký hiệu là để tránh sự cần thiết phải sử dụng mutexes. Điều này thuộc danh mục cố gắng duy trì mức sử dụng của hệ thống cao như đã đề cập ở trên.
Các hệ thống ban đầu đã sử dụng coroutines để cung cấp đồng thời chủ yếu vì chúng là cách đơn giản nhất để làm điều đó. Các luồng yêu cầu một lượng hỗ trợ khá lớn từ hệ điều hành (bạn có thể triển khai chúng ở cấp độ người dùng, nhưng bạn sẽ cần một số cách sắp xếp để hệ thống làm gián đoạn quá trình của bạn) và khó thực hiện hơn ngay cả khi bạn có hỗ trợ .
Các chủ đề bắt đầu tiếp quản sau đó bởi vì, vào thập niên 70 hoặc 80, tất cả các hệ điều hành nghiêm túc đều hỗ trợ chúng (và, vào thập niên 90, thậm chí là Windows!), Và chúng phổ biến hơn. Và chúng dễ sử dụng hơn. Đột nhiên mọi người nghĩ chủ đề là điều lớn tiếp theo.
Vào cuối những năm 90, các vết nứt bắt đầu xuất hiện và vào đầu những năm 2000, rõ ràng là có vấn đề nghiêm trọng với các luồng:
Theo thời gian, số lượng các chương trình nhiệm vụ thường cần thực hiện bất cứ lúc nào đã tăng lên nhanh chóng, làm tăng các vấn đề gây ra bởi (1) và (2) ở trên. Sự chênh lệch giữa tốc độ xử lý và thời gian truy cập bộ nhớ ngày càng tăng, làm trầm trọng thêm vấn đề (3). Và sự phức tạp của các chương trình về số lượng và loại tài nguyên khác nhau mà chúng yêu cầu đã tăng lên, làm tăng sự liên quan của vấn đề (4).
Nhưng bằng cách mất một chút tính tổng quát và đặt thêm một chút trách nhiệm cho lập trình viên để suy nghĩ về cách các quy trình của họ có thể hoạt động cùng nhau, các coroutine có thể giải quyết tất cả các vấn đề này.
Tôi muốn bắt đầu bằng cách nêu rõ lý do tại sao các xác chết không được hồi sinh, song song. Nhìn chung, các coroutines hiện đại không phải là một phương tiện để đạt được sự song song dựa trên nhiệm vụ, vì các triển khai hiện đại không sử dụng chức năng đa xử lý. Thứ gần nhất bạn có được là những thứ như sợi .
Các coroutine hiện đại là một cách để đạt được sự đánh giá lười biếng , một thứ rất hữu ích trong các ngôn ngữ chức năng như haskell, thay vì lặp đi lặp lại toàn bộ một tập hợp để thực hiện một thao tác, bạn sẽ có thể thực hiện một thao tác chỉ đánh giá khi cần thiết ( hữu ích cho các tập hợp vô hạn của các mục hoặc các tập hợp lớn khác có kết thúc sớm và tập hợp con).
Với việc sử dụng các từ khóa Năng suất để tạo ra máy phát điện (mà trong bản thân đáp ứng một phần nhu cầu đánh giá lười biếng) trong các ngôn ngữ như Python và C #, coroutines, trong việc thực hiện hiện đại không chỉ là có thể, nhưng có thể với ra cú pháp đặc biệt bằng ngôn ngữ riêng của mình (mặc dù python cuối cùng đã thêm một vài bit để giúp đỡ). Co-thói quen giúp đỡ với evaulation lười biếng với ý tưởng tương lai của nơi mà nếu bạn không cần giá trị của một biến vào thời điểm đó, bạn có thể trì hoãn thực sự mua nó cho đến khi bạn rõ ràng yêu cầu cho giá trị đó (cho phép bạn sử dụng các giá trị và lười biếng đánh giá nó tại một thời điểm khác với khởi tạo).
Tuy nhiên, ngoài việc đánh giá lười biếng, đặc biệt là trong thế giới web, các thói quen chung này giúp khắc phục cuộc gọi lại địa ngục . Coroutines trở nên hữu ích trong việc truy cập cơ sở dữ liệu, giao dịch trực tuyến, ui, v.v., trong đó thời gian xử lý trên máy khách sẽ không dẫn đến việc truy cập nhanh hơn những gì bạn cần. Việc xâu chuỗi có thể hoàn thành điều tương tự, nhưng đòi hỏi nhiều chi phí hơn trong lĩnh vực này, và ngược lại với coroutines, thực sự hữu ích cho việc xử lý song song .
Nói tóm lại, khi sự phát triển web phát triển và các mô hình chức năng hợp nhất nhiều hơn với các ngôn ngữ bắt buộc, coroutines đã trở thành một giải pháp cho các vấn đề không đồng bộ và đánh giá lười biếng. Các coroutines đến các không gian có vấn đề trong đó việc phân luồng và xử lý đa luồng nói chung là không cần thiết, bất tiện hoặc không thể.
Các coroutines trong các ngôn ngữ như Javascript, Lua, C # và Python đều xuất phát từ việc thực hiện các chức năng riêng lẻ từ bỏ quyền kiểm soát luồng chính sang các chức năng khác (không liên quan gì đến các cuộc gọi của hệ điều hành).
Trong ví dụ về con trăn này , chúng ta có một hàm python vui nhộn với một cái gì đó được gọi là await
bên trong nó. Về cơ bản, đây là một sản lượng, mang lại sự thực thi để loop
sau đó cho phép một chức năng khác chạy (trong trường hợp này là một factorial
chức năng khác ). Lưu ý rằng khi nó nói "Thực thi song song các nhiệm vụ" là một cách viết sai, thì thực tế nó không thực thi song song, việc thực thi chức năng xen kẽ của nó thông qua việc sử dụng từ khóa await (mà hãy nhớ chỉ là một loại năng suất đặc biệt)
Chúng cho phép duy nhất, không song song, sản lượng điều khiển cho đồng thời các quy trình mà không phải là nhiệm vụ song song , theo nghĩa rằng những nhiệm vụ không hoạt động bao giờ cùng một lúc. Coroutines không phải là chủ đề trong việc thực hiện ngôn ngữ hiện đại. Tất cả các ngôn ngữ này thực hiện các thường trình đồng đều bắt nguồn từ các lệnh gọi năng suất hàm này (mà lập trình viên phải thực sự đưa vào thủ công vào các thường trình chung của bạn).
EDIT: C ++ Boost coroutine2 hoạt động theo cách tương tự, và lời giải thích chặt chẽ hơn sẽ cho hình dung rõ hơn về những gì tôi đang nói với các anh em, xem tại đây . Như bạn có thể thấy, không có "trường hợp đặc biệt" nào với việc triển khai, những thứ như sợi tăng cường là ngoại lệ của quy tắc và thậm chí sau đó yêu cầu đồng bộ hóa rõ ràng.
EDIT2: vì ai đó nghĩ rằng tôi đang nói về hệ thống dựa trên nhiệm vụ c #, nên tôi đã không làm vậy. Tôi đã nói về hệ thống của Unity và các triển khai c # ngây thơ