Lập Trình Đa Tiến Trình
NỘI DUNG BÀI VIẾT
1. Giới thiệu:
Xin chào các bạn. Hôm nay mình sẽ giới thiệu vs các bạn một kỹ thuật, khái niệm hơi cao cấp trong lập trình, mang tên là “lập trình đa tiến trình”, tiếng anh gọi là “multi process programming”
Nếu bạn đã từng nghe và sử dụng kĩ thuật này, không sao cả, bài này sẽ giúp bạn hiểu rõ hơn và biết thêm 1 số thứ. Còn nếu chưa từng nghe và tìm hiểu, đây sẽ là một kĩ thuật khá mới và bổ ích, được dùng khá nhiều trong các bài toán, hệ thống lớn.
2. Khái niệm process và thread
Trước hết chúng ta cần hiểu hai khái niệm “process” và “thread“
Đại khái, process (tiến trình) là thể hiện của mỗi chương trình/ứng dụng độc lập đang được chạy trên hệ thống/hệ điều hành (không chạy thì ko gọi là tiến trình nhé). Mỗi tiến trình sẽ có không gian, vùng nhớ, tập hợp các câu lệnh (chỉ dẫn) để chạy tiến trình đó. Mỗi một tiến trình cũng sẽ có 1 cái định danh gọi là process id, hệ điều hành sẽ dùng process id để quản lý tiến trình. Ứng dụng, hệ thống hay một service nào đó mà bạn đang dùng có thể là kết hợp của nhiều process (hoặc chỉ là 1 process)
Trên windows, để hiển thị danh sách các process, bạn vào tính năng task manager. Trên Linux, để hiển thị danh sách các process đang chạy, bạn dùng câu lệnh ps
như hình dưới. Cụ thể về lệnh này và các thông tin liên quan, bạn có thể tìm hiểu kĩ hơn câu lệnh trên google

Mỗi một tiến trình mà bạn chạy, có thể có nhiều luồng (thread). Các luồng hoạt động song song trong cùng một tiến trình để thực hiện các nhiệm vụ nào đó của tiến trình
Hiểu nôm na thì, máy tính bạn chạy như hiện tại có nhiều tiến trình, mỗi tiến trình có nhiều luồng cùng chạy, hay có thể nói luồng (thread) là một phần (hay là con) của một tiến trình (process)
3. Sự khác nhau giữa process và thread
Tuy nhiên, để ngắn gọn và dễ nhớ, mình viết một vài ý chính thể hiện sự khác nhau của 2 cái này như sau:
Process | Thread |
Một bản thể độc lập | Là một phần, là con của process |
Sử dụng vùng nhớ độc lập | Sử dụng vùng nhớ chung với process cha, có thể chia sẻ vùng nhớ vs các thread có chung process |
Khi process chết (có thể bị buộc dừng bởi tác nhân khác), toàn bộ các thread con đều dừng hoat động | Khi một thread chết, các thread khác trong cùng process vẫn hoạt động |
Giao tiếp giữa các process rất khó (ví dụ muốn chia sẻ một tham số), phải dùng kĩ thuật giao tiếp đa tiến trình (inter-process communication) | Giao tiếp giữa các thread đơn giản hơn, do có cơ chế chia sẻ bộ nhớ |
Chi phí để tạo và hủy một process là rất đắt (Thời gian, cpu và bộ nhớ) | Chi phí để tạo và hủy một thread thì khá rẻ |
Khó cài đặt | Dễ cài đặt |
Với bảng so sánh trên, hãy chú ý vào phần bôi đậm. Đó vừa là ưu điểm, cũng là nhược điểm lớn nhất của 2 khái niệm này. Tùy vào mục đích của bài toán, tính năng, bạn lựa chọn lập trình đa tiến trình hay đa luồng, không có cái gì là chắc chắn có lợi thế.
4. Bài toán lập trình đa tiến trình, ưu và nhược:
Khi lập trình và phát triển các sản phẩm, nhiều lúc chương trình của bạn cần “thực hiện nhiều nhiệm vụ cùng một lúc”. Đó có thể là:
- Xử lý nhiều nhiệm vụ giống hệt nhau tại cùng thời điểm
- Xử lý các nhiệm vụ khác nhau một cách đồng thời (ví dụ một nhiệm vụ luôn đọc dữ liệu, một nhiệm vụ luôn ghi dữ liệu)
Lúc này bạn sẽ cần chọn giải pháp “lập trình đa luồng” hay “lập trình đa tiến trình”. Mình ví dụ với một bài toán cụ thể mà mình đã từng làm cách đây 3 năm như sau:
“Viết ứng dụng chỉnh sửa ảnh của người dùng, theo đó với mỗi người dùng gửi lên một ảnh bạn sẽ xử lí và gửi trả lại người dùng một ảnh đã xử lý”
Với bài toán như trên, bạn tưởng tượng rằng bạn cần có nhiều luồng/tiến trình, trong đó mỗi luồng/tiến trình (gọi là 1 công nhân) sẽ đảm nhận 1 trong các công việc như sau:
- Loại nhiệm vụ 1: N công nhân để lắng nghe/ghi nhận các ảnh gửi lên của người dùng (mỗi 1 người dùng gửi lên ta cần 1 công nhân lắng nghe và ghi nhận)
- Loại nhiệm vụ 2: M công nhân để xử lý ảnh của người dùng tại 1 thời điểm
- Loại nhiệm vụ 3: K công nhân để gửi trả ảnh đã chỉnh sửa về cho người dùng
Như vậy có thể thấy vs một bài toán như thế này, bạn đã phải sử dụng rất nhiều luồng/tiến trình để giải quyết. Việc sử dụng nhiều luồng/tiến trình chưa chắc giúp bạn tăng tốc độ xử lý (tham khảo bài này), nhưng chắc chắn dễ tổ chức và dễ cài đặt hơn (chuyên môn hóa từng nhiệm vụ).
Với bài toán trên của mình, lúc đầu mình xử lý tất cả bằng lập trình đa luồng (do chi phí cpu và ram tạo đa luồng tốt hơn đa tiến trình). Và rồi 1 ngày mình phát hiện ra, vs loại nhiệm vụ số 2 (xử lý ảnh), thì tỉ lệ crash chương trình lên tới 20%. Và lúc này yếu điểm của lập trình đa luồng đc bộc lộ, đó là chỉ cần 1 luồng bị crash thì cả tiến trình (process) sẽ bị ngừng hoạt động theo. Trong khi đó, nếu lập trình đa tiến trình, một tiến trình con bị chết thì tiến trình chính vẫn hoạt động bình thường, bạn có thể cài đặt xử lý các ngoại lệ/báo lỗi ở trong tiến trình chính (tổng thể hoạt động bình thường)
Nếu bạn lập trình C++ nhiều, bạn sẽ thấy tỉ lệ đoạn mã bị crash là khá cao (các vđ liên quan tới con trỏ, bộ nhớ). Và nếu chương trình của bạn lập trình đa luồng và 1 luồng nào đó sử dụng đoạn mã có tỉ lệ crash cao như thế, hậu quả là cả chương trình sẽ thường xuyên bị crash. Ý tưởng cải tiến ở đây là, chúng ta tạo ra 1 process chính có nhiệm vụ quản lý các process con, các process con mà bị chết thì process chính vẫn ko sao, có thể tạo lại process con chạy lại từ đầu.
Hình dưới minh họa điểm mạnh trong việc kiểm soát crash giữa ứng dụng đa tiến trình và một tiến trình đa luồng:

Có 1 sự thật là, khoảng 60% các sản phẩm mình code trên hệ điều hành Linux bằng ngôn ngữ C++ đc code theo kiến trúc đa tiến trình. Lí do thì khá đơn giản, đó là do mình hay sử dụng các mã nguồn mở có ẩn chứa tỉ lệ crash khá cao. Lập trình đa tiến trình theo mình tác dụng lớn nhất là như thế (kiểm soát tốt hơn sự crash)
À còn có 1 điểm quan trọng nữa đó là, nếu lập trình đa luồng thì bạn cần code tất cả bằng một ngôn ngữ (ví dụ cùng là C++ hay C#), nhưng nếu lập trình đa tiến trình, bạn có thể tùy từng nhiệm vụ của tiến trình mà sử dụng ngôn ngữ khác nhau. Ví dụ vs bài toán của mình, nhiệm vụ 1 và 3 các bạn sử dụng C# (hay Python hoặc Java), còn nhiệm vụ 2 bạn sử dụng C++, rất tiện lợi đúng ko?
Tuy nhiên, nhược điểm của lập trình đa tiến trình là thời gian khởi tạo một tiến trình rất lâu, và hơi tốn CPU lẫn bộ nhớ. Để truyền tham số, giao tiếp liên lạc giữa các tiến trình vs nhau cũng là 1 bài toán khó khăn khác mà người lập trình phải giải quyết. Do đó, mình gọi kĩ năng lập trình đa tiến trình là kĩ năng hơi cao cấp
Chốt lại, nếu chương trình bạn viết ổn định (ko crash), đc phát triển bởi cùng một ngôn ngữ, thì bạn có thể sử dụng đa luồng để đạt hiệu năng cao và dễ cài đặt. Ngược lại, nếu chương trình hay crash và đc phát triển bởi nhiều ngôn ngữ lập trình, bạn nên nghĩ tới hướng lập trình đa tiến trình
Lập trình đa tiến trình cũng không phải chỉ đc áp dụng cho các ứng dụng desktop đâu nhé. Khá nhiều ứng dụng điện thoại cũng đc phát triển theo hình thức này, khi vỏ bọc/giao diện đc code bằng Java còn core function đc code bằng C++ và chạy ở 1 tiến trình web tách biệt. Với chính web app CodeLearn.io mà các bạn đang dùng, cũng có 1 phần nhỏ đoạn mã phía server đc code theo thiết kế đa tiến trình nha
5. Lập trình đa tiến trình như thế nào?
Lập trình đa tiến trình là về cơ bản, bạn sẽ có 1 tiến trình chính (main process), sau đó tiến trình này sẽ đẻ ra các tiến trình con để làm việc. Nhiệm vụ của tiến trình chính khá là đơn giản, ko thể có lỗi, thường là nhiệm vụ quản lý và giao việc cho các tiến trình con hơn là trực tiếp xử lý một công việc gì đó
Ngắn gọn lại:
- Tạo ra tiến trình chính, tiến trình chính gọi các tiến trình con
- Tiến trình con nhận nhiệm vụ (thường là nhận nhiệm vụ thông qua giao tiếp vs tiến trình chính), xử lý công việc liên tục
- Tiến trình chính quản lý tiến trình con, nếu thằng con chết/treo/hoàn thành nhiệm vụ thì xử lý (chết thì tạo mới, treo thì stop, ….)
Để lập trình đc, bạn cần biết các kĩ thuật chính như:
- Tiến trình chính tạo ra tiến trình con như thế nào?
- Tiến trình con nhận tham số, công việc từ tiến trình chính ra làm sao?
- Làm sao để tiến trình chính biết chương trình con hoạt động ntn (còn sống hay đã chết, có bị treo hay không, ….)
Chú ý rằng mỗi tiến trình sẽ là một chương trình/không gian chạy độc lập. Do đó, nếu lập trình đa tiến trình, bạn sẽ có nhiều hàm main. Một hàm main thể hiện tiến trình chính, một vài hàm main thể hiện tiến trình phụ. Tất cả các đoạn mã cần đc build thành công và có thể chạy độc lập vs nhau
6. Tạo và chạy tiến trình con
Để chạy một tiến trình con khá là đơn giản, với C# bạn sử dụng lớp System.diagnostics.process
Chung chung vs các ngôn ngữ, để chạy một process bạn cần:
- Đường dẫn của chương trình mà bạn cần chạy (ví dụ
C:/A.exe
) - Các tham số truyền vào chương trình đó
- Các tùy chọn khác (tùy từng ngôn ngữ)
Với C++, để chạy một tiến trình đơn giản nhất có thể, bạn có thể dùng câu lệnh System vs cú pháp kiểu "System("ping 192.168.1.1 -t")"
Riêng vs ngôn ngữ C++ trên hđh Linux, C++ cung cấp các 1 hàm để tạo ra process con một cách khá đặc biệt mà bạn ko thể tìm thấy ở các ngôn ngữ khác là hàm fork
. Trong giới hạn của bài viết này do không có đủ thời gian để giải thích chi tiết hơn, nên mình gửi các bạn link tham khảo ở dưới đây (hẹn bài khác nha)
http://www.yolinux.com/TUTORIALS/ForkExecProcesses.html
Có 1 chú ý rất lớn, đó là nếu bạn chạy một tiến trình thông thường, bạn sẽ ko nắm được trạng thái kết thúc hoặc số phận của tiến trình mà bạn vừa chạy. Trong phần lớn lập trình đa tiến trình, bạn cần nắm đc trạng thái của tiến trình con sau khi đc chạy để đưa ra các xử lý phù hợp. Hãy đọc tiếp các phần phía dưới nha
7. Truyền tham số cho tiến trình con
Một bài toán khá quan trọng trong lập trình đa tiến trình, đó là truyền tham số cho chương trình con
Lấy ví dụ bài toán xử lý ảnh trong ví dụ của mình, đó là giả sử mỗi tiến trình con cần xử lý một cái ảnh mà người dùng gửi lên. Khi đó, tiến trình chính cần gửi tham số là cái ảnh đó cho các chương trình con xử lý, và chương trình con cần đưa lại kết quả đã xử lý cho chương trình cha
Cách truyền tham số cho chương trình con đơn giản nhất, chính là cách truyền tham số theo dạng dòng lệnh:
- Tiến trình con xử lý tham số nhận vào từ dòng lệnh (thông qua tham số argc, argv ở hàm main)
- Tiến trình cha truyền tham số cho chương trình con khi khởi chạy
Đến phần này, tự nhiên bạn đã hiểu 2 tham số argc và argv trong hàm main của mỗi chương trình mà bạn viết để làm gì chưa?
Đối vs phần lớn các ngôn ngữ lập trình, hàm main nào cũng đều có 2 tham số argc
và argv
. Argc là một số nguyên thể hiện số tham số truyền vào trong tiến trình của bạn, argv
là một mảng gồm argc
các xâu kí tự thể hiện các tham số truyền vào
Ví dụ như nếu bạn gõ "ping 192.168.1.1 -t"
thì ping chính là tên tiến trình của bạn, argc = 2
, argv[] = {"192.168.1.1", "-t"}
Để tìm hiểu thêm, hãy thử tìm kiếm theo từ khóa "command line arguments"
kèm theo ngôn ngữ mà bạn mong muốn nhé. Hẹn 1 hôm nào rảnh, mình sẽ viết chi tiết 1 bài về kĩ thuật này sau
Rồi, bây giờ giả sử tiến trình con của bạn đã xử lý được các tham số từ dòng lệnh, thì truyền tham số khi chạy từ tiến trình cha ra làm sao? Với đa phần các ngôn ngữ, bạn chỉ cần cài tham số khi khởi chạy cho process là đc. Tham khảo dòng lệnh C# như sau:
ProcessStartInfo startInfo = new ProcessStartInfo("IExplore.exe");
startInfo.WindowStyle = ProcessWindowStyle.Minimized;
Process.Start(startInfo);
// set command line arguments for process
startInfo.Arguments = "www.northwindtraders.com";
Code language: JavaScript (javascript)
8. Giao tiếp giữa 2 tiến trình – IPC?
Với bài toán đơn giản, tiến trình cha gọi tiến trình con rồi tiến trình con cứ thế chạy là xong. Nhưng có nhưng bài toán rất phức tạp, bạn cần tiến trình con giao tiếp liên tục với tiến trình cha, thì làm thế nào???
Để giao tiếp giữa các tiến trình cha và con, hay giữa các tiến trình độc lập vs nhau, người ta dùng các thuật ngữ/kĩ thuật gọi là inter-process-communication-ipc
Lấy ví dụ tiến trình con cần xử lý 1 công việc trong thời gian rất dài và liên tục phải báo cho tiến trình cha về số % công việc đã làm đc, thì bạn sẽ làm ntn? Nếu là lập trình đa luồng, khi các luồng có thể cùng truy cập vào chung vùng nhớ, bạn sẽ giải quyết nó bằng cách lưu % hoàn thành vào 1 biến là xong. Nhưng giữa các process thì ko có chuyện đó, kĩ thuật trao đổi dữ liệu trở nên khó khăn hơn rất nhiều
Ở phạm vi bài viết này, mình sẽ chỉ nói sơ sơ thôi. Cái này cũng đành hẹn có 1 bài viết chuyên sâu hơn về IPC cùng các ưu điểm của các phương pháp. Ở đây nói ngắn gọn, sẽ có mấy kĩ thuật chính:
- Dùng stdin/stdout: bạn ghi output của process A ra stdout của A, process B đọc stdout của A rồi xử lý. Hoặc bạn write dữ liệu vào stdin của process A từ process B
- Dùng file: ghi và nhận dữ liệu qua một file trung gian
- Dùng socket/pipe: một cách thức truyền nhận dữ liệu thông qua các ống và ổ cắm, như kiểu lắp đường ống nước để luân chuyển nước giữa các tòa nhà ấy
- Dùng database
Chi tiết về các kĩ thuật này, bạn có thể tìm kiếm vs từ khóa tương ứng hoặc đọc thêm bài mà mình đã gửi link ở trên nha
9. Quản lý tiến trình được tạo ra
Một bài toán quan trọng trong lập trình đa tiến trình, chính là quản lý tiến trình đc tạo ra như:
- Tiến trình đã hoàn thành bình thường, làm sao để biết
- Tiến trình chạy quá lâu, cần tiêu diệt/stop việc hoạt động
- Tiến trình gặp lỗi, làm sao biết lỗi?
Để nhận biết tiến trình đã hoàn thành, có 1 số cách như sau:
- Lưu giữ id của process con, sau đó dùng lệnh kiểm tra process đó còn chạy hay không?
- trước khi kết thúc process con, hãy báo hiệu cho tiến trình cha rằng process con đã kết thúc (thông qua các phương pháp như mục 8)
- Đối vs 1 số ngôn ngữ (không phải là tất cả), có 1 cách để đăng ký lời gọi khi process con đã kết thúc. Cái này bạn tra google xem
Với phần lớn các ngôn ngữ thì phương án số 1 hay đc áp dụng và hỗ trợ. Hãy xem thử thông qua ví dụ bằng ngôn ngữ C# như sau:
System.Diagnostics.Process process = System.Diagnostics.Process.Start("cmd.exe");
while (!process.HasExited)
{
//update UI
}
Code language: JavaScript (javascript)
Trường hợp process chạy quá lâu, bạn có thể stop process đó. Bạn có thể giới hạn thời gian chạy của process thông qua api process.WaitForExit
hoặc đo thời gian chạy của chương trình xem nó có kết thúc trong khoảng thời gian cho phép hay không?
Trường hợp bạn muốn dừng chương trình một cách đột ngột, hãy dùng api process.kill()
Với C++ hay C# (Java không rõ có ko) cũng có câu lệnh rất mạnh (cần chạy bằng quyền admin/root) để tiêu diệt/stop một process nào đó đang chạy, đó là lệnh kill.
Để sử dụng lệnh này, bạn cần biết id của process cần tiêu diệt, sau đó truyền tham số cho lệnh đó là xong. Chú ý nếu bạn kill 1 process hệ thống, máy tính của bạn có thể khởi động lại ngay lập tức nhé
Trong lập trình đa luồng, bạn có thể sử dụng try catch để bắt đc các lỗi/ngoại lệ còn lập trình đa tiến trình thì không. Hãy cố gắng để tiến trình không có lỗi, nếu có lỗi bạn có thể bắt đc lỗi trong chính tiến trình bị lỗi, ghi các lỗi này ra file/database hoặc stdout trước khi thoát để xử lý sau
10. Tổng kết:
Một bài viết khá dài nhưng cũng chưa hẳn đầy đủ về lập trình đa tiến trình. Mình sẽ dành thêm thời gian để viết những bài và các kĩ thuật liên quan
Mong rằng bài viết giúp bạn hiểu biết thêm và vận dụng 1 số kiến thức vào trong các sản phẩm của các bạn
Hãy chia sẻ và để lại câu hỏi nếu bạn cần mình giải đáp thêm nhé. Chúc mọi người luôn học tập tốt
Nguồn: codelearn.io
Leave a Reply