Cách mô hình IO không chặn luồng đơn hoạt động trong Node.js


325

Tôi không phải là lập trình viên Node, nhưng tôi quan tâm đến cách hoạt động của mô hình IO đơn luồng không chặn . Sau khi tôi đọc bài viết về sự hiểu biết về bài viết -the-node-js-event-loop , tôi thực sự bối rối về nó. Nó đã đưa ra một ví dụ cho mô hình:

c.query(
   'SELECT SLEEP(20);',
   function (err, results, fields) {
     if (err) {
       throw err;
     }
     res.writeHead(200, {'Content-Type': 'text/html'});
     res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');
     c.end();
    }
);

Que: Khi có hai yêu cầu A (đến trước) và B vì chỉ có một luồng duy nhất, chương trình phía máy chủ sẽ xử lý yêu cầu A trước tiên: thực hiện truy vấn SQL là chờ lệnh I / O chờ. Và chương trình bị kẹt khi I/Ochờ và không thể thực thi mã làm cho trang web phía sau. Chương trình sẽ chuyển sang yêu cầu B trong thời gian chờ đợi? Theo tôi, vì mô hình luồng đơn, không có cách nào để chuyển đổi một yêu cầu từ một yêu cầu khác. Nhưng tiêu đề của mã ví dụ nói rằng mọi thứ chạy song song ngoại trừ mã của bạn .

(PS Tôi không chắc liệu tôi có hiểu nhầm mã hay không vì tôi chưa bao giờ sử dụng Node.) Làm thế nào Node chuyển A sang B trong khi chờ đợi? Và bạn có thể giải thích mô hình IO không chặn luồng đơn của Node một cách đơn giản không? Tôi sẽ đánh giá cao nếu bạn có thể giúp tôi. :)

Câu trả lời:


374

Node.js được xây dựng dựa trên libuv , một thư viện đa nền tảng trừu tượng hóa apis / s tòa nhà cho đầu vào / đầu ra không đồng bộ (không chặn) được cung cấp bởi các HĐH được hỗ trợ (ít nhất là Unix, OS X và Windows).

IO không đồng bộ

Trong mô hình lập trình này, thao tác mở / đọc / ghi trên các thiết bị và tài nguyên (ổ cắm, hệ thống tệp, v.v.) được quản lý bởi hệ thống tệp không chặn luồng cuộc gọi (như trong mô hình giống như c đồng bộ điển hình) và chỉ đánh dấu quá trình (trong cấu trúc dữ liệu cấp kernel / OS) sẽ được thông báo khi có dữ liệu hoặc sự kiện mới. Trong trường hợp ứng dụng giống như máy chủ web, quy trình sau đó có trách nhiệm tìm ra yêu cầu / bối cảnh nào mà sự kiện được thông báo thuộc về và tiến hành xử lý yêu cầu từ đó. Lưu ý rằng điều này nhất thiết sẽ có nghĩa là bạn sẽ ở trên một khung ngăn xếp khác với khung yêu cầu bắt nguồn từ hệ điều hành vì sau này phải đưa ra một bộ điều phối quy trình để một quy trình xử lý luồng đơn xử lý các sự kiện mới.

Vấn đề với mô hình mà tôi mô tả là nó không quen thuộc và khó lý luận đối với lập trình viên vì bản chất nó không tuần tự. "Bạn cần đưa ra yêu cầu trong chức năng A và xử lý kết quả trong một chức năng khác trong đó người dân địa phương của bạn thường không có sẵn."

Mô hình của Node (Kiểu liên tục và Vòng lặp sự kiện)

Nút đã giải quyết vấn đề tận dụng các tính năng ngôn ngữ của javascript để làm cho mô hình này trông đồng bộ hơn một chút bằng cách khiến lập trình viên sử dụng một phong cách lập trình nhất định. Mọi chức năng yêu cầu IO đều có chữ ký function (... parameters ..., callback)và cần được gọi lại sẽ được gọi khi hoàn thành thao tác được yêu cầu (hãy nhớ rằng hầu hết thời gian được chờ đợi để HĐH báo hiệu thời gian hoàn thành có thể dành làm công việc khác). Hỗ trợ đóng cửa của Javascript cho phép bạn sử dụng các biến bạn đã xác định trong hàm (gọi) bên ngoài bên trong phần thân của cuộc gọi lại - điều này cho phép giữ trạng thái giữa các chức năng khác nhau sẽ được gọi bởi thời gian chạy nút một cách độc lập. Xem thêm Phong cách tiếp tục .

Ngoài ra, sau khi gọi một hàm sinh ra một hoạt động IO, hàm gọi thường sẽ returnđiều khiển vòng lặp sự kiện của nút . Vòng lặp này sẽ gọi hàm gọi lại hoặc chức năng tiếp theo đã được lên lịch để thực hiện (rất có thể là do sự kiện tương ứng đã được HĐH thông báo) - điều này cho phép xử lý đồng thời nhiều yêu cầu.

Bạn có thể nghĩ vòng lặp sự kiện của nút tương tự như bộ điều phối của kernel : kernel sẽ lên lịch để thực thi một luồng bị chặn sau khi IO đang chờ xử lý của nó hoàn thành trong khi nút sẽ lên lịch gọi lại khi sự kiện tương ứng xảy ra.

Đồng thời cao, không song song

Như một nhận xét cuối cùng, cụm từ "mọi thứ chạy song song ngoại trừ mã của bạn" thực hiện công việc nắm bắt điểm mà nút cho phép mã của bạn xử lý các yêu cầu từ hàng trăm ngàn ổ cắm mở với một luồng đồng thời bằng cách ghép kênh và xâu chuỗi tất cả js của bạn logic trong một luồng thực thi duy nhất (mặc dù nói "mọi thứ chạy song song" có lẽ không đúng ở đây - xem Đồng thời so với song song - Sự khác biệt là gì? ). Điều này hoạt động khá tốt đối với các máy chủ webapp vì phần lớn thời gian thực sự dành cho việc chờ mạng hoặc đĩa (cơ sở dữ liệu / ổ cắm) và logic không thực sự tốn nhiều CPU - nghĩa là: điều này hoạt động tốt cho khối lượng công việc ràng buộc IO .


45
Câu hỏi tiếp theo: làm thế nào để I / O thực sự xảy ra sau đó? Node đang thực hiện một yêu cầu đến hệ thống và yêu cầu được thông báo khi nó kết thúc. Vì vậy, hệ thống đang chạy một luồng đang thực hiện I / O, hay hệ thống cũng đang thực hiện I / O không đồng bộ ở cấp phần cứng bằng cách sử dụng các ngắt? Một cái gì đó ở đâu đó phải đợi I / O kết thúc, và điều đó sẽ chặn cho đến khi nó hoàn thành và tiêu tốn một lượng tài nguyên.
Philip

6
Chỉ cần lưu ý rằng nhận xét tiếp theo này được trả lời bởi @ user568109 bên dưới, tôi ước có một cách để hợp nhất hai câu trả lời này.
lfalin

4
Tôi ước bạn có thể viết một phản hồi dài gấp đôi, vì vậy tôi sẽ hiểu rõ hơn gấp đôi.
Rafael Eyng

Nút được hỗ trợ ở rất nhiều nơi, cho hồ sơ. Khi tôi đang thiết kế phần sụn cho các bộ định tuyến MIPS32, Node.JS có thể chạy trên các bộ định tuyến thông qua OpenWRT.
Qix - MONICA ĐƯỢC PHÂN BIỆT

Làm thế nào để nó ghi điểm trên apache? Apache cũng có khả năng xử lý các kết nối đồng thời với một luồng riêng biệt.
Suhail Gupta

209

Chà, để đưa ra một số phối cảnh, hãy để tôi so sánh node.js với apache.

Apache là một máy chủ HTTP đa luồng, với mỗi và mọi yêu cầu mà máy chủ nhận được, nó tạo ra một luồng riêng biệt xử lý yêu cầu đó.

Mặt khác, Node.js được điều khiển theo sự kiện, xử lý tất cả các yêu cầu không đồng bộ từ một luồng.

Khi A và B được nhận trên apache, hai luồng được tạo để xử lý các yêu cầu. Mỗi xử lý truy vấn riêng biệt, mỗi lần chờ kết quả truy vấn trước khi phục vụ trang. Trang chỉ được phục vụ cho đến khi truy vấn kết thúc. Việc tìm nạp truy vấn bị chặn vì máy chủ có thể thực thi phần còn lại của luồng cho đến khi nhận được kết quả.

Trong nút, c.query được xử lý không đồng bộ, có nghĩa là trong khi c.query lấy kết quả cho A, nó nhảy để xử lý c.query cho B và khi kết quả đến A, nó sẽ gửi lại kết quả để gọi lại phản ứng. Node.js biết thực hiện gọi lại khi tìm nạp kết thúc.

Theo tôi, vì đó là một mô hình luồng đơn, không có cách nào để chuyển từ yêu cầu này sang yêu cầu khác.

Trên thực tế, máy chủ nút thực hiện chính xác điều đó cho bạn mọi lúc. Để thực hiện chuyển đổi, (hành vi không đồng bộ) hầu hết các chức năng mà bạn sẽ sử dụng sẽ có các cuộc gọi lại.

Biên tập

Truy vấn SQL được lấy từ thư viện mysql . Nó thực hiện kiểu gọi lại cũng như trình phát sự kiện để xếp hàng các yêu cầu SQL. Nó không thực thi chúng một cách không đồng bộ, được thực hiện bởi các luồng libuv bên trong cung cấp sự trừu tượng hóa của I / O không chặn. Các bước sau đây xảy ra để thực hiện một truy vấn:

  1. Mở một kết nối tới db, bản thân kết nối có thể được thực hiện không đồng bộ.
  2. Khi db được kết nối, truy vấn được chuyển đến máy chủ. Truy vấn có thể được xếp hàng.
  3. Vòng lặp sự kiện chính được thông báo về việc hoàn thành với cuộc gọi lại hoặc sự kiện.
  4. Vòng lặp chính thực hiện cuộc gọi lại / eventhandler của bạn.

Các yêu cầu đến máy chủ http được xử lý theo cách tương tự. Kiến trúc luồng nội bộ là như thế này:

vòng lặp sự kiện node.js

Các luồng C ++ là các libuv làm I / O không đồng bộ (đĩa hoặc mạng). Vòng lặp sự kiện chính tiếp tục thực thi sau khi gửi yêu cầu tới nhóm luồng. Nó có thể chấp nhận nhiều yêu cầu hơn vì nó không chờ đợi hoặc ngủ. Các truy vấn SQL / yêu cầu HTTP / hệ thống tệp đọc tất cả xảy ra theo cách này.


16
Sơ đồ rất hữu ích.
Anmol Saraf

14
Đợi đã, vì vậy trong sơ đồ của bạn, bạn có "luồng C ++ bên trong", có nghĩa là tất cả các hoạt động chặn IO sẽ sinh ra một luồng, phải không? Vì vậy, nếu ứng dụng Node của tôi thực hiện một số IO cho mọi yêu cầu , thì hầu như không có sự khác biệt giữa mô hình Node và mô hình Apache? Tôi không nhận được phần này xin lỗi.
gav.newalkar

21
@ gav.newalkar Họ không sinh ra một chủ đề, các yêu cầu được xếp hàng. Các chủ đề trong threadpool xử lý chúng. Các luồng không động và theo yêu cầu như trong Apache. Chúng thường được cố định và khác nhau từ hệ thống này đến hệ thống khác.
dùng568109

10
@ user568109 Nhưng Apache cũng đang sử dụng một threadpool ( httpd.apache.org/docs/2.4/mod/worker.html ). Vì vậy, cuối cùng, sự khác biệt giữa một thiết lập với node.js khác với một với Apache ở phía trước chỉ ở vị trí của luồng xử lý, phải không?
Kris

13
Sơ đồ đó nên ở trang đầu tiên của tài liệu chính thức.
bouvierr

52

Node.js sử dụng libuv đằng sau hậu trường. libuv có một nhóm luồng (có kích thước 4 theo mặc định). Do đó, Node.js sử dụng các luồng để đạt được sự tương tranh.

Tuy nhiên , mã của bạn chạy trên một luồng duy nhất (nghĩa là tất cả các cuộc gọi lại của các hàm Node.js sẽ được gọi trên cùng một luồng, được gọi là vòng lặp chuỗi hoặc vòng lặp sự kiện). Khi mọi người nói "Node.js chạy trên một luồng", họ thực sự đang nói "các cuộc gọi lại của Node.js chạy trên một luồng".


1
Câu trả lời ngắn gọn nhưng rõ ràng (y)
Sudhanshu Bò tót

1
câu trả lời hay Tôi sẽ thêm rằng I / O xảy ra bên ngoài vòng lặp sự kiện chính này, vòng lặp chủ đề, chuỗi yêu cầu
I Muff Popa

đó là câu trả lời tôi đã tìm kiếm từ 2 giờ, cách quản lý đồng thời trong ứng dụng đơn luồng
Muhammad Ramzan

vâng, khó có được câu trả lời 'cấp độ tiếp theo'. Điều này giải thích nơi IO thực sự được thực hiện (trong một nhóm luồng ở nơi khác)
Oliver Shaw

9

Node.js dựa trên mô hình lập trình vòng lặp sự kiện. Vòng lặp sự kiện chạy trong một chuỗi và liên tục chờ đợi các sự kiện và sau đó chạy bất kỳ trình xử lý sự kiện nào đã đăng ký vào các sự kiện đó. Sự kiện có thể là ví dụ

  • thời gian chờ đợi đã hoàn tất
  • đoạn dữ liệu tiếp theo đã sẵn sàng để ghi vào tập tin này
  • Có một yêu cầu HTTP mới sắp tới

Tất cả điều này chạy trong một luồng và không có mã JavaScript nào được thực thi song song. Miễn là những người xử lý sự kiện này còn nhỏ và chờ đợi thêm nhiều sự kiện nữa, mọi thứ sẽ diễn ra tốt đẹp. Điều này cho phép nhiều yêu cầu được xử lý đồng thời bởi một quy trình Node.js.

(Có một chút ma thuật dưới mui xe khi các sự kiện bắt nguồn. Một số trong đó liên quan đến các luồng công nhân cấp thấp chạy song song.)

Trong trường hợp SQL này, có rất nhiều điều (sự kiện) xảy ra giữa việc thực hiện truy vấn cơ sở dữ liệu và nhận kết quả của nó trong cuộc gọi lại . Trong thời gian đó, vòng lặp sự kiện tiếp tục bơm cuộc sống vào ứng dụng và thúc đẩy các yêu cầu khác từng sự kiện nhỏ. Do đó, nhiều yêu cầu đang được phục vụ đồng thời.

vòng lặp sự kiện cấp cao

Theo: "Vòng lặp sự kiện từ 10.000ft - khái niệm cốt lõi đằng sau Node.js" .


5

Hàm c.query () có hai đối số

c.query("Fetch Data", "Post-Processing of Data")

Hoạt động "Tìm nạp dữ liệu" trong trường hợp này là Truy vấn DB, bây giờ việc này có thể được xử lý bởi Node.js bằng cách sinh ra một luồng công nhân và giao cho nó nhiệm vụ thực hiện Truy vấn DB. (Hãy nhớ Node.js có thể tạo luồng trong nội bộ). Điều này cho phép chức năng trả về ngay lập tức mà không có bất kỳ độ trễ nào

Đối số thứ hai "Xử lý dữ liệu sau" là một hàm gọi lại, khung nút đăng ký cuộc gọi lại này và được gọi bởi vòng lặp sự kiện.

Do đó, câu lệnh c.query (paramenter1, parameter2)sẽ trả về ngay lập tức, cho phép nút phục vụ cho yêu cầu khác.

PS: Tôi mới bắt đầu hiểu nút, thực sự tôi muốn viết bài này dưới dạng bình luận cho @Philip nhưng vì không có đủ điểm danh tiếng nên đã viết nó như một câu trả lời.


3

nếu bạn đọc thêm một chút - "Tất nhiên, trong phần phụ trợ, có các luồng và quy trình để truy cập DB và thực thi quy trình. Tuy nhiên, những điều này không được hiển thị rõ ràng với mã của bạn, vì vậy bạn không thể lo lắng về chúng ngoài việc biết rằng các tương tác I / O, ví dụ với cơ sở dữ liệu hoặc với các quy trình khác sẽ không đồng bộ theo quan điểm của từng yêu cầu do kết quả từ các luồng đó được trả về qua vòng lặp sự kiện cho mã của bạn. "

about - "mọi thứ chạy song song ngoại trừ mã của bạn" - mã của bạn được thực thi đồng bộ, bất cứ khi nào bạn gọi một hoạt động không đồng bộ như chờ IO, vòng lặp sự kiện sẽ xử lý mọi thứ và gọi lại cuộc gọi lại. nó chỉ không phải là một cái gì đó bạn phải suy nghĩ về.

trong ví dụ của bạn: có hai yêu cầu A (đến trước) và B. bạn thực hiện yêu cầu A, mã của bạn tiếp tục chạy đồng bộ và thực hiện yêu cầu B. vòng lặp sự kiện xử lý yêu cầu A, khi nó kết thúc yêu cầu gọi lại yêu cầu A với kết quả, cùng đi đến yêu cầu B.


3
"Tất nhiên, trong phần phụ trợ, có các luồng và quy trình để truy cập DB và thực thi quy trình. Tuy nhiên, chúng không được hiển thị rõ ràng với mã của bạn" - Nếu tôi lấy từ cụm từ này, thì tôi không thấy bất kỳ sự khác biệt nào giữa Node làm hoặc bất kỳ khung công tác đa luồng nào - giả sử Spring Framework của Java - thực hiện. Có chủ đề, nhưng bạn không kiểm soát sáng tạo của họ.
Rafael Eyng

@RafaelEyng Tôi nghĩ rằng để xử lý chuỗi nhiều yêu cầu, nút sẽ luôn có một luồng cho điều đó. Tôi không chắc chắn nếu mỗi cuộc gọi lại được đưa vào phiên bản mới của các luồng ngoài các quy trình khác như truy cập db nhưng ít nhất chúng tôi chắc chắn biết rằng nút đó không khởi tạo các luồng mỗi khi nó nhận được yêu cầu sẽ phải chờ xếp hàng trước khi xử lý (thực thi trước cuộc gọi lại).
Cerberus lạnh

1

Được rồi, hầu hết mọi thứ nên rõ ràng cho đến nay ... phần khó khăn là SQL : nếu nó không thực sự chạy trong một luồng khác hoặc xử lý toàn bộ, thì việc thực thi SQL phải được chia thành các bước riêng lẻ (bởi một Bộ xử lý SQL được tạo để thực thi không đồng bộ!), Trong đó các bộ chặn không được thực thi và các bộ chặn (ví dụ như chế độ ngủ) thực sự có thể được chuyển đến kernel (dưới dạng ngắt / báo động sự kiện) và đưa vào danh sách sự kiện cho vòng lặp chính.

Điều đó có nghĩa là, ví dụ, việc giải thích SQL, v.v ... được thực hiện ngay lập tức, nhưng trong thời gian chờ đợi (được lưu trữ như một sự kiện sẽ xảy ra trong tương lai bởi hạt nhân trong một số cấu trúc kqueue, epoll, ...; cùng với các hoạt động IO khác ) vòng lặp chính có thể làm những việc khác và cuối cùng kiểm tra xem có điều gì đã xảy ra với những IO đó và chờ đợi không.

Vì vậy, để viết lại nó một lần nữa: chương trình không bao giờ (được phép nhận) bị kẹt, các cuộc gọi ngủ không bao giờ được thực hiện. Nhiệm vụ của họ được thực hiện bởi kernel (viết một cái gì đó, đợi một cái gì đó đi qua mạng, chờ thời gian trôi qua) hoặc một luồng hoặc quá trình khác. - Quá trình Node kiểm tra xem ít nhất một trong số các nhiệm vụ đó được kết thúc bởi kernel trong lệnh gọi chặn duy nhất tới HĐH một lần trong mỗi chu kỳ vòng lặp sự kiện. Điểm đó đạt được, khi mọi thứ không chặn được thực hiện.

Thông thoáng? :-)

Tôi không biết Node. Nhưng c.query đến từ đâu?


kqueue epoll dành cho thông báo I / O không đồng bộ có thể mở rộng trong kernel linux. Nút có libuv cho điều đó. Nút hoàn toàn trên userland. Nó không phụ thuộc vào nhân thực hiện.
dùng568109

1
@ user568109, libuv là người trung gian của Node. Bất kỳ khung không đồng bộ nào phụ thuộc (trực tiếp hoặc không) vào một số hỗ trợ I / O không đồng bộ trong kernel. Vì thế?
Robert Siemer

Xin lỗi vì sự nhầm lẫn. Các hoạt động của socket yêu cầu I / O không chặn từ kernel. Nó chăm sóc việc xử lý không đồng bộ. Nhưng I / O tệp không đồng bộ được xử lý bởi chính libuv. Câu trả lời của bạn không nói lên điều đó. Nó coi cả hai như nhau, được xử lý bởi kernel.
dùng568109
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.