Có bao nhiêu cách thực thi luồng

Trong Khoa học Máy tính, luồng [thread] là đơn vị thực thi nhỏ nhất được quản lý một cách độc lập bởi bộ lập lịch của hệ điều hành. Bộ lập lịch này cho phép nhiều thread có thể chạy song song gọi là Đa luồng [Multithreading]. Đây là một kỹ thuật quan trọng và được hầu hết ngôn ngữ lập trình hỗ trợ. Một lập trình viên sơ cấp cũng có thể dễ dàng khởi tạo hoặc sử dụng thread để xử lý dữ liệu một cách song song. Nhưng không phải ai cũng có thể trả lời câu hỏi tại sao multithreading chạy nhanh hơn single-threading? Bao nhiêu thread là đủ?

Cơ chế hoạt động của CPU cache

Cấu trúc của bộ vi xử lý [CPU] hiện đại với nhiều lõi [core] và vùng nhớ đệm[1]

Tốc độ của bộ nhớ rất chậm so với bộ vi xử lý, vì vậy để tăng tốc độ thực người ta thiết kế các bộ nhớ nhỏ hơn, nhanh hơn, gọi là vùng nhớ đệm [cache], gồm có 3 level: L1-L2-L3 ở gần CPU. Tốc độ của cache L1 có thể nhanh hơn 30 lần so với bộ nhớ chính. Vì dung lượng của các cache này không cao, nên chỉ những dữ liệu cần thiết nhất cho CPU mới được sao chép vào cache: các lệnh ở mức mã máy, dữ liệu nằm trong các biến và có cả các dữ liệu được chia sẻ giữa nhiều thread. Vì các cache L1-L2 của mỗi core là độc lập, nên dẫn tới 2 vấn đề lớn đối với multithreading: context switch của CPU và làm tươi [refresh] các dữ liệu dùng chung.

Context switch của CPU

Context switch[3] [đôi khi được gọi là process switch hoặc task switch] là quá trình lưu trữ trạng thái của CPU hoặc của một thread để có thể tiếp tục thực thi sau đó. Việc lưu trữ này cho phép nhiều tiến trình có thể cùng thực thi trên một CPU vật lý và là chức năng quan trọng của các hệ điều hành đa nhiệm. Context switch là một quá trình phức tạp, đòi hỏi nhiều bước như lưu trữ các thanh ghi [registers], lưu trữ trạng thái các cache… tùy thuộc vào từng loại CPU và hệ điều hành khác nhau. Ví dụ, đối với nhân Linux, context switch liên quan tới các thanh ghi [registers], con trỏ ngăn xếp [stack pointer] và con trỏ chương trình. Nếu context switch xảy ra giữa 2 thread thuộc 2 tiến trình [process] khác nhau sẽ phức tạp và tốn thời gian hơn.

Tiến trình của context switch [nguồn ghi trong hình]

Ngoài chi phí cho việc lưu trữ/phục hồi trạng thái của các thread, hệ điều hành cũng phải tốn chi phí cho bộ lập lịch [task scheduler] để chọn lựa thread tiếp theo được đưa vào xử lý.

Làm tươi các dữ liệu dùng chung

Các dữ liệu sau khi được sao chép sang các cache L1-L2 riêng biệt của từng core, khi một core làm thay đổi dữ liệu dùng chung thì các core khác không thể tự động cập nhật các thay đổi đó. Điều này dẫn đến yêu cầu phải có cơ chế chia sẻ hoặc thông báo giữa các core. Mỗi ngôn ngữ và hệ điều hành có cách thức xử lý khác nhau, nhưng tựu chung lại có thể phân loại thành mấy phương pháp:

Lock object: sử dụng một biến trung gian để cấp quyền thực thi cho từng thread, mỗi thread muốn được thực thi cần phải “chiếm quyền” [acquire] một cờ được định nghĩa trước, sau khi hoàn thành xử lý đối với dữ liệu dùng chung sẽ giải phóng [release] cờ để các thread khác có thể “chiếm quyền”. Như vậy tại mỗi thời điểm chỉ có một thread được quyền thay đổi dữ liệu dùng chung và các thread khác dễ dàng cập nhập dữ liệu mới nhất. Phương pháp này rất hiệu quả nhưng có 2 nhược điểm lớn. Đầu tiên là tất cả các thread muốn thay đổi [hoặc đơn giản chỉ muốn đọc] dữ liệu dùng chung sẽ bị treo lại [pending] cho đến khi chiếm được quyền thực thi, khiến cho các core phải thực hiện context switch để chọn các thread đã sẵn sàng thực thi khác. Thứ hai là dẫn tới tình trạng các thread chờ nhau thành một vòng tròn [deadlock] hoặc rất khó kiểm soát trình tự thực thi của các thread [race condition], dẫn tới các lỗi tiềm ẩn.

Các ngôn ngữ lập trình sử dụng lock object để tạo ra các cấu trúc dữ liệu chuyên biệt phức tạp hơn để quản lý các thread như Semaphore, Lock của Java hoặc Monitor, Mutex của .NET[4]. Các cấu trúc dữ liệu này cho phép lập trình viên dễ dàng thao tác với thread và đạt được hiệu năng cao hơn so với sử dụng các từ khóa có sẵn của ngôn ngữ lập trình [synchronized của Java và lock của .NET].

Memory barrier: sử dụng các lệnh đặc biệt của CPU để sắp xếp trình tự thực hiện các thao tác đọc - ghi giữa các CPU. Phương pháp này khá phức tạp và tùy thuộc mỗi loại CPU lại có cách thực hiện khác nhau. Ví dụ đơn giản nhất có lẽ là từ khóa volatile của Java, mỗi câu lệnh ghi dữ liệu vào biến được khai báo với từ khóa này sẽ luôn được thực hiện trước mọi yêu cầu đọc dữ liệu từ biến đó[6]

Ví dụ về memory barrier[5]

Chọn lựa giữa đơn luồng và đa luồng

Trên lý thuyết ta thấy đa luồng phức tạp và chạy chậm hơn đơn luồng, trên thực tế hướng tiếp cận đa luồng có thể tăng hiệu năng của hệ thống, nhưng điều này không phải lúc nào cũng đúng. Đối với các nghiệp vụ hay hệ thống cần tính toán số lượng lớn dữ liệu đã được nạp sẵn vào RAM thì đơn luồng luôn cho kết quả khả quan hơn như trên lý thuyết đã dự báo. Đối với các hệ thống cần dữ liệu thông qua các kênh "IO" [ổ cứng, card mạng, thiết bị ngoại vi] thì đa luồng sẽ dễ dàng cho hiệu năng vượt trội so với đơn luồng.

Điều này được giải thích bởi vì tốc độ của các thiết bị ngoại vi rất chậm so với RAM [và RAM rất chậm so với CPU], vì vậy khi CPU phải chờ dữ liệu được thiết bị ngoại vi thu thập đầy đủ [như nhận đủ các gói tin của một thông điệp TCP] sẽ rất lãng phí tài nguyên. Lúc này hệ điều hành nên nạp các thread khác đã có đầy đủ dữ liệu và sẵn sàng thực thi vào core để thực hiện. Các thư viện non-blocking IO áp dụng nguyên lý này rất hiệu quả, chỉ cần 1 thread để thu thập dữ liệu, sau đó chuyển cho nhiều thread khác xử lý. Có một ví dụ kinh điển khác là Node.JS, mặc dù chỉ sử dụng 1 thread [JavaScript là đơn luồng] nhưng có thể xử lý số lượng lớn request HTTP.

Quay trở lại với câu hỏi từ ban đầu: bao nhiêu thread là đủ? Không có một quy tắc cụ thể để xác định số thread mà hệ thống cần, dù sao thì cũng có vài quy tắc cơ bản:

  • Nếu hệ thống thiên về xử lý số liệu thì single-thread thường tối ưu hơn.
  • Nếu hệ thống có giao tiếp với thiết bị ngoại vi thì nên sử dụng các thư viện Non-blocking IO hoặc sử dụng multi-thread.
  • Cách dễ nhất để xác định là thay đổi số lượng thread được sử dụng và tiến hành kiểm tra hiệu năng hệ thống [stress test hoặc benchmark] với nhiều kịch bản khác nhau, qua vài lần thay đổi ta có thể xác định số lượng thread tối ưu.

---------------------------------------------

Nguồn tham khảo:

Thời gian đăng: 25/1/2019 17:22:55

Tất tần tận về Thread - luồng trong hệ điều hành

Bạn có thể đã bắt gặp chữ luồng/ thread khi nhìn vào thông số trên CPU, hay nghe bạn bè người thân nói về khái niệm này. Trong bài viết sau, Stream Hub sẽ giải thích cho bạn cặn kẽ và đầy đủ về thông số cơ bản này.
Table of Contents
  • Luồng CPU là gì
  • Sự khác nhau giữa single-threaded và multithreaded
  • Các mô hình trong multithreading
    • Mô hình many-to-one
    • Mô hình one-to-one
    • Mô hình many-to-many
  • Hyperthreading là gì

Luồng CPU là gìThread là một đơn vị cơ bản trong CPU. Một luồng sẽ chia sẻ với các luồng khác trong cùng process về thông tin data, các dữ liệu của mình. Việc tạo ra thread giúp cho các chương trình có thể chạy được nhiều công việc cùng một lúc.
Có hai khái niệm ta cần xem qua đó là single-threaded và multithreaded.

    • Phần lớn các phần mềm trong máy tính hiện đại đều có dạng multithreaded, tức đa luồng. Các ứng dụng trong máy tính đa phần đều chạy một process nhất định cùng với đó là nhiều luồng chạy bên trong. Bạn có thể hình dung thế này: trong một trang web, một thread sẽ đảm nhiệm việc chạy hình ảnh và bài viết, và một thread khác cùng lúc sẽ có nhiệm vụ nhận thêm các dữ liệu vào web.
    • Các ứng dụng cũng có thể được thiết kế để tận dụng khả năng xử lý trên các hệ thống multicore, giúp thực hiện nhiều CPU task song song.
    • Trong nhiều trường hợp nhất định, một ứng dụng có thể được yêu cầu thực hiện [request] nhiều nhiệm vụ giống nhau. Ví dụ: một web server nhận lệnh từ khách hàng nhấn vào trang web, hình ảnh, âm thanh… và tất nhiên, một web server có thể phải nhận rất nhiều [hàng nghìn, hàng triệu cho đến hàng trăm triệu] yêu cầu cùng một lúc. Vì thế, nếu web server đó chạy theo dạng single-threaded, tức là chỉ một khách hàng được giải quyết yêu cầu trong 1 khoảng thời gian, thì những khách hàng khác sẽ phải đợi rất lâu để mình có thể access vào trang web. Trước khi có multithreaded, một cách giải quyết cho vấn đề này đó là web server sẽ chạy một process nhận nhiều request cùng một lúc, và với một request được tiếp nhận, nó sẽ tạo ra một process khác để giải quyết request đó. Điều này sẽ tốn rất nhiều thời gian và nguồn lực. Multithreads giúp giải quyết vấn đề này. Thay vì tạo ra một process mới y chang process đang có, chúng ta chỉ cần một process duy nhất có nhiều luồng cùng chạy với nhau. Khi server nhận được một yêu cầu từ khách hàng, nó sẽ tạo ra một luồng mới để luồng đó giải quyết yêu cầu nhận được, trong khi đó, server sẽ quay lại với những yêu cầu tiếp theo.
    • Bên cạnh đó, thread cũng rất quan trọng đối với hệ thống RPC [Remote Procedure Call – hệ thống cho phép quá trình truyền tin giữa các tiến trình IPC – interprocess communication được diễn ra]. Và hiển nhiên, RPC servers cũng là một dạng multithreaded. Khi một server PRC nhận được một tin nhắn, nó sẽ tạo ra một thread để giải quyết tin nhắn đó. Multithreaded giúp RPC có thể giải quyết nhiều yêu cầu cùng một lúc.
    • Và cuối cùng, multithreaded cũng được sử dụng rộng rãi trong nhân hệ hiều hành [operating system kernels]. Có nhiều luồng hoạt động trong một kernel, và mỗi luồng đảm nhiệm một công việc riêng biệt, như quản lý thiết bị, quản lý bộ nhớ, quản lý ngắt… Một vài ví dụ có thể đưa ra như là: Solaris có một set những thread chuyên quản lý bộ xử lý ngắt; hay Linux có một kernel thread chuyên quản lý những vùng bộ nhớ trống trong hệ thống.

  • Lợi ích của Multithreaded: có 4 lợi ích chính:
    • Khả năng đáp ứng: Multithread giúp các ứng dụng tương tác có thể hoạt động tốt hơn vì ngay cả khi một phần chương trình bị block hoặc cần một thời gian dài để hoạt động, chương trình nhìn chung vẫn có thể chạy. Và điều này giúp người dùng hài lòng hơn vì khả năng đáp ứng cao của ứng dụng. Điều này đặc biệt đúng với người dùng là các designer. Bạn có thể hình dung: khi một chương trình đang chạy, và người dùng nhấn vào một nút lệnh mà cần rất nhiều thời gian để process, thì một hệ thống dạng single-threaded sẽ không kích hoạt bất kì hoạt động nào khác cho tới khi hoàn thành bước lệnh vừa rồi. Ngược lại, ứng dụng dạng multithread sẽ không làm gián đoạn quá nhiều quá trình của người dùng vì trong khi một thread được kích hoạt để thực hiện bước lệnh kia, một thread khác sẽ được kích hoạt để thực hiện bất kì bước lệnh ít tốn thời gian hơn mà người dùng yêu cầu.
    • Khả năng chia sẻ tài nguyên: các tiến trình chỉ có thể chia sẻ dữ liệu thông qua các kĩ thuật như shared memory [vùng bộ nhớ chung] và message sharing [chia sẻ tin]. Các kĩ thuật này chỉ có thể được thiết lập bởi lập trình viên. Tuy nhiên, các luồng chia sẻ thông tin hoặc tài nguyên theo hệ thống được mặc định. Lợi ích của việc chia sẻ code và dữ liệu là nó giúp ứng dụng có nhiều threads hoạt động trong một vùng địa chỉ chung.
    • Tiết kiệm: việc cung cấp tài nguyên và dữ liệu cho quá trình tạo process rất tốn kém. Và vì threads tự động chia sẻ data cho process mà nó thuộc về, việc tạo các thread cho việc context-switch sẽ giúp tiết kiệm chi phí rất nhiều. Không chỉ chi phí mà còn là thời gian, vì việc tạo một process mới sẽ lâu hơn nhiều so với tạo một thread mới. Như trong Solaris, tạo ra một process lâu hơn 30 lần so với tạo ra một thread trong process đó, và lâu hơn 5 lần so với tạo một context-switch.
    • Scalability: Lợi ích của multithreaded thể hiện rõ hơn trong kiến trúc đa xử lý [multiprocessor architecture], vì multithread giúp các threads hoạt động song song trong các lõi xử lý khác nhau, trong khi đối với tiến trình dạng single-threaded, một thread chỉ có thể chạy trên một bộ xử lý, không quan trọng việc có bao nhiêu thread trong hệ thống hiện tại.
Các mô hình trong multithreadingSau phần bài tổng quan về luồng - threads, bài viết này sẽ nói về các mô hình đa luồng [multithreading models]. Trước hết, threads có thể được hỗ trợ qua hai cách sau: thông qua người dùng, để thành lập user threads, và thông qua nhân máy tính, để hình thành kernel threads. User threads được hỗ trợ trên kernel và được quản lý không cần sự hỗ trợ từ kernel, trong khi đó kernel threads được hỗ trợ và quản lý trực tiếp từ hệ điều hành. Các hệ điều hành hiện nay như Window, Linux, Mac OS X, Solaris đều hỗ trợ kernel threads. Dù là hai đơn vị riêng biệt, user threads và kernel threads có mối liên hệ không thể tách rời. Ba mô hình sau cũng là ba cách để hình thành mối quan hệ giữa user threads và kernel threads: mô hình Many-to-one, mô hình one-to-one, và mô hình one-to-many. Mỗi hoặc nhiều user thread phải được map sang một hoặc nhiều kernel thread tương ứng để được xử lý bởi hệ điều hành.


Mô hình many-to-oneMô hình many-to-one là mô hình nhiều user threads nối vào một kernel thread. Việc quản lý các luồng này dựa vào thư viện luồng trong không gian người dùng. Tuy nhiên, cả hệ thống sẽ bị chặn nếu một luồng nào đó thực hiện một blocking system call. Và cũng vì chỉ một luồng được tiếp cận kernel trong một lần, các threads khác không thể chạy song song trong hệ thống đa lõi. Green threads – tên gọi của một thư viện luồng của hệ thống Solaris và đã được sử dụng trong những versions cũ của Java – sử dụng mô hình many-to-one. Không có quá nhiều hệ thống sử dụng mô hình này vì nó không sử dụng được lợi thế của multiple processing cores.
Mô hình one-to-oneMô hình one-to-one là mô hình 1-1, một user thread kết nối với một kernel thread. Mô hình 1-1 này đảm bảo được tính liên tục vì nếu một thread bị block thì một thread khác sẽ vẫn kết nối được với kernel. Nó cũng đảm bảo được nhiều luồng có thể hoạt động cùng một lúc trong bộ đa xử lý. Khuyết điểm duy nhất của mô hình này là nó đòi hỏi khi một user thread hoạt động thì một kernel thread phải được kích hoạt theo. Và vì quá nhiều kernel threads sẽ gây nên sự quá tải trong ứng dụng, những app sử dụng model này đều giới hạn số lượng thread được tạo ra trong hệ thống. Linux và Windows là những hệ điều hành sử dụng model one-to-one.

Mô hình many-to-manyMô hình many-to-many chia các user-level threads cho một lượng nhỏ hơn hoặc bằng các kernel threads. Lượng kernel threads này tùy thuộc vào yêu cầu của ứng dụng sử dụng hoặc bộ máy sử dụng [một ứng dung thường dùng nhiều kernel threads trên multiprocessors hơn là trên single processor]. Mô hình này khác mô hình many-to-one ở tính liên lục. Trong mô hình many-to-one, người dùng có thể tạo bao nhiêu user threads tùy thích, nhưng nó không đảm bảo được tính liên tục vì một kernel chỉ kết nối được với một user thread, như đã nói ở trên. Mô hình one-to-one cho phép tính liên tục cao hơn, tuy nhiên số lượng threads được tạo ra rất quan trọng nếu bạn không muốn ứng dụng của mình bị quá tải.
Mô hình many-to-many sẽ giải quyết được vấn đề của hai mô hình trước: số lượng user threads tạo ra là tùy thích, và kernel thread tương ứng có thể chạy song song trong hệ đa xử lý. Và, khi một thread đang thực hiện blocking system call, kernel threads tương ứng có thể chuyển sang một user thread khác và giải quyết user thread đó. Một biến thể của many-to-many model là two-level model. Model này như là sự kết hợp giữa many-to-many model và one-to-one model, vì nó vừa chia các user-level threads cho một lượng nhỏ hơn hoặc bằng các kernel threads tương ứng, và vừa cho phép một user thread kết nối riêng với một kernel thread. Trước version Solaris 9, hệ điều hành Solaris đã sử dụng two-level model. Tuy nhiên, từ version 9 trở đi, Solaris sử dụng one-to-one model.
Hyperthreading là gìHyperthreading là khi CPU có khả năng cho một core đơn thực thi nhiều hơn một luồng cùng một lúc. Hyperthreading có khả năng tăng mức độ xử lí/ tạo nhiều luồng hơn cho nhiều core, nhưng không phải tất cả core.. Tùy thuộc vào một nhiệm vụ đang làm, hyperthreading có thể giúp mang lại hiệu năng khác nhau trong từng core khác nhau, nhưng đôi khi tổng thể có thể hụt hiệu năng. Với công nghệ hiện nay, một core có thể sản sinh ra 2 threads, đó là lý do có nhiều CPU đời mới, ví dụ: ryzen 5 1600 với số core, thread lần lượt là 6, 12. Tuy vậy, cũng có những con CPU đời mới, ví dụ: Intel Core i5 7400 [Kaby Lake], lại chỉ có số core, thread lần lượt là 4, 4. Do đó, tùy thuộc vào công nghệ sử dụng của từng hãng, cho từng con CPU mà số core, số thread nó lại khác nhau.
Kết luận, Hyperthreading là khi CPU có khả năng cho một core đơn thực thi nhiều hơn một luồng cùng một lúc. MỜI XEM THÊM TẠI:

//stream-hub.com/thread-la-gi


Chia sẻ
Yêu thích0
Tốt0
Không tốt0

Video liên quan

Chủ Đề