Một tiến trình có ít nhất máy luồng

  • Process là gì?
  • Thread là gì?
  • So sánh Process với Thread

Thread và Process, đây là những thuật ngữ mà bạn sẽ được nghe rất nhiều nếu theo học ngành công nghệ thông tin, và cần phải nắm rõ khi đi làm việc. Tuy nhiên, định nghĩa và sự khác nhau giữa chúng ra sao thì không phải ai cũng biết, bởi có thể chúng không nằm trong những kiến thức được nhà trường giảng dạy. Để hiểu được Thread là gì, có gì khác so với Process, bạn hãy tham khảo bài viết dưới đây nhé.

Process là gì?

Một tiến trình có ít nhất máy luồng

Process, hay tiến trình, là sự thực thi của một chương trình và thực hiện các hành động liên quan được chỉ định trong một chương trình, hoặc nó là một đơn vị thực thi nơi chương trình chạy. Hệ điều hành tạo, lên lịch và chấm dứt các tiến trình. Các tiến trình khác được tạo bởi tiến trình chính được gọi là tiến trình con.

Hoạt động của bất kì tiến trình nào cũng được kiểm soát bởi khối điều khiển tiến trình (Process Control Block, viết tắt là PCB). PCB chứa tất cả những thông tin quan trọng liên quan đến các tiến trình, chẳng hạn như: id tiến trình, ưu tiên, trạng thái, CPU, …

Vòng đời của một tiến trình có một số trạng thái nhất định như ready (sẵn sàng), running (đang chạy), blocked (bị chặn) và terminated (đã chấm dứt). Các trạng thái của tiến trình được sử dụng để theo dõi hoạt động của tiến trình đó ở thời điểm hiện tại.

Những hoạt động xen kẽ của các tiến trình giúp nâng cao tốc độ tính toán khi hoạt động I / O trong một tiến trình trùng lặp với hoạt động tính toán trong tiến trình khác.

Các thuộc tính của một tiến trình:

- Việc tạo mỗi tiến trình bao gồm các cuộc gọi hệ thống cho từng tiến trình riêng biệt.

- Một tiến trình là một thực thể thực thi bị cô lập và không chia sẻ dữ liệu và thông tin.

- Các tiến trình sử dụng cơ chế IPC (Inter-process Communication, tức Giao tiếp giữa các tiến trình) để liên lạc làm tăng đáng kể số lượng cuộc gọi hệ thống.

- Quản lý tiến trình tiêu thụ nhiều cuộc gọi hệ thống hơn.

- Mỗi tiến trình đều có ngăn xếp và bộ nhớ heap, dữ liệu và bản đồ bộ nhớ riêng.

Thread là gì?

Thread, hay luồng, là một tiến trình nhỏ có thể được quản lý độc lập bởi một bộ lập lịch. Tất cả các luồng trong một chương trình đơn được chứa hợp lý trong một tiến trình. Nhân cấp phát một ngăn xếp và khối điều khiển luồng (TCB) cho mỗi luồng. Hệ điều hành chỉ lưu con trỏ ngăn xếp và trạng thái CPU tại thời điểm chuyển đổi giữa các luồng của cùng một tiến trình.

Luồng được triển khai theo ba cách khác nhau: luồng cấp nhân, luồng cấp người dùng, và luồng lai. Luồng có thể có ba trạng thái running (đang chạy), ready (sẵn sàng) và blocked (bị chặn); nó chỉ bao gồm trạng thái tính toán không phân bổ tài nguyên và trạng thái giao tiếp làm giảm chi phí chuyển đổi, khiến tăng cường sự tương tranh (song song) do đó tốc độ cũng tăng lên.

Đa luồng cũng đi kèm với nhiều vấn đề. Việc có nhiều luồng không tạo ra sự phức tạp, nhưng sự tương tác giữa chúng thì không như vậy.

Một luồng phải có thuộc tính ưu tiên khi có nhiều luồng đang hoạt động. Thời gian để nó thực thi tương ứng với các luồng đang hoạt động khác trong cùng tiến trình được chỉ định bởi mức độ ưu tiên của luồng.

Các thuộc tính của một luồng:

- Một cuộc gọi hệ thống có thể tạo nhiều hơn một luồng.

- Luồng chia sẻ dữ liệu và thông tin.

- Luồng chia sẻ bộ nhớ heap nhưng có ngăn xếp và thanh ghi riêng của mình.

- Quản lý luồng không tiêu thụ hoặc tiêu thụ ít cuộc gọi hệ thống hơn vì giao tiếp giữa các luồng có thể đạt được bằng cách sử dụng bộ nhớ dùng chung.

- Thuộc tính cô lập của tiến trình làm tăng chi phí của nó về mặt tiêu thụ tài nguyên.

Một tiến trình có ít nhất máy luồng

So sánh Process với Thread

Ta có bảng so sánh những sự khác nhau cơ bản giữa Process (tiến trình) và Thread (luồng) như sau:

Cơ sở so sánh

Process (tiến trình)

Thread (luồng)

Định nghĩa

Là chương trình đang thực thi

Là một tiến trình nhỏ

Chia sẻ bộ nhớ

Hoàn toàn cô lập và không chia sẻ bộ nhớ

Chia sẻ bộ nhớ với nhau

Chia sẻ dữ liệu và mã

Dữ liệu và đoạn mã độc lập

Chia sẻ phân đoạn dữ liệu, phân đoạn mã, tệp, … với các luồng ngang hàng

Tiêu thụ tài nguyên

Nhiều hơn

Ít hơn

Thời gian cần thiết để tạo

Nhiều hơn

Ít hơn

Thời gian cần thiết để chấm dứt

Nhiều hơn

Ít hơn

Thời gian chuyển đổi bối cảnh

Nhiều hơn

Ít hơn

Hiệu quả về giao tiếp

Thấp hơn

Cao hơn

Hoạt động khi bị chặn

Nếu một tiến trình bị chặn, các tiến trình còn lại vẫn có thể tiếp tục thực thi

Nếu một luồng cấp người dùng bị chặn, tất cả các luồng ngang hàng của nó cũng bị chặn

Chấm dứt không ổn định

Tiến trình bị mất

Luồng có thể được phục hồi

Tóm lại, ta có thể tổng kết những điểm khác biệt quan trọng nhất như sau:

- Tất cả các luồng của một chương trình được chứa một cách hợp lý trong một tiến trình.

- Luồng nhẹ hơn tiến trình.

- Một tiến trình là một đơn vị thực thi bị cô lập trong khi luồng không bị cô lập và có chia sẻ bộ nhớ.

- Một luồng không thể tồn tại riêng biệt; nó được gắn liền với một tiến trình. Mặt khác, một tiến trình có thể tồn tại riêng lẻ.

- Tại thời điểm hết hạn của một luồng, ngăn xếp liên kết của nó có thể được phục hồi vì mỗi luồng có ngăn xếp riêng. Ngược lại, nếu một tiến trình chết, tất cả các luồng cũng sẽ chết bao gồm cả tiến trình.

Qua bài viết này, bạn đã hiểu được Process và Thread là gì, cũng như sự khác nhau giữa chúng rồi phải không? Hi vọng những thông tin vừa chia sẻ ở trên sẽ giúp ích cho bạn!

Tiến trình - process là một khái niệm cơ bản trong bất kì một hệ điều hành nào. Một tiến trình có thể được định nghĩa là một thực thể chương trình đang được chạy trong hệ thống. Một web server chạy trong thiết bị là một tiến trình, hoặc một chương trình soạn thảo văn bản đang chạy trong thiết bị cũng là một tiến trình.

Một ví dụ khác: Cùng là một chương trình soạn thảo (ví dụ chương trình quen thuộc vim), người dùng mở 16 chương trình vim để thao tác với 16 file khác nhau, chúng ta cho 16 tiến trình chạy trong hệ thống.

Bài này sẽ giới thiệu về tiến trình (process), tiến trình nhẹ (lightweight process) và luồng (thread) trong Linux.

Tiến trình – Process

Như đã đề cập ở trên, tiến trình là một thực thể chương trình đang được thực thi. Nó là một tập hợp các cấu trúc dữ liệu mô tả đầy đủ quá trình một chương trình được chạy trong hệ thống.

Một tiến trình cũng trải qua các quá trình như con người: Nó được sinh ra, nó có thể có một cuộc đời ít ý nghĩa hoặc nhiều ý nghĩa, nó có thể sinh ra một hoặc nhiều tiến trình con, và thậm chí, nó có có thể chết đi. Điều khác biệt nhỏ duy nhất là: tiến trình không có giới tính. Mỗi tiến trình chỉ có một tiến trình cha (hoặc có thể gọi là mẹ, ở trong khóa học sẽ thống nhất gọi là cha J) duy nhất.

Dưới góc nhìn của kernel, tiến trình là một thực thể chiếm dụng tài nguyên của hệ thống (Thời gian sử dụng CPU, bộ nhớ, …)

Khi một tiến trình con được tạo ra, nó hầu như giống hệt như tiến trình cha. Tiến trình con sao chép toàn bộ không gian địa chỉ, thực thi cùng một mã nguồn như tiến trình cha, và bắt đầu chạy tiếp những mã nguồn riêng cho tiến trình con từ thời điểm gọi hàm tạo tiến trình mới.

Mặc dù tiến trình cha và tiến trình con cùng chia sẻ sử dụng phần mã nguồn của chương trình, nhưng chúng lại có phần dữ liệu tách biệt nhau (stack và heap). Điều này có nghĩa là, những sự thay đổi dữ liệu của tiến trình con không ảnh hưởng đến dữ liệu trong tiến trình cha.

Một ví dụ để làm rõ khái niệm này:​​​​

#include <unistd.h>   #include <sys/types.h>   #include <errno.h>   #include <stdio.h>   #include <sys/wait.h>   #include <stdlib.h>      int main()   {       pid_t child_pid;       int counter = 2;          printf("Gia tri khoi tao cua counter = %d\n", counter);          child_pid = fork();       if(child_pid >= 0)       {           if(0 == child_pid)           {               counter ++;               sleep(2);               printf("Day la tien trinh con, gia tri counter sau khi tang = %d\n", counter);               while(1);           }           else           {               counter ++;               sleep(5);               printf("Day la tien trinh cha, gia tri counter sau khi tang = %d\n", counter);               while(1);           }       }       else       {           printf("Tao process con khong thanh cong\n");       }              return 0;   }  

Kết quả chương trình khi chạy:

Một tiến trình có ít nhất máy luồng

Trong ví dụ trên, tiến trình con được tạo ra từ tiến trình cha. Bằng câu lệnh ps trong linux, ta sẽ thấy có 2 tiến trình tên là test được chạy trong hệ thống.

Tiến trình con được kế thừa biến counter từ tiến trình cha. Tuy nhiên sự thay đổi biến counter ở cả 2 tiến trình cha và con đều không ảnh hưởng tới nhau.

Luồng – User threads

Luồng – User thread (phân biệt với kernel thread) là một luồng thực thi mã nguồn trong một tiến trình.  Một tiến trình bao gồm nhiều luồng thực thi như thế được gọi là một tiến trình đa luồng - multithreaded process.

Các threads trong một tiến trình chỉ có ý nghĩa đối trong tiến trình đó (không thể nhìn thấy từ bên ngoài), và chúng chia sẻ dữ liệu sử dụng trong tiến trình. Chính vì thế, khác với tiến trình cha – con, sự thay đổi dữ liệu ở luồng này sẽ ảnh hưởng tới những luồng còn lại.

Một ví dụ về luồng: Một ứng dụng chơi cờ đơn giản khi chạy cũng là một tiến trình đa luồng. Khi ứng dụng chạy, ứng dụng sẽ phải vừa chờ đợi các nước đi của người chơi để thể hiện trên hình ảnh (đây sẽ là một luồng), vừa phải tính toán các nước đi tiếp theo của máy để chiến thắng được người chơi (đây là luồng thứ 2). Hai luồng này chia sẻ dữ liệu chung là nước đi của người chơi, nhưng vẫn hoạt động song song và thực hiện 2 công việc khác nhau.

Tuy nhiên, dưới góc nhìn của kernel, một tiến trình đa luồng cũng giống hệt một tiến trình bình thường khác. Ví dụ chúng ta đang chạy 3 tiến trình, 2 tiến trình 1 luồng, 1 tiến trình 3 luồng. Khi kernel lập lịch chạy cho 3 tiến trình đó, tiến trình có 3 luồng vẫn chỉ có thể chiếm 1/3 thời gian hoạt động của CPU, giống như 2 tiến trình còn lại. Điều này có nghĩa là, việc tạo ra, quản lý và lập lịch cho các luồng trong một tiến trình sẽ được thực hiện hoàn toàn ở user space.

Ngày nay, hầu hết các tiến trình đa luồng được viết bởi thư viện chuẩn pthread (POSIX thread).

Một ví dụ về chương trình đa luồng:

#include <stdio.h>   #include <pthread.h>   #include <semaphore.h>   #include <stdlib.h>      pthread_mutex_t lock= PTHREAD_MUTEX_INITIALIZER;   int counter = 2;      void* thread1()   {       sleep(1);       pthread_mutex_lock(&lock);       counter ++;       printf("Day la thread 1: gia tri counter = %d\n", counter);       pthread_mutex_unlock(&lock);       while(1);   }      void* thread2()   {       sleep(3);       pthread_mutex_lock(&lock);       counter ++;       printf("Day la thread 2: gia tri counter = %d\n", counter);       pthread_mutex_unlock(&lock);       while(1);   }      int main()   {           pthread_t th1, th2;              pthread_create(&th1,NULL,thread1,NULL);           pthread_create(&th2,NULL,thread2,NULL);              pthread_join(th1,NULL);           pthread_join(th2,NULL);           return 0;   }  

Trong ví dụ này, tiến trình test tạo ra 2 luồng.

Hai luồng cùng sử dụng chung biến counter. Khi có sự thay đổi giá trị của biến counter tại một luồng, thì luồng còn lại cũng sẽ “nhận biết” được sự thay đổi đó.

Kết quả khi chạy chương trình:

Một tiến trình có ít nhất máy luồng

Tiến trình nhẹ - Lightweight process

Như đề cập ở phần trên, tiến trình đa luồng được kernel nhìn giống hệt như các tiến trình đơn luồng khác, nên kernel sẽ cung cấp các tài nguyên của hệ thống (thời gian sử dụng CPU) giống nhau giữa các tiến trình đa luồng và tiến trình đơn luồng. Điều nay khiến cho các tiến trình đa luồng trở nên không thực sự tốt.

Trở lại ví dụ ứng dụng chơi cờ đề cập ở phần trên. Trong khi luồng thứ nhất (chờ đợi các nước đi của người chơi để thể hiện trên hình ảnh) được thực hiện, thì luồng thứ hai (tính toán các nước đi tiếp theo của máy) nên được chạy một cách liên tục, tận dụng khoảng thời gian suy nghĩ của người chơi để đưa ra các nước đi tốt nhất.

Tuy nhiên, giả sử luồng thứ nhất thực thi một blocking system call nào đó, thì luồng thứ hai cũng sẽ bị block lại theo. Lý do bởi vì kernel coi một tiến trình đa luồng giống như một tiến trình đơn luồng bình thường khác, nên khi luồng thứ nhất thực hiện một blocking system call, kernel sẽ coi đó là hành động của cả tiến trình và block cả tiến trình đó lại. Do đó, tiến trình đa luồng trở nên không tối ưu cho ứng dụng.

Linux sử dụng tiến trình nhẹ - lightweight process để cung cấp phương thức thực hiện tối ưu hơn cho một ứng dụng đa luồng. Về cơ bản, hai tiến trình nhẹ có thể chia sẻ và sử dụng cùng một tài nguyên của hệ thông như không gian địa chỉ, files … Khi một tiến trình nhẹ thay đổi tài nguyên dùng chung, tiến trình nhẹ còn lại có thể nhìn thấy sự thay đổi đó.

Tiến trình nhẹ là một tiến trình, nhưng thay vì có không gian địa chỉ, tài nguyên riêng biệt như khái niệm tiến trình bình thường, chúng có thể chia sẻ sử dụng chung tài nguyên với nhau như khái niệm của luồng trên user space.

Điểm khác biệt giữa tiến trình nhẹ và luồng là việc kernel nhìn tiến trình nhẹ như một tiến trình bình thường, nên kernel sẽ cung cấp tài nguyên hệ thống, lập lịch làm việc cho tiến trình nhẹ. Nên nếu hệ thống đang chạy hai tiến trình, một tiền trình đơn luồng, một tiến trình đa luồng gồm hai tiến trình nhẹ, thì tiến trình đa luồng đó sẽ chiếm 2/3 thời gian sử dụng CPU.

Một cách cơ bản để tạo ra một ứng dụng đa luồng là gán mỗi một luồng với một tiến trình nhẹ. Có nhiều thư viện tạo luồng sử dụng phương thức này ví dụ như: LinuxThreads, Native POSIX Thread Library (NPTL) …

Quay trở lại lần nữa với ví dụ về ứng dụng chơi cờ: Nếu gán hai luồng của ứng dụng vào hai tiến trình nhẹ, thì dùng luồng một (chờ đợi các nước đi của người chơi để thể hiện trên hình ảnh) có thực thi một blocking system call, thì luồng thứ hai (tính toán các nước đi tiếp theo của máy) sẽ vẫn được thực hiện liên tục do kernel sẽ lập lịch làm việc cho luồng thứ hai.

Hiển thị tiến trình nhẹ: Chạy lại chương trình ví dụ với luồng bên trên và dùng câu lệnh ps với tùy chọn “-fL -C”. Cột LWP hiển thị số tiến trình nhẹ mà tiến trình test đang có. Tiến trình test sẽ có 3 tiến trình nhẹ, một là tiến trình chính, và hai tiến trình nhẹ con tương ứng với hai luồng con được tạo ra (thread1 và thread2)

Một tiến trình có ít nhất máy luồng

Mối quan hệ giữa tiến trình - luồng - tiến trình nhẹ:

Một tiến trình có ít nhất máy luồng

Hình vẽ bên trên mô tả mối quan hệ giữa tiến trình – luồng – tiến trình nhẹ. Một tiến trình chạy trong hệ thống có thể gồm nhiều luồng (user threads), một luồng này được liên kết trực tiếp 1-1 với một tiến trình nhẹ, hoặc nhiều luồng có thể liên kết nhiều – 1 với một tiến trình nhẹ. Việc thực thi, lập lịch cho các tiến trình nhẹ được thực hiện bởi kernel.

Sau bài này, các bạn đã có những khái niệm về tiến trình, luồng và tiến trình nhẹ trong Linux. Bài tiếp theo sẽ tìm hiểu sâu hơn những trạng thái của tiến trình, và làm cách nào Kernel quản lý các tiến trình trong hệ thống.