Khi nào thì thread pool được sử dụng?


104

Vì vậy, tôi đã hiểu về cách thức hoạt động của Node.js: nó có một luồng lắng nghe duy nhất nhận một sự kiện và sau đó ủy quyền nó cho một nhóm công nhân. Chuỗi công nhân sẽ thông báo cho người nghe sau khi nó hoàn thành công việc và sau đó người nghe sẽ trả lại phản hồi cho người gọi.

Câu hỏi của tôi là: nếu tôi dựng một máy chủ HTTP trong Node.js và gọi chế độ ngủ trên một trong các sự kiện đường dẫn được định tuyến của tôi (chẳng hạn như "/ test / sleep"), toàn bộ hệ thống sẽ dừng lại. Ngay cả chủ đề người nghe duy nhất. Nhưng sự hiểu biết của tôi là mã này đang xảy ra trên nhóm công nhân.

Ngược lại, bây giờ khi tôi sử dụng Mongoose để nói chuyện với MongoDB, việc đọc DB là một thao tác I / O tốn kém. Node dường như có thể ủy quyền công việc cho một luồng và nhận lệnh gọi lại khi nó hoàn thành; thời gian tải từ DB dường như không chặn hệ thống.

Làm thế nào để Node.js quyết định sử dụng luồng nhóm luồng so với luồng người nghe? Tại sao tôi không thể viết mã sự kiện ở chế độ ngủ và chỉ chặn một chuỗi nhóm luồng?


@Tobi - Tôi đã thấy điều đó. Nó vẫn không trả lời câu hỏi của tôi. Nếu công việc nằm trên một chuỗi khác, chế độ ngủ sẽ chỉ ảnh hưởng đến chuỗi đó chứ không ảnh hưởng đến người nghe.
Haney

8
Một câu hỏi chân thực, nơi bạn cố gắng hiểu điều gì đó một mình và khi bạn không thể tìm thấy lối ra vào mê cung, bạn yêu cầu trợ giúp.
Rafael Eyng

Câu trả lời:


240

Sự hiểu biết của bạn về cách hoạt động của nút là không chính xác ... nhưng đó là một quan niệm sai lầm phổ biến, bởi vì thực tế của tình huống thực sự khá phức tạp và thường được đúc kết thành các cụm từ nhỏ bé như "nút là một chuỗi đơn" đơn giản hóa mọi thứ .

Hiện tại, chúng tôi sẽ bỏ qua đa xử lý / đa luồng rõ ràng thông qua các chuỗi cụmwebworker , và chỉ nói về nút không phân luồng điển hình.

Nút chạy trong một vòng lặp sự kiện duy nhất. Đó là một luồng duy nhất và bạn chỉ có thể nhận được một luồng đó. Tất cả javascript bạn viết đều thực thi trong vòng lặp này và nếu một hoạt động chặn xảy ra trong mã đó, thì nó sẽ chặn toàn bộ vòng lặp và không có gì khác sẽ xảy ra cho đến khi nó kết thúc. Đây là bản chất thường đơn luồng của nút mà bạn đã nghe rất nhiều về. Nhưng, nó không phải là toàn bộ bức tranh.

Một số chức năng và mô-đun nhất định, thường được viết bằng C / C ++, hỗ trợ I / O không đồng bộ. Khi bạn gọi các hàm và phương thức này, chúng quản lý nội bộ việc chuyển lệnh gọi tới một chuỗi công nhân. Ví dụ: khi bạn sử dụng fsmô-đun để yêu cầu một tệp, fsmô-đun chuyển lệnh gọi đó đến một chuỗi công nhân và trình xử lý đó đợi phản hồi của nó, sau đó nó sẽ trình bày trở lại vòng lặp sự kiện đã được kích hoạt mà không có trong chuỗi chờ đợi. Tất cả những điều này được trừu tượng hóa khỏi bạn, nhà phát triển nút và một số trong số đó được tóm tắt khỏi các nhà phát triển mô-đun thông qua việc sử dụng libuv .

Như đã chỉ ra bởi Denis Dollfus trong phần nhận xét (từ câu trả lời này cho một câu hỏi tương tự), chiến lược được sử dụng bởi libuv để đạt được I / O không đồng bộ không phải lúc nào cũng là một nhóm luồng, cụ thể là trong trường hợp của httpmô-đun, một chiến lược khác dường như là được sử dụng tại thời điểm này. Đối với mục đích của chúng tôi ở đây, điều quan trọng chủ yếu là phải lưu ý cách đạt được ngữ cảnh không đồng bộ (bằng cách sử dụng libuv) và nhóm luồng được duy trì bởi libuv là một trong nhiều chiến lược được cung cấp bởi thư viện đó để đạt được sự không đồng bộ.


Về một tiếp tuyến chủ yếu có liên quan, có một phân tích sâu hơn về cách nút đạt được sự không đồng bộ và một số vấn đề tiềm ẩn liên quan và cách giải quyết chúng, trong bài viết tuyệt vời này . Hầu hết nó mở rộng những gì tôi đã viết ở trên, nhưng ngoài ra nó còn chỉ ra:

  • Bất kỳ mô-đun bên ngoài nào mà bạn đưa vào dự án của mình sử dụng C ++ và libuv nguyên bản đều có thể sử dụng nhóm luồng (hãy nghĩ: truy cập cơ sở dữ liệu)
  • libuv có kích thước nhóm luồng mặc định là 4 và sử dụng một hàng đợi để quản lý quyền truy cập vào nhóm luồng - kết quả là nếu bạn có 5 truy vấn DB chạy dài cùng một lúc, thì một trong số chúng (và bất kỳ truy vấn không đồng bộ nào khác hành động dựa trên nhóm luồng) sẽ đợi những truy vấn đó kết thúc trước khi chúng bắt đầu
  • Bạn có thể giảm thiểu điều này bằng cách tăng kích thước của nhóm luồng thông qua UV_THREADPOOL_SIZEbiến môi trường, miễn là bạn làm điều đó trước khi nhóm luồng được yêu cầu và tạo:process.env.UV_THREADPOOL_SIZE = 10;

Nếu bạn muốn đa xử lý truyền thống hoặc đa luồng trong nút, bạn có thể lấy nó thông qua clustermô-đun tích hợp sẵn hoặc nhiều mô-đun khác như mô-đun đã nói ở trên webworker-threadshoặc bạn có thể giả mạo nó bằng cách thực hiện một số cách phân chia công việc của bạn và sử dụng thủ công setTimeouthoặc setImmediatehoặc process.nextTicktạm dừng công việc của bạn và tiếp tục nó trong vòng lặp sau để cho phép các quy trình khác hoàn thành (nhưng điều đó không được khuyến nghị).

Xin lưu ý, nếu bạn đang viết mã chặn / chạy dài bằng javascript, có thể bạn đang mắc lỗi. Các ngôn ngữ khác sẽ hoạt động hiệu quả hơn nhiều.


1
Khỉ thật, điều này hoàn toàn giải quyết cho tôi. Cảm ơn bạn rất nhiều @Jason!
Haney

5
Không thành vấn đề :) Tôi đã tìm thấy chính mình nơi bạn đang ở cách đây không lâu, và thật khó để đi đến một câu trả lời được xác định rõ ràng bởi vì một bên là bạn có các nhà phát triển C / C ++ mà câu trả lời là hiển nhiên, và mặt khác, bạn có những nhà phát triển web chưa từng nghiên cứu quá sâu về những loại câu hỏi này trước đây. Tôi thậm chí không chắc câu trả lời của mình là đúng 100% về mặt kỹ thuật khi bạn xuống cấp độ C, nhưng nó đúng về mặt kỹ thuật.
Jason

3
Sử dụng nhóm luồng cho các yêu cầu mạng sẽ là một sự lãng phí tài nguyên rất lớn. Theo câu hỏi này "Nó thực hiện I / O mạng không đồng bộ dựa trên các giao diện I / O không đồng bộ trong các nền tảng khác nhau, chẳng hạn như epoll, kqueue và IOCP, không có nhóm luồng" - điều này có ý nghĩa.
Denis Dollfus

1
... điều đó nói rằng, nếu bạn thực hiện một số thao tác nặng trong luồng javascript chính trực tiếp hoặc bạn không có đủ tài nguyên hoặc không quản lý chúng một cách thích hợp để cung cấp đủ khoảng trống cho luồng luồng, bạn có thể gây ra độ trễ ở mức đồng thời thấp hơn ngưỡng - kết quả là, đối với cùng một tài nguyên hệ thống, thông thường bạn sẽ trải nghiệm thông lượng cao hơn với node.js so với các tùy chọn khác (mặc dù có các hệ thống dựa trên sự kiện bằng các ngôn ngữ khác nhằm mục đích thách thức điều đó - tôi chưa đã thấy các điểm chuẩn gần đây) - rõ ràng là mô hình dựa trên sự kiện hoạt động tốt hơn mô hình phân luồng.
Jason

1
@Aabid Luồng trình xử lý không thực thi truy vấn cơ sở dữ liệu, vì vậy sẽ mất khoảng 6 giây để hoàn thành tất cả 10 truy vấn đó (theo kích thước nhóm luồng mặc định là 4). Nếu bạn cần thực hiện bất kỳ công việc nào trong javascript mà không yêu cầu hoàn thành kết quả của truy vấn cơ sở dữ liệu đó, ví dụ: có nhiều yêu cầu hơn mà không yêu cầu bất kỳ công việc không đồng bộ nào được hoàn thành bởi nhóm luồng, nó sẽ tiếp tục hoạt động trong chính vòng lặp sự kiện.
Jason

20

Vì vậy, tôi đã hiểu về cách thức hoạt động của Node.js: nó có một luồng lắng nghe duy nhất nhận một sự kiện và sau đó ủy quyền nó cho một nhóm công nhân. Chuỗi công nhân sẽ thông báo cho người nghe sau khi nó hoàn thành công việc và sau đó người nghe sẽ trả lại phản hồi cho người gọi.

Điều này không thực sự chính xác. Node.js chỉ có một luồng "worker" duy nhất thực thi javascript. Có các luồng bên trong nút xử lý quá trình xử lý IO, nhưng việc coi chúng là "công nhân" là một quan niệm sai lầm. Thực sự chỉ có xử lý IO và một vài chi tiết khác về triển khai nội bộ của nút, nhưng với tư cách là một lập trình viên, bạn không thể ảnh hưởng đến hành vi của họ ngoài một vài tham số sai như MAX_LISTENERS.

Câu hỏi của tôi là: nếu tôi dựng một máy chủ HTTP trong Node.js và gọi chế độ ngủ trên một trong các sự kiện đường dẫn được định tuyến của tôi (chẳng hạn như "/ test / sleep"), toàn bộ hệ thống sẽ dừng lại. Ngay cả chủ đề người nghe duy nhất. Nhưng sự hiểu biết của tôi là mã này đang xảy ra trên nhóm công nhân.

Không có cơ chế ngủ trong JavaScript. Chúng tôi có thể thảo luận về vấn đề này một cách cụ thể hơn nếu bạn đăng một đoạn mã về ý nghĩa của "ngủ". Chẳng hạn, không có chức năng nào như vậy để gọi mô phỏng một cái gì đó giống như time.sleep(30)trong python. Có setTimeoutnhưng về cơ bản đó KHÔNG phải là ngủ. setTimeoutgiải phóngsetInterval một cách rõ ràng , không phải khối, vòng lặp sự kiện để các bit mã khác có thể thực thi trên chuỗi thực thi chính. Điều duy nhất bạn có thể làm là bận lặp CPU với tính toán trong bộ nhớ, điều này thực sự sẽ bỏ đói luồng thực thi chính và khiến chương trình của bạn không phản hồi.

Làm thế nào để Node.js quyết định sử dụng luồng nhóm luồng so với luồng người nghe? Tại sao tôi không thể viết mã sự kiện ở chế độ ngủ và chỉ chặn một chuỗi nhóm luồng?

Mạng IO luôn không đồng bộ. Kết thúc câu chuyện. Disk IO có cả API đồng bộ và không đồng bộ, vì vậy không có "quyết định". node.js sẽ hoạt động theo các chức năng cốt lõi của API mà bạn gọi là đồng bộ hóa so với không đồng bộ thông thường. Ví dụ: fs.readFilevs fs.readFileSync. Đối với các quy trình con, cũng có các API child_process.execvà riêng biệt child_process.execSync.

Nguyên tắc chung là luôn sử dụng các API không đồng bộ. Các lý do hợp lệ để sử dụng các API đồng bộ là để khởi tạo mã trong một dịch vụ mạng trước khi nó lắng nghe các kết nối hoặc trong các tập lệnh đơn giản không chấp nhận các yêu cầu mạng cho các công cụ xây dựng và những thứ tương tự.


1
Các API không đồng bộ này đến từ đâu? Tôi hiểu những gì bạn đang nói, nhưng bất kỳ ai viết API này đã chọn tham gia IOCP / async. Làm thế nào họ chọn để làm điều này?
Haney

3
Câu hỏi của anh ta là làm thế nào anh ta sẽ viết mã tốn nhiều thời gian của riêng mình và không chặn.
Jason

1
Đúng. Node cung cấp kết nối mạng UDP, TCP và HTTP cơ bản. Nó CHỈ cung cấp các API "dựa trên nhóm" không đồng bộ. Tất cả mã node.js trên thế giới không có ngoại lệ sử dụng các API không đồng bộ dựa trên nhóm này vì đơn giản là có tất cả những gì có sẵn. Hệ thống tệp và các quy trình con là một câu chuyện khác, nhưng mạng nhất quán là không đồng bộ.
Peter Lyons

4
Cẩn thận đấy, Peter, kẻo cậu trở thành cái bình đối với ấm nước của anh ấy. Anh ấy muốn biết những người viết API mạng đã làm điều đó như thế nào, chứ không phải những người sử dụng API mạng làm điều đó như thế nào. Cuối cùng tôi đã hiểu được cách nút hoạt động với các sự kiện không chặn bởi vì tôi muốn viết mã không chặn của riêng mình không liên quan đến mạng hoặc bất kỳ API không đồng bộ nào khác được tích hợp sẵn. Rõ ràng là David cũng muốn làm như vậy.
Jason

2
Nút không sử dụng hồ bơi thread cho IO, nó sử dụng có nguồn gốc non-blocking IO, ngoại lệ duy nhất là fs, như xa như tôi biết
vkurchatkin

2

Nhóm chủ đề như thế nào và ai đã sử dụng:

Đầu tiên khi chúng ta sử dụng / cài đặt Node trên máy tính, nó sẽ bắt đầu một quá trình trong số các quá trình khác được gọi là quá trình nút trong máy tính và nó sẽ tiếp tục chạy cho đến khi bạn giết nó. Và quá trình đang chạy này được gọi là luồng đơn của chúng tôi.

nhập mô tả hình ảnh ở đây

Vì vậy, cơ chế của một luồng nó dễ dàng chặn một ứng dụng nút nhưng đây là một trong những tính năng độc đáo mà Node.js mang lại. Vì vậy, một lần nữa nếu bạn chạy ứng dụng nút của mình, nó sẽ chỉ chạy trong một luồng duy nhất. Không có vấn đề nếu bạn có 1 hoặc hàng triệu người dùng truy cập vào ứng dụng của bạn cùng một lúc.

Vì vậy, hãy hiểu chính xác những gì sẽ xảy ra trong chuỗi đơn của nodejs khi bạn khởi động ứng dụng nút của mình. Lúc đầu chương trình được khởi tạo, sau đó tất cả mã cấp cao nhất được thực thi, có nghĩa là tất cả các mã không nằm trong bất kỳ hàm gọi lại nào ( hãy nhớ tất cả các mã bên trong tất cả các hàm gọi lại sẽ được thực thi theo vòng lặp sự kiện ).

Sau đó, tất cả các mã mô-đun được thực thi rồi đăng ký tất cả các lệnh gọi lại, cuối cùng, vòng lặp sự kiện bắt đầu cho ứng dụng của bạn.

nhập mô tả hình ảnh ở đây

Vì vậy, như chúng ta đã thảo luận trước khi tất cả các hàm gọi lại và mã bên trong các hàm đó sẽ thực thi trong vòng lặp sự kiện. Trong vòng lặp sự kiện, các tải được phân phối theo các giai đoạn khác nhau. Dù sao, tôi sẽ không thảo luận về vòng lặp sự kiện ở đây.

Để hiểu rõ hơn về Thread pool, tôi yêu cầu bạn tưởng tượng rằng trong vòng lặp sự kiện, các mã bên trong một hàm gọi lại thực thi sau khi hoàn thành việc thực thi các mã bên trong một hàm gọi lại khác, bây giờ nếu có một số tác vụ thực sự quá nặng. Sau đó, họ sẽ chặn luồng đơn nodejs của chúng tôi. Và vì vậy, đó là nơi luồng luồng đi vào, giống như vòng lặp sự kiện, được cung cấp cho Node.js bởi thư viện libuv.

Vì vậy, nhóm luồng không phải là một phần của bản thân nodejs, nó được cung cấp bởi libuv để giảm tải các nhiệm vụ nặng nề cho libuv và libuv sẽ thực thi các mã đó trong các luồng của chính nó và sau khi thực thi libuv sẽ trả về kết quả cho sự kiện trong vòng lặp sự kiện.

nhập mô tả hình ảnh ở đây

Nhóm luồng cung cấp cho chúng ta bốn luồng bổ sung, những luồng này hoàn toàn tách biệt với luồng đơn chính. Và chúng tôi thực sự có thể cấu hình nó lên đến 128 luồng.

Vì vậy, tất cả các chủ đề này cùng nhau tạo thành một nhóm chủ đề. và vòng lặp sự kiện sau đó có thể tự động giảm tải các tác vụ nặng vào nhóm luồng.

Phần thú vị là tất cả những điều này diễn ra tự động ở hậu trường. Không phải chúng tôi là các nhà phát triển quyết định cái gì đi vào nhóm luồng và cái gì không.

Có nhiều tác vụ được đưa vào nhóm luồng, chẳng hạn như

-> All operations dealing with files
->Everyting is related to cryptography, like caching passwords.
->All compression stuff
->DNS lookups

0

Sự hiểu lầm này chỉ đơn thuần là sự khác biệt giữa đa nhiệm ưu tiên trước và đa nhiệm hợp tác ...

Giấc ngủ sẽ tắt toàn bộ lễ hội bởi vì thực sự có một hàng cho tất cả các chuyến đi, và bạn đã đóng cổng. Hãy coi nó như "một trình thông dịch JS và một số thứ khác" và bỏ qua các luồng ... đối với bạn, chỉ có một luồng, ...

... vì vậy đừng chặn nó.

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.