Process, Thread and Routine

Process and Thread

Process

Tiến trình có thể hiểu đơn giản là một chương trình đang chạy trong máy tính. Khi chúng ta mở một trang web trình duyệt thì đây được xem là một tiến trình. Khi chúng ta viết 1 chương trình máy tính bằng ngôn ngữ lập trình như C, Java, hay Go, sau khi tiến hành biên dịch và chạy chương trình thì hệ điều hành sẽ cấp cho chương trình một không gian bộ nhớ nhất định, PID (process ID),… Mỗi tiến trình có ít nhất một luồng chính (main thread) để chạy chương trình, nó như là xương sống của chương trình vậy. Khi luồng chính này ngừng hoạt động tương ứng với việc chương trình bị tắt.

Thread

Thread hay được gọi là tiểu trình nó là một luồng trong tiến trình đang chạy. Các luồng được chạy song song trong mỗi tiến trình và có thể truy cập đến vùng nhớ được cung cấp bởi tiến trình, các tài nguyên của hệ điều hành,…

inner-process

  • Các thread trong process sẽ được cấp phát riêng một vùng nhớ stack để lưu các biến riêng của thread đó. Stack được cấp phát cố định khoảng 1MB-2MB.
  • Các thread chia sẻ chung vùng nhớ heap của process. ( các process không chung memory resource - được đảm bảo bởi OS )

Hệ quả

  • Khi process tạo quá nhiều thread sẽ dẫn đến tình trạng stack overflow.
  • Stack overflow khi thread thực thi hàm gọi hàm đệ quy sâu.
  • Thread stack size cố định cũng gây lãng phí nếu thread đó chỉ chạy task đơn giản.
  • Khi các thread sử dụng chung vùng nhớ sẽ dễ gây ra hiện tượng race condition.
  • Số lượng thread chạy song song trong cùng một thời điểm sẽ bằng với số lượng nhân CPU mà máy tính chúng ta có. Theo kinh nghiệm lập trình chúng ta chỉ nên tạo số thread bằng số nhân CPU * 2. ( bản chất số thread được sinh ra trong process không bị giới hạn nếu không tính đến vấn đề stack overflow như hệ quả 1 nói trên, tuy nhiên số lượng thread tối đa chạy trong cùng 1 thời điểm bằng số cpu của hệ điều hành - xem lại image 2 ).

Thread in action

Tại một thời điểm chỉ có một tác vụ được xử lý hay một thread được chạy trên một nhân CPU. Khi nhân CPU chuyển qua xử lý tác vụ khác cũng có nghĩa là thread khác được chạy. Thao tác đó được gọi là context switch.

Context switch xảy ra dựa trên cơ chế cpu cycle time: sau mỗi khoảng thời gian cố định, hardware timer interrupt processor, trigger kernel scheduler. Hàm scheduler của kernel sẽ dừng thực thi các thread ( os thread ) hiện tại, lưu lại các giá trị trên register của các thread đó vào thread local storage ( trên memory ), kiểm tra lại list các thread đã được đăng kí với os, chọn ra các thread sẽ được thực thi tiếp theo, restore các giá trị cần dùng để thực thi các thread đó từ memory và tiến hành thực thi. // chi phí cho context switch là tốn kém!

chi tiết hơn có thể xem tại: đây.

Conclusion about thread and process

  • OS thread != program thread, tuy nhiên trong execution context, chúng có tương quan 1:1
  • OS thread chỉ số task processor có thể thực hiện đk trong cùng 1 thời điểm ( = CPU number )
  • số lượng thread tạo được trong 1 program chỉ bị giới hạn bởi bộ nhớ stack của process chạy program đó ( phụ thuộc vào stack được cấp phát bởi OS )

Routine ( in Golang :) )

So sánh với thread trong các ngôn ngữ khác ( eg. java )

  • Một Goroutines sẽ được bắt đầu bằng một vùng nhớ nhỏ (8KB).
  • Khi gọi đệ quy sâu (không gian stack hiện tại là không đủ) Goroutines sẽ tự động tăng không gian stack (kích thước tối đa của stack có thể được đạt tới 1GB).
  • model M goroutine : N thread. Biến runtime.GOMAXPROCS quy định số lượng thread ( prog thread ) hiện thời đang được handle bởi go runtime ( số n trong model ).

go model in action

Để test model m routines : n threads, có thể thí nghiệm như sau

for {
    go print(0)
    print(1)
}

then

$ GOMAXPROCS=1 go run main.go
11111111111111100000000000000011...
$ GOMAXPROCS=2 go run main.go
01010101010101010101010101010101...

Ở trường hợp đầu ( runtime handle 1 thread pro ~ 1 os thread ), có 2 goroutine chạy trong prog tuy nhiên có duy nhất 1 thread pro ~ 1 os thread do đó tối đa 1 routine thực thi trong 1 thời điểm, switch value 0-1 xảy ra tại interrupt của hardware.
Ở trường hợp sau ( runtime handle 2 thread pro ~ 2 os thread ), prog có thể có tối đa 2 thread chạy trong cùng 1 thời điểm được khai báo với os. Cũng tương tự như trên, với 2 goroutine chạy trong prog, runtime có thể cho 2 routine thực thi đồng thời tại mọi thời điểm ( không nhận ra đk interrupt của hardware ).

but

$ GOMAXPROCS=1 go run main.go
1111111111111111111111111111111111111.....111111111110000000000000000000000...000....
$ GOMAXPROCS=2 go run main.go
111111111111111000000000000000111111111111111000000000000000111111111111111000...

( chú ý: số số 1 và 0 trong mỗi block phải bằng nhau )

Trong thực tế 1 cycle time của cpu hiện đại ( 1 lượt thread os execute ) dài hơn nhiều so với thời gian chạy của 1 statement trong goroutine ( hay cả thread prog ) do đó, có thể khẳng định sự khác biệt giữa os thread và prog thread.

Đọc thêm tại đây