Javascript - Single-thread liệu đã lỗi thời?
Sống trong 1 thế giới công nghệ thay đổi đến chóng mặt, trong trí nhớ của tôi thì mấy con PC những năm tôi học cấp 2, cấp 3 tầm 200x cấu hình còn không mạnh bằng smartphone bây giờ nữa. Khoảng năm 2005 trở về trước là thời đại của Pentium 4 và Athlon64, bước ngoặt có lẽ là khi Intel ra mắt ...
Sống trong 1 thế giới công nghệ thay đổi đến chóng mặt, trong trí nhớ của tôi thì mấy con PC những năm tôi học cấp 2, cấp 3 tầm 200x cấu hình còn không mạnh bằng smartphone bây giờ nữa.
Khoảng năm 2005 trở về trước là thời đại của Pentium 4 và Athlon64, bước ngoặt có lẽ là khi Intel ra mắt Pentium D, còn AMD ra mắt Athlon 64 X2, đánh dấu kỉ nguyên của chip multi-core.
Trước đấy thì đa phần các nhà sản xuất CPU đua performance với nhau bằng xung nhịp (clock speed - bị ảnh hưởng bởi số lượng, kích thước của các transistor). Đơn giản thời kỳ này so chip là so con nào clock speed cao hơn là con đấy win. Sau này việc thu nhỏ transistor hay tăng clock speed ngày càng trở nên khó khăn hơn do các vấn đề về giới hạn vật lý, chi phí, v..v.. buộc các NSX chip phải phát triển những phương án khác để gia tăng hiệu năng CPU. Cuộc đua clock speed dừng lại tại đây, con chip có xung nhịp cao nhất thời bấy giờ là Pentium EE với mức xung nhịp lên tới 4.0Ghz, và cho đến bây giờ nó vẫn nằm top những CPU có clock speed cao nhất thế giới - nhưng tất nhiên là hiệu năng thì chắc bằng không bằng 1/10 mấy em chip bây giờ.
CPU multi-core ra đường trong hoàn cảnh này và cuộc đua số lượng core trong CPU lại 1 lần nữa được bắt đầu, lần này không chỉ gói trong mảng PC, mà các NSX mobile chip cũng tham gia vào.
Cùng với sự xuất hiện của CPU multi-core là cũng là sự chuyển mình của ngành công nghiệp phần mềm. Chip multi-core thời kỳ đầu không đạt được kỳ vọng về hiệu năng 1 phần do phần mềm ít hỗ trợ. Nhưng dần dần các nhà phát triển cũng đã refactor lại theo hướng mới này.
Những khái niệm multi-processes , multi-threads,.. như kim chỉ nam cho bất cứ dev nào muốn become guru. Các ngôn ngữ thời thượng C#, C++, Java, cũng đều support multi-threading, các máy chủ web Apache, IIS,.. cũng là những ứng dụng multi-thread, nói chung là người người, nhà nhà multi-thread.
Ấy vậy mà trong thế giới đa luồng - đa tiến trình ấy, Javascript vẫn nổi lên là 1 trong những ngôn ngữ thông dụng nhất thế giới và thậm chí Nodejs run-time environment còn đang lăm le soán ngôi của các ngôn ngữ kỳ cựu khác trong cuộc chiến server-side.
Nhưng javascript - và Nodejs environment - hoàn toàn là 1 ngôn ngữ đơn luồng (single - thread)
Liệu điều này có đi ngược lại quy luật phát triển không? Chắc chắn về mặt nhận thức thực tế thì là không rồi. Người ta đang dùng nó ngày 1 nhiều và nhất là về tốc độ thì nó đang tỏ ra vượt trội so với các đối thủ khác. Server xây dựng bằng Nodejs có khả năng chịu được nhiều hơn hàng ngàn connection đồng thời so với Apache, IIS ,...
Vậy liệu gì đã biến Javascript trở nên như vậy, chúng ta sẽ cùng tìm hiểu qua bài viết này nhé các bạn !
Trước khi bắt đầu, tôi remind lại 1 chút cho các bạn là Nodejs bản chất của nó là 1 runtime-environment dùng để chạy code javascipt, điều đó có nghĩa là nodejs chính là code javascript và tất nhiên chúng cũng chia sẻ nhau những khái niệm về cấu trúc và cách hoạt động.
Bởi vì có những bài viết trên mạng thậm chí từ nguồn uy tín định nghĩa Nodejs là 1 ngôn ngữ => điều này là không đúng.
Threads và Processes
Đầu tiên chúng ta sẽ quay lại lịch sử 1 chút, thời kỳ đầu thì các máy tính chỉ có thể làm 1 việc vào mỗi thời điểm chứ không đa tác vụ như bây giờ. Tức là bạn chỉ có thể làm được 1 trong 2 việc: "gõ văn bản" hay "nghe nhạc" chứ không thể làm được cả 2 việc này cùng 1 lúc.
Tất cả các chương trình đều chạy 1 cách tuần tự, chương trình này chạy hết rồi mới đến chương trình tiếp theo, đây gọi là batch processing. Với sự mạnh mẽ lên từng ngày của phần cứng, batch processing gây nên lãng phí tài nguyên, 1 tác vụ đơn giản không thể tận dụng được toàn bộ sức mạnh của CPU.
Các hệ điều hành Multitasking ra đời khắc phục nhược điểm này, các hệ thống multitasking cho phép người dùng chuyển đổi giữa các tác vụ như nghe nhạc, lướt web và làm chúng dường như "có thể" chạy 1 cách đồng thời.
Tại sao tôi dùng từ có thể thì chúng ta sẽ làm rõ hơn vấn đề này bên dưới. Note lại nhé.
Sự ra đời của Multitasking OS kéo theo 2 khái niệm: Thread và Process
Process
Process là 1 tiến trình => Khi bạn click đúp vào icon Chrome => 1 process chrome đã được khởi tạo.
Mỗi process khi được khởi tạo sẽ chạy 1 thread chính, tuy nhiên nó cũng có thể tạo ra nhiều thread con trong quá trình hoạt động.
Thread
Thread là 1 thực thể hiện hữu trong process. Có thể coi thread là 1 process con của process cha. 1 Process có thể có nhiều thread con.
1 đặc điểm quan trọng của thread là trình tự thực thi trong CPU của nó có thể sắp xếp (schedule) được, là tiền đề để máy tính có thể xử lý đa tác vụ
Để so sánh kỹ hơn về process và thread các bạn có thể tham khảo ở đây
Máy tính xử lý đa tác vụ như thế nào?
Các bạn còn nhớ tôi đã dùng "có thể" khi nói về cách các hệ điều hành xử lý đa tác vụ ? Đúng vậy, trên thực tế CPU đơn nhân không thể chạy được nhiều hơn 1 tác vụ tại mỗi thời điểm, nó không khác gì CPU từ những thế kỷ trước cả. Vậy làm cách nào hệ điều hành có thể biến quy trình xử lý tuần tự trở thành quy trình xử lý đa tác vụ?
Nếu như các bạn đã biết thì mỗi tác vụ - ngay cả đơn giản như 1 click chuột - bao gồm hàng ngàn, hàng vạn , thậm chí hàng triệu phép tính => lợi dụng đặc điểm này mà các nhà phát triển nghĩ ra cách cho phép OS chỉ thực thi 1 phần các phép tính của tác vụ này sau đó chuyển sang làm tác vụ khác. Và vì mỗi phép tính là cực kỳ nhanh nên nó làm cho ta bị đánh lừa rằng máy tính đang làm nhiều tác vụ cùng 1 lúc.
Thôi nói nhảm đi, zing mp3 của tôi không bị khựng lại tí nào trong lúc tôi đang lướt facebook
Đúng vậy, OS xử lý đa tác vụ không chỉ nhờ vào tốc độ cực kỳ nhanh của CPU, 1 chiếc máy tính còn nhiều con chip khác nữa, bên trong mỗi con chip đó được lấp đầy bởi bộ nhớ cache và hàng đợi các công việc nó phải làm. CPU sẽ tính toán vài giây mp3 trước khi nó chạy tới và ném kết quả sang cho chip audio, trong lúc chip audio phát ra nhạc thì CPU rảnh rỗi để có thể xử lý tác vụ khác.
CPU ngày nay cực kỳ nhanh, tốc độ của nó được đo bằng flops (thao tác tính số thực dấu phẩy động - Floating Point Operations per second). 1 CPU thường thì có tốc độ khoảng 50 - 70 GigaFlops tương đương với 1 phép tính được xử lý trong vòng 1/triệu giây. Để so sánh thì 1 người ấn nút bàn phím nhanh nhất cỡ 4mili giây. Sau khi bấm xong nút đó thì máy tính đã xử lý xong ~200 000 phép tính rồi. Cực kỳ nhanh!
Áp dụng chung lối thiết kế đó, các chương trình ngày nay cũng được phân thành các threads hoạt động phụ thuộc vào nhau nhằm tận dụng hoàn toàn sức mạnh của CPU.
1 CPU đơn lõi xử lý multi-threading như ví dụ sau:
Cho 2 threads A và B cần phải chạy 1 tập các lệnh. Sau mỗi lệnh, threads cần kết quả trả về thì mới thực hiện lệnh tiếp theo.
CPU sẽ thực thi bắt đầu từ thread A (with multi-threading) và như tôi đã nói ở phần trên, thread có thể được sắp xếp lại trình tự hoạt động của nó khi được thực thi trong CPU, nghĩa là việc CPU thực hiện command của A => thực hiện xong nhảy sang thread B => rồi lại nhảy trở về thực hiện tiếp lệnh tiếp theo của thread A là khả thi ?!?
Thread A và Thread B được luân phiên xử lý. ĐIều này giúp cho tài nguyên CPU không bị lãng phí khi thread A chạy đến lệnh waiting, trong thời gian chờ kết quả thì CPU đã nhảy sang thực thi tập lệnh của thread B. Khi không chạy multi-threading CPU sẽ xử lý 1 cách tuần tự như sau:
Dễ dàng nhận thấy xử lý multy-threading hiệu quả và tận dụng tài nguyên tốt hơn rất nhiều! Tuy là giữa những lần thay đổi ngữ cảnh thực thi giữa 2 thread sẽ phải đánh đổi 1 lượng tài nguyên (delay) nhất định - còn gọi là time slicing - nhưng multi-thread vẫn hoàn toàn chiến thắng trong các bài test kiểu như thế này. (thậm chí với công nghệ HT - Hyper Threading - điều này còn được tối ưu hơn nữa).
Từ nãy tới giờ chỉ thấy nói đến điểm mạnh của multi-threading, vậy Javascript đứng ở đâu trong bức tranh này?
Multi-threading rất mạnh, nhưng xin được nhắc lại 1 lần nữa, js hoàn toàn là single-thread, nhưng tại sao nó lại được trọng dụng đến vậy? Để làm sáng rõ thì phần tiếp theo tôi sẽ nói 1 chút về cái hồn của js - Eventloop
Note: Eventloop tài liệu trên mạng rất nhiều, và nếu bạn nào đã biết về Eventloop thì có thể bỏ qua đọc phần next-next luôn.
Eventloop trong javascript
Tôi sẽ chỉ nói qua về event-loop trong javascript thôi, bởi vì trên mạng đã có rất nhiều tài liệu rồi, nhưng chính thống nhất thì vẫn là trang chủ Mozilla Ngoài ra tôi cũng đã đọc được 1 series khá hay và dễ hiểu dành cho những ai chưa biết tý gì về Eventloop -hongkiat
OK, vậy thì đã có ai từng tự hỏi là tại sao cơ chế lắng nghe sự kiện hay gửi request AJAX trong javascript dường như được thực hiện theo thời gian thực, bạn vẫn có thể tương tác với trang web mà đồng thời vẫn gửi được các request đi? Thực ra về mặt lý thuyết thì đây không phải là real-time mà được gọi là xử lý bất đồng bộ "ASYNCHRONOUS", và nó cũng không hoạt động theo kiểu multi-threading để xử lý nhiều việc song song nhau 1 lúc.
Javascript sử dụng mô hình xử lý Concurrency dựa trên Event-loop để xử lý bất đồng bộ.
Cũng như mọi ngôn ngữ khác, javascript được biên dịch từ trên xuống dưới và từ trái sang phải.
Khi 1 function() được gọi trong code, javascript không thực thi nó ngay mà đưa nó vào 1 hàng đợi (queue), những function() đã được đưa vào hàng đợi sẽ lần lượt được được vào Callstack ngay khi CallStack rỗng. Đây cũng là nơi function() sẽ được thực thi. Thứ tự của Queue là function nào được đưa vào trước thì sẽ được đưa sang Callstack trước, function nào sau thì sang sau.
Đối với Callstack khi 1 function được đưa vào đây nó sẽ trở thành ngữ cảnh thực thi - gọi đây là function context - và Callstack sẽ gọi đệ quy tất cả các function được function context gọi đến. Bởi vì có 1 vòng lặp chạy mãi mãi thực thi Queue nên nó có tên gọi là Event-loop.
while (queue.waitForMessage()) { queue.processNextMessage(); }
Nhưng đây không phải cách duy nhất để gọi 1 function trong javascript.
JS web API là những API được cung cấp bởi trình duyệt, nó có nhiệm vụ đưa eventHandle và callback function vào hàng đợi Queue khi có sự kiện xảy ra. Javascript dùng 1 process chạy ngầm để theo dõi DOM và thành phần khác xem khi nào có sự kiện sẽ dùng những API để đưa eventHandle vào Queue.
Timer cũng tương tự như vậy nhưng nó dùng để đưa function vào Queue khi ta sử dụng hàm setTimeout.
Thì vẫn là javascript đang thực hiện các tác vụ song song không phải vậy sao?
Không, chỉ có duy nhất 1 tác vụ được thực hiện trong 1 khoảng thời gian, hãy nhìn vào hình sau để rõ hơn, trong callstack luôn luôn chỉ có 1 function được thực thi hết function này thì tới function khác.
Vậy thì cái gì mà gọi là Concurrency, chẳng phải đây chính là cách CPU xử lý multi-threads đó sao, vậy có thể nói javascript là ngôn ngữ single-thread được ?
Đúng vậy, Concurrency là 1 khái niệm dễ gây hiểu nhầm, Concurrency có thể đem lại kết quả tương tự như khi ta xử lý đa luồng, vừa gửi request lại vừa xem video chả hạn? nhưng nó hoàn toàn không phải là xử lý mọi việc song song, CPU vẫn có thể chỉ sử dụng 1 thread duy nhất để xử lý Concurrency và tôi sẽ làm rõ vấn đề này ngay bây giờ.
Concurrency và Paralelism
Nhiều người mới tìm hiểu Javascript rất dễ nhầm lẫn xử lý Concurrency tức là xử lý song song (Paralelism). Thực ra đây là 2 khái niệm khác nhau nhưng liên quan đến nhau. Xử lý Concurrency không giống với Multi-thread, multi-core, multi-processes hay bất cứ gì tương tự như thế.
Concurrency
Concurrency là khi 2 hoặc nhiều task có thể bắt đầu, chạy và hoàn thành trong những khoảng thời gian chồng lên nhau. Nó không nhất thiết là chúng cần phải chạy cùng 1 lúc Tồn tại khi ít nhất 2 threads đang xử lý. Đây là 1 thuộc tính của chương trình hoặc hệ thống.
Parallelism
Parallelism là khi các task chạy cùng 1 lúc theo nghĩa đen. Là khi 2 threads được thực thi đồng thời. Đây là 1 hành vi của run-time thực thi đa tác vụ cùng 1 lúc.
Đúng vậy, Parallelism mới thực sự là đa tác vụ !
Lý thuyết thì hơi khó hiểu chúng ta xem thử 1 ví dụ sau:
Thời đại này ta có HHVM, ta có Node, ta có Swift, ta có Golang, Haskell.. chắc chả ai còn đọc sách lập trình C nữa, đem đốt nó đi thôi, nhưng vì cái xe và cái lò có hạn nên mỗi lần chỉ đốt được vài quyển thôi =)) (vui thôi nhé đừng gạch đá - vì thực ra tôi cũng không có quyển sách C nào =))))
OK, bây giờ thanh niên trong ví dụ sẽ phải làm những task sau :
- Chất sách vào xe
- Đẩy xe đến lò
- Đốt sách
- Đẩy xe về
Concurrency là phân chia công việc đó ra thành các phần nhỏ hơn, mà cụ thể trong trường hợp này là 4 task trên: 4 thằng, mỗi thằng 1 việc - không thằng nào chờ thằng nào hết, công việc nhanh hơn 4 lần:
Nếu có 2 cái lò, chia đống sách thành 2 phần , có thêm xe đẩy, có thêm người thì việc này còn nhanh hơn nhiều lần nữa:
Về lý thuyết, ta hoàn thành công việc nhanh hơn 16 lần.
Ví dụ chả liên quan nhỉ ?
Chưa liên quan thôi chứ không phải là không liên quan, tưởng tượng 1 chút nào :
- Đống sách: Nội dung web
- Người làm: CPU
- Xe đẩy: Băng thông mạng
- Lò: Trình duyệt
Nếu bạn đã hình dung ra thì Vâng, đây chính là thiết kế chương trình theo hướng concurrency, 1 tác vụ được chia thành nhiều phần nhỏ. Tuy thế việc nó có được xử lý đa tác vụ (multithreading) hay không thì còn tùy thuộc vào ngôn ngữ lập trình và CPU (số lượng cores - và các tập lệnh trên CPU)
Công việc đốt sách sẽ không thể nhanh hơn 16 lần, hay 4 lần nếu như trong 1 khoảng thời gian chỉ có 1 người làm việc (1 CPU core). Đây chính là khi chương trình được viết theo hướng concurrency nhưng không được runtime xử lý parallel, hay nói cách khác nó là single-thread.
Với 2 CPU tốc độ về lý thuyết tăng lên gấp 2, đây là điều mà ở thời kỳ đầu phát triển của CPU multicore không thể đạt được dù là trên lý thuyết do các chương trình không được viết theo hướng concurrency. Tất nhiên với những chương trình thiết kế tốt 16 cores của CPU được tận dụng hoàn toàn đạt được tốc độ xử lý nhanh gấp 16 lần so với 1 core. Tuyệt vời !!!
Tóm lại, JavaScript là Concurrency chứ không phải Parallelism
Single-thread ? Is it good?
Như vậy chúng ta đã thống nhất với nhau JS là 1 ngôn ngữ thuần đơn luồng, vậy lý do gì khiến nó trở nên đặc biệt?
Trong PHP (hoặc Java/ASP.NET/Ruby) web-server mỗi client request sẽ được khởi tạo 1 thread mới, những trong Nodejs tất cả các client chạy trên cùng 1 thread - thậm chí chia sẻ nhau chung biến). Eventloop cho phép chúng ta làm điều này bởi vì nó không block thread chính.
Nhưng nếu có 1 tác vụ nặng chạy trên CPU server node thì nó sẽ block thread này lại và những connection tiếp theo sẽ phải chờ thread xử lý xong tác vụ. Điều này không xảy ra với những webserver multithreads khi cho phép các tác vụ chạy trên những thread khác nhau.
Có thể thấy là mỗi cái đều có điểm lợi và hại
Nodejs được tạo ra 1 cách chính thức sau 1 nghiên cứu về tiến trình async( async process). Theo lý thuyết này thì xử lý async đơn luồng sẽ mang lại nhiều hiệu năng và khả năng mở rộng hơn cho trình duyệt web. Và lý thuyết này đã được thực tế xác minh. 1 nodejs có thể chạy nhiều hơn hàng ngàn connection so với máy chủ Apache hoặc IIS. Thường thì các máy chủ PHP/Java/ASP.NET/Ruby mỗi connection sẽ tạo ra 1 thread. Nhưng Nodejs thì tất cả client đều chạy trên cùng 1 thread. Nodejs được tạo ra 1 cách có chủ đích như vậy và sẽ KHÔNG BAO GIỜ hỗ trợ đa luồng.
Để giải quyết vấn đề khi có những process nặng chạy trên server node chúng ta sẽ chọn giải pháp là tạo ra các child process, từ 1 cái process to chia ra làm các cái nhỏ thì sẽ đơn giản hơn nếu làm ngược lại các bạn ạ. Làm sao để mở rộng máy chủ node
No locking bugs/ No Overloading
Mỗi 1 task trong JS đòi hỏi 1 lượng rất nhỏ trong bộ nhớ Heap do nó chạy trên chỉ 1 thread. Còn việc tạo ra thread mới thì đòi hỏi nhiều bộ nhớ hơn nhiều, vài MB đối với 1 số nền tảng. Nhưng nhược điểm chí mạng của đa luồng tới từ việc chuyển đổi ngữ cảnh thực thi. Khi số lượng thread lên tới con số trăm ngàn, thậm chí hàng triệu thì việc chuyển đổi thread là hết sức nặng nề.
Nếu ở trên bạn nhớ tôi đã nói vấn đề delay (time slicing) khi chuyển đổi thread trong CPU là không đáng kể, điều đó vẫn đúng nhưng là với máy tính cá nhân, rất ít máy tính cá nhân đạt đến con số vài ngàn threads, còn với máy chủ server thì con số hàng triệu, thậm chí hàng chục triệu threads thì đây thực sự là 1 vấn đề lớn.
Việc lập trình tốt multithread cũng khó hơn rất rất nhiều, để các threads tương tác với nhau tốt cũng là một vấn đề lớn phải quan tâm.
No DOM collapse
Còn javascript phía client-side thì sao? 1 khi code khởi tạo chạy xong (khi trang web đã được load và js code đã chạy, tất cả event handler đã được gắn vào element của nó, AJAX request thì đã được gửi) và trình duyệt đã bắt đầu lắng nghe sự kiện DOM.
Ngay tại lúc này , sao chúng ta lại không handle các event theo cách parallelism truyền thống? Hay nói cách khác là sử dụng nhiều thread để điều khiển DOM và các event gắn với nó, chắc chắn sẽ giúp hiệu năng tăng lên như đã phân tích phía trên, nó cũng không bị gò bó bởi số lượng threads như khi triển khai code server-side. Giả như nếu code js có thể thực thi parallelism, thì rõ ràng ta đủ quyền năng để khống chế số lượgn threads đảm bảo tối ưu hiệu năng.
Nhưng tại sao những nhà phát triển JS lại không làm như vậy?
Lý do là bởi vì các bạn sẽ không muốn thao túng DOM bằng nhiều threads đâu. DOM tree không phải 1 thứ an toàn để chỉnh sửa, nó rất dễ trở thành 1 mớ hỗn loạn nếu cứ cố gắng làm điều đó. Việc sử dụng nhiều thread thao túng DOM có thể ví von với việc ta làm ô nhiễm biến tòan cục bởi nhiều function liên kết với nhau hết sức chặt chẽ. Mà như đã biết, lập trình dependency threads thậm chí còn khó khăn hơn nhiều và không có gì đảm bảo nó có thể chạy trơn tru, chỉ 1 sai sót nhỏ và cái ta nhận được sẽ là DOM tree collapse.
Is it enough?
Đúng với câu, chất lượng hơn số lượng, với 2 đặc điểm nổi bật đã nêu giúp JS tranh bá với các ngôn ngữ khác bất chấp việc nó còn rất nhiều thiếu sót
Giờ đây ,JS gần như là sự lựa chọn duy nhất khi lập trình web client-side , Nodejs thì ngày càng lớn mạnh vững chắc.
Nhưng các bạn thấy đấy, multithreading là xu hướng, là tương lai của công nghiệp phần mềm, làm web bằng JS tốt không có nghĩa là nó là hoàn hảo. Thậm chí ngay cả khi làm web cũng còn nhiều tranh cãi không có hồi kết giữa các ngôn ngữ với nhau. Dù sao thì mục đích của bài viết không phải là so sánh giữa các ngôn ngữ mà để đi tới 1 kết luận:
Single-thread hữu dụng và sẽ vẫn còn hữu dụng trong tương lai
Và tôi cũng muốn nhắc bạn 1 câu cũ rích, không phải cứ công nghệ mới hơn là tốt hơn và thậm chí nếu nó tốt hơn thật thì cũng chưa chắc nó đã làm công việc của bạn tốt hơn cách bạn đang làm.
Rob Pike - 'Concurrency Is Not Parallelism' - Golang co-Creator Philip Roberts: What the heck is the event loop anyway? https://en.wikipedia.org https://techmaster.vn/posts/33604/su-khac-nhau-giua-process-va-thread http://www.dtp.fmph.uniba.sk/javastuff/javacourse/week11/01.html https://www.simple-talk.com/dotnet/asp-net/javascript-single-threaded/