Node có một mô hình hoàn toàn khác và một khi nó được nắm bắt chính xác, sẽ dễ dàng thấy cách giải quyết vấn đề khác nhau này. Bạn không bao giờ cần nhiều luồng trong một ứng dụng Node (1) vì bạn có một cách khác để thực hiện cùng một việc. Bạn tạo nhiều quy trình; nhưng nó rất khác so với ví dụ như cách Prefork của Apache Web Server mpm hoạt động.
Bây giờ, hãy nghĩ rằng chúng ta chỉ có một lõi CPU và chúng ta sẽ phát triển một ứng dụng (theo cách của Node) để thực hiện một số công việc. Công việc của chúng tôi là xử lý một tệp lớn chạy trên từng byte nội dung của nó. Cách tốt nhất cho phần mềm của chúng tôi là bắt đầu công việc từ đầu tệp, theo từng byte cho đến cuối.
- Này, Hasan, tôi cho rằng bạn là học sinh mới hoặc rất cũ từ thời Ông của tôi !!! Tại sao bạn không tạo một số luồng và làm cho nó nhanh hơn nhiều?
- Ồ, chúng ta chỉ có một lõi CPU.
-- Vậy thì sao? Tạo một số người đàn ông chủ đề, làm cho nó nhanh hơn!
- Nó không hoạt động như vậy. Nếu tôi tạo chủ đề, tôi sẽ làm cho nó chậm hơn. Bởi vì tôi sẽ thêm rất nhiều chi phí vào hệ thống để chuyển đổi giữa các luồng, cố gắng cho chúng một khoảng thời gian vừa đủ và bên trong quy trình của tôi, cố gắng giao tiếp giữa các luồng này. Ngoài tất cả những sự kiện này, tôi cũng sẽ phải suy nghĩ về cách tôi sẽ chia một công việc thành nhiều phần có thể được thực hiện song song.
- Thôi được rồi, anh thấy em tội nghiệp lắm. Hãy sử dụng máy tính của tôi, nó có 32 lõi!
- Chà, bạn thật tuyệt, bạn thân mến của tôi, cảm ơn bạn rất nhiều. Tôi rât cảm kich!
Sau đó chúng tôi quay trở lại làm việc. Bây giờ chúng tôi có 32 lõi cpu nhờ người bạn giàu có của chúng tôi. Các quy tắc chúng ta phải tuân thủ vừa thay đổi. Bây giờ chúng tôi muốn sử dụng tất cả của cải chúng tôi được cho.
Để sử dụng nhiều lõi, chúng ta cần tìm cách chia công việc của mình thành các phần để chúng ta có thể xử lý song song. Nếu nó không phải là Node, chúng tôi sẽ sử dụng các luồng cho việc này; 32 luồng, một luồng cho mỗi lõi cpu. Tuy nhiên, vì chúng ta có Node, chúng ta sẽ tạo 32 Node process.
Các luồng có thể là một sự thay thế tốt cho các quy trình Node, thậm chí có thể là một cách tốt hơn; nhưng chỉ trong một loại công việc cụ thể mà công việc đã được xác định và chúng tôi có toàn quyền kiểm soát cách xử lý công việc đó. Ngoài ra, đối với mọi loại vấn đề khác mà công việc đến từ bên ngoài theo cách mà chúng tôi không kiểm soát được và chúng tôi muốn trả lời càng nhanh càng tốt, cách của Node là vượt trội hơn hẳn.
- Này, Hasan, bạn vẫn đang làm việc đơn luồng à? Anh bị làm sao vậy? Tôi vừa cung cấp cho bạn những gì bạn muốn. Bạn không có lời bào chữa nào nữa. Tạo chủ đề, làm cho nó chạy nhanh hơn.
- Tôi đã chia công việc thành nhiều phần và mọi quy trình sẽ thực hiện song song trên một trong những phần này.
- Tại sao bạn không tạo chủ đề?
- Xin lỗi, tôi nghĩ nó không sử dụng được. Bạn có thể lấy máy tính của bạn nếu bạn muốn?
- Không sao đâu, anh ngầu lắm, chỉ là không hiểu sao em không dùng đề thôi?
- Cảm ơn bạn về chiếc máy tính. :) Tôi đã chia công việc thành nhiều phần và tôi tạo các quy trình để làm việc trên các phần này song song. Tất cả các lõi CPU sẽ được sử dụng tối đa. Tôi có thể làm điều này với các chủ đề thay vì các quy trình; nhưng Node có cách này và sếp của tôi, Parth Thakkar muốn tôi sử dụng Node.
- Được rồi, hãy cho tôi biết nếu bạn cần một máy tính khác. : p
Nếu tôi tạo 33 quy trình, thay vì 32, bộ lập lịch của hệ điều hành sẽ tạm dừng một luồng, bắt đầu luồng kia, tạm dừng nó sau một số chu kỳ, bắt đầu lại luồng khác ... Đây là chi phí không cần thiết. Tôi không muốn nó. Trên thực tế, trên một hệ thống có 32 lõi, tôi thậm chí sẽ không muốn tạo chính xác 32 quy trình, 31 có thể đẹp hơn . Bởi vì nó không chỉ là ứng dụng của tôi sẽ hoạt động trên hệ thống này. Để lại một chút không gian cho những thứ khác có thể tốt, đặc biệt nếu chúng tôi có 32 phòng.
Tôi tin rằng chúng ta đang ở cùng một trang về việc sử dụng đầy đủ các bộ xử lý cho các tác vụ đòi hỏi nhiều CPU .
- Hmm, Hasan, tôi xin lỗi vì đã chế giễu cậu một chút. Tôi tin rằng tôi hiểu bạn hơn bây giờ. Nhưng vẫn còn điều gì đó mà tôi cần giải thích: Tất cả những gì đang xôn xao về việc chạy hàng trăm chủ đề? Tôi đọc ở khắp mọi nơi rằng các luồng nhanh hơn nhiều để tạo và ngu ngốc hơn nhiều so với các quy trình rèn? Bạn fork các quy trình thay vì các luồng và bạn nghĩ rằng đó là mức cao nhất bạn sẽ nhận được với Node. Vậy thì có phải Node không thích hợp cho loại công việc này không?
- Đừng lo, tôi cũng rất tuyệt. Mọi người đều nói những điều này nên tôi nghĩ tôi đã quen với việc nghe chúng.
-- Vì thế? Node không tốt cho điều này?
- Node hoàn toàn tốt cho việc này mặc dù các luồng cũng có thể tốt. Đối với chi phí tạo luồng / quy trình; về những thứ mà bạn lặp lại nhiều, mỗi mili giây đều có giá trị. Tuy nhiên, tôi chỉ tạo 32 quy trình và sẽ mất một khoảng thời gian nhỏ. Nó sẽ chỉ xảy ra một lần. Nó sẽ không tạo ra bất kỳ sự khác biệt nào.
- Khi nào tôi muốn tạo hàng nghìn chủ đề?
- Bạn không bao giờ muốn tạo hàng nghìn chủ đề. Tuy nhiên, trên một hệ thống đang thực hiện công việc đến từ bên ngoài, chẳng hạn như máy chủ web xử lý các yêu cầu HTTP; nếu bạn đang sử dụng một chủ đề cho mỗi yêu cầu, bạn sẽ tạo ra rất nhiều chủ đề, nhiều người trong số họ.
- Node thì khác? Đúng?
-- Đúng chính xác. Đây là nơi Node thực sự tỏa sáng. Giống như một luồng nhẹ hơn nhiều so với một quy trình, một lời gọi hàm nhẹ hơn nhiều so với một luồng. Node gọi các hàm, thay vì tạo luồng. Trong ví dụ về máy chủ web, mọi yêu cầu đến đều gây ra một lệnh gọi hàm.
- Hừ, thú vị; nhưng bạn chỉ có thể chạy một chức năng cùng lúc nếu bạn không sử dụng nhiều luồng. Làm thế nào điều này có thể hoạt động khi nhiều yêu cầu đến máy chủ web cùng một lúc?
- Bạn hoàn toàn đúng về cách chạy của các hàm, từng hàm một, không bao giờ là hai hàm song song. Ý tôi là trong một quy trình duy nhất, chỉ có một phạm vi mã đang chạy tại một thời điểm. Trình lập lịch hệ điều hành không đến và tạm dừng chức năng này và chuyển sang chức năng khác, trừ khi nó tạm dừng quy trình để dành thời gian cho quy trình khác, không phải luồng khác trong quy trình của chúng tôi. (2)
- Sau đó, làm thế nào một tiến trình có thể xử lý 2 yêu cầu cùng một lúc?
- Một tiến trình có thể xử lý hàng chục nghìn yêu cầu cùng một lúc miễn là hệ thống của chúng ta có đủ tài nguyên (RAM, Mạng, v.v.). Cách thức hoạt động của các chức năng đó là SỰ KHÁC BIỆT CHÍNH.
- Hừ, bây giờ có nên cao hứng không?
- Có thể :) Node chạy một vòng lặp qua hàng đợi. Trong hàng đợi này là các công việc của chúng tôi, tức là, các cuộc gọi mà chúng tôi bắt đầu xử lý các yêu cầu đến. Điểm quan trọng nhất ở đây là cách chúng ta thiết kế các hàm của mình để chạy. Thay vì bắt đầu xử lý một yêu cầu và bắt người gọi đợi cho đến khi chúng tôi hoàn thành công việc, chúng tôi nhanh chóng kết thúc chức năng của mình sau khi thực hiện một lượng công việc có thể chấp nhận được. Khi chúng ta đến một điểm mà chúng ta cần đợi một thành phần khác thực hiện một số công việc và trả về cho chúng ta một giá trị, thay vì chờ đợi điều đó, chúng ta chỉ cần hoàn thành hàm của mình thêm phần còn lại của công việc vào hàng đợi.
- Nghe phức tạp quá nhỉ?
- Không không, tôi nghe có vẻ phức tạp; nhưng bản thân hệ thống rất đơn giản và nó có ý nghĩa hoàn hảo.
Bây giờ tôi muốn ngừng trích dẫn cuộc đối thoại giữa hai nhà phát triển này và kết thúc câu trả lời của mình sau một ví dụ nhanh cuối cùng về cách các chức năng này hoạt động.
Bằng cách này, chúng tôi đang làm những gì OS Scheduler thường làm. Chúng tôi tạm dừng công việc của mình vào một thời điểm nào đó và để các lệnh gọi hàm khác (như các luồng khác trong môi trường đa luồng) chạy cho đến khi chúng tôi quay lại lượt. Điều này tốt hơn nhiều so với việc để công việc cho OS Scheduler, công cụ cố gắng cung cấp thời gian cho mọi luồng trên hệ thống. Chúng tôi biết những gì chúng tôi đang làm tốt hơn nhiều so với OS Scheduler và chúng tôi dự kiến sẽ dừng lại khi chúng tôi nên dừng lại.
Dưới đây là một ví dụ đơn giản trong đó chúng tôi mở một tệp và đọc tệp đó để thực hiện một số thao tác trên dữ liệu.
Cách đồng bộ:
Open File
Repeat This:
Read Some
Do the work
Cách không đồng bộ:
Open File and Do this when it is ready: // Our function returns
Repeat this:
Read Some and when it is ready: // Returns again
Do some work
Như bạn thấy, chức năng của chúng tôi yêu cầu hệ thống mở một tệp và không đợi nó được mở. Nó tự kết thúc bằng cách cung cấp các bước tiếp theo sau khi tệp đã sẵn sàng. Khi chúng ta quay trở lại, Node sẽ chạy các lệnh gọi hàm khác trên hàng đợi. Sau khi chạy qua tất cả các chức năng, vòng lặp sự kiện chuyển sang lượt tiếp theo ...
Tóm lại, Node có một mô hình hoàn toàn khác so với phát triển đa luồng; nhưng điều này không có nghĩa là nó thiếu thứ. Đối với một công việc đồng bộ (nơi chúng ta có thể quyết định thứ tự và cách thức xử lý), nó hoạt động cũng như song song đa luồng. Đối với một công việc đến từ bên ngoài như các yêu cầu đến máy chủ, nó chỉ đơn giản là vượt trội.
(1) Trừ khi bạn đang xây dựng thư viện bằng các ngôn ngữ khác như C / C ++, trong trường hợp đó bạn vẫn không tạo luồng để phân chia công việc. Đối với loại công việc này, bạn có hai luồng, một trong số đó sẽ tiếp tục giao tiếp với Node trong khi luồng kia thực hiện công việc thực sự.
(2) Trên thực tế, mọi quy trình Node đều có nhiều luồng vì những lý do tương tự mà tôi đã đề cập trong chú thích đầu tiên. Tuy nhiên đây không phải là cách giống như 1000 chủ đề làm các công việc tương tự. Các chuỗi bổ sung đó dành cho những thứ như chấp nhận các sự kiện IO và xử lý thông báo giữa các quá trình.
CẬP NHẬT (Như trả lời một câu hỏi hay trong nhận xét)
@Mark, cảm ơn bạn vì những lời phê bình mang tính xây dựng. Trong mô hình của Node, bạn không bao giờ nên có các hàm mất quá nhiều thời gian để xử lý trừ khi tất cả các lệnh gọi khác trong hàng đợi được thiết kế để chạy lần lượt. Trong trường hợp các tác vụ tốn kém về mặt tính toán, nếu chúng ta nhìn vào bức tranh toàn cảnh, chúng ta thấy rằng đây không phải là câu hỏi "Chúng ta nên sử dụng luồng hay quy trình?" nhưng một câu hỏi đặt ra là "Làm thế nào chúng ta có thể phân chia các tác vụ này một cách cân bằng thành các tác vụ con mà chúng ta có thể chạy chúng song song bằng cách sử dụng nhiều lõi CPU trên hệ thống?" Giả sử chúng tôi sẽ xử lý 400 tệp video trên hệ thống có 8 lõi. Nếu chúng ta muốn xử lý từng tệp một, thì chúng ta cần một hệ thống xử lý các phần khác nhau của cùng một tệp, trong trường hợp đó, có thể, một hệ thống xử lý đơn đa luồng sẽ dễ xây dựng hơn và thậm chí hiệu quả hơn. Chúng ta vẫn có thể sử dụng Node cho việc này bằng cách chạy nhiều tiến trình và chuyển các thông báo giữa chúng khi cần chia sẻ / giao tiếp trạng thái. Như tôi đã nói trước đây, cách tiếp cận đa quy trình với Node làcũng như cách tiếp cận đa luồng trong loại nhiệm vụ này; nhưng không nhiều hơn thế. Một lần nữa, như tôi đã nói trước đây, tình huống mà Node tỏa sáng là khi chúng ta có các tác vụ này làm đầu vào cho hệ thống từ nhiều nguồn vì việc giữ nhiều kết nối đồng thời trong Node nhẹ hơn nhiều so với luồng cho mỗi kết nối hoặc quá trình cho mỗi kết nối hệ thống.
Đối với setTimeout(...,0)
các cuộc gọi; đôi khi nghỉ giải lao trong một tác vụ tốn thời gian để cho phép các cuộc gọi trong hàng đợi có thể yêu cầu phần xử lý của chúng. Phân chia nhiệm vụ theo nhiều cách khác nhau có thể giúp bạn thoát khỏi những điều này; tuy nhiên, đây không thực sự là một vụ hack, nó chỉ là cách hoạt động của hàng đợi sự kiện. Ngoài ra, sử dụng process.nextTick
cho mục đích này tốt hơn nhiều vì khi bạn sử dụng setTimeout
, việc tính toán và kiểm tra thời gian đã qua sẽ là cần thiết trong khi process.nextTick
đơn giản là những gì chúng tôi thực sự muốn: "Này nhiệm vụ, quay lại cuối hàng đợi, bạn đã sử dụng phần của mình! "