Chi tiết bài học Quản lý Posix thread

Quản lý Posix thread

Bài học trước đã trình làng về Posix thread ( pthread ), cách tạo và kết thúc một pthread. Trong bài này, tất cả chúng ta sẽ tìm hiểu và khám phá thêm 1 số ít kỹ thuật quản trị tài nguyên và những API tương ứng quan trọng khác của pthread .

Quản lý tài nguyên pthread

Thread được tạo ra với mục tiêu chạy đồng thời nhiều tác vụ trong tiến trình, qua đó tăng hiệu năng của ứng dụng. Trong bài trước, tất cả chúng ta đã biết mỗi thread có khoảng trống bộ nhớ thread stack riêng. Khi một thread kết thúc với hàm pthread_exit ( ), kernel sẽ không tự động hóa tịch thu tài nguyên của thread đó, lúc này thread đã kết thúc sẽ có trạng thái giống như zombie process mà tất cả chúng ta đã học ở trong những bài về tiến trình ( hoàn toàn có thể goi là zombie thread ). Việc này gây ra tiêu tốn lãng phí tài nguyên bộ nhớ vì tài nguyên này không hề tái sử dụng cho những thread khác. Bên cạnh đó nếu số lượng zombie thread quá lớn, mạng lưới hệ thống không hề tạo thêm thread được nữa .

Hãy cùng xem xét ví dụ dưới đây: hàm main() có một while loop tạo ra và kết thúc 1 thread liên tục cho đến khi xảy ra lỗi sẽ in ra màn hình:

#include 
#include 
#include 

void *thread_start(void *args)
{
	pthread_exit(NULL);
}

int main (void)
{
	pthread_t threadID;
	int ret;
	long count = 0;

	while(1)
	{
		if(ret = pthread_create(&threadID, NULL, thread_start, NULL))
		{
			printf("pthread_create() fail ret: %d\n", ret);
			perror("Fail reason:");
			break;
		}
		count++;
	}
	printf("Number of threads are created:%ld\n", count);
	return 0;
}

Bây giờ compile chương trình trên và để chương trình chạy một lúc, ta sẽ thấy tiến trình bị kết thúc với lỗi sau :

Tiến trình kết thúc và in ra dòng lỗi không hề cấp phép thêm memory sau khi đã tạo ra và kết thúc 32751 thread .
Như vậy, một thread được tạo ra cũng giống như một tiến trình con, cần phải được theo dõi trạng thái và tịch thu tài nguyên khi kết thúc, tránh việc tạo ra zombie thread gây tiêu tốn lãng phí tài nguyên của tiến trình. Sau đây tất cả chúng ta sẽ tìm hiểu và khám phá 2 kỹ thuật quản trị việc tịch thu tài nguyên thread là joinable thread và detached thread .

Joinable Thread

Xét về góc nhìn quản trị và tịch thu tài nguyên, một thread được tạo ra hoàn toàn có thể được chia thành 2 loại : thread được kernel theo dõi trạng thái kết thúc và tịch thu tài nguyên gọi là joinable thread, và thread tự động hóa bị tịch thu tài nguyên sau khi kết thúc mà không chăm sóc đến trạng thái kết thúc gọi là detached thread .
Trong Linux, một pthread khi được tạo ra sẽ mặc định là joinable thread. Do đó, thread sau khi kết thúc sẽ được “ join ” để lấy được trạng thái kết thúc và tịch thu tài nguyên của nó .
Trong Posix thread, tiến sau khi tạo thread sẽ gọi hàm pthread_join ( ) có prototype như sau :

include 

int pthread_join(pthread_t thread, void **retval);
             /*Trả về 0 nếu thành công, hoặc một số dương error number nếu có lỗi*/

Hàm pthread_join() sẽ block chương trình và chờ cho thread với ID là “thread” kết thúc, và giá trị return của thread đó được lưu vào biến con trỏ “retval”.  Nếu thread đã kết thúc trước khi gọi pthread_join(), thì hàm sẽ return ngay lập tức. Việc gọi hàm pthread_join() 2 lần với cùng một thread ID có thể dẫn đến lỗi “unpredictable behavior”.

Việc gọi hàm pthread_join ( ) sau khi tạo ra thread mới bằng hàm pthread_create ( ) cũng tương tự như như việc gọi system call wait ( ) sau khi tạo ra tiến trình con bằng fork ( ). Chỉ khác là pthread_join ( ) hoàn toàn có thể được gọi từ bất kể thread nào trong tiến trình, còn wait ( ) phải được gọi bởi tiến trình cha đã tạo ra nó. Ví dụ, thread A tạo ra thread B và thread B tạo ra thread C thì thread A hoàn toàn có thể gọi pthread_jon ( ) với thread C và ngược lại. Ngoài ra, pthread_join ( ) phải join vào 1 thread đơn cử, không có khái niệm join với toàn bộ những thread giống như system call wait ( ) .

Bây giờ, chúng ta sẽ dùng pthread_join() để fix ví dụ trên: mỗi thread được tạo ra sẽ được “join” để giải phóng tài nguyên sau khi kết thúc. Ở chương trình trên, lỗi xảy ra sau khi tạo ra và kết thúc liên tục 32751 thread, chúng ta sẽ kiểm chứng bằng cách in ra số thread được tạo sau mỗi 10000 thread.

#include 
#include 
#include 

void *thread_start(void *args)
{
	pthread_exit(NULL);
}

int main (void)
{
	pthread_t threadID;
	int ret;
	long count = 0;
	void *retval;

	while(1)
	{
		if(ret = pthread_create(&threadID, NULL, thread_start, NULL))
		{
			printf("pthread_create() fail ret: %d\n", ret);
			perror("Fail reason:");
			break;
		}
		pthread_join(threadID, &retval);
		count++;
		if (0 == count %10000)
		{
			printf("Number of threads are created:%ld\n", count);
		}
	}
	printf("Number of threads are created:%ld\n", count);
	return 0;
}

Bây giờ compile và chạy chương trình, ta thấy lỗi không xảy ra dù bao nhiêu thread được tạo ra và kết thúc đi chăng nữa :

Detached thread

Một thread được thiết lập là detached thread khi lập trình viên không cần chăm sóc đến trạng thái kết thúc của nó. Khác với joinable thread, kernel sẽ tự động hóa tịch thu tài nguyên của detached thread khi nó kết thúc .
Như đã nói ở trên, một thread mới được tạo ra sẽ mặc định là joinable. Để chuyển thread thành detached, hoàn toàn có thể dùng hàm pthread_detach ( ) với prototype như sau :

#include 

int pthread_detach(pthread_t thread);
         /*Trả về 0 nếu thành công, hoặc 1 số dương error number nếu có lỗi*/

Đối số được được truyền vào pthread_detach ( ) là ID của thread muốn thiết lập .
Một thread hoàn toàn có thể tự thiết lập nó thành detached thread bằng cách sử dụng hàm pthread_self ( ) để truyền thread ID của nó vào hàm pthread_detach ( ) :

pthread_detach(pthread_self());

Khi là detached thread, bạn không cần gọi hàm pthread_join ( ) để chờ thread kết thúc. Tuy nhiên, tất cả chúng ta sẽ không hề truy vấn trạng thái kết thúc của thread đó, cũng không hề chuyển thread đó thành joinable thread nữa. Lưu ý rằng detached thread chỉ xác lập cách hành xử của mạng lưới hệ thống khi thread đó kết thúc, nó không hề tránh được việc kết thúc thread khi tiến trình chứa nó kết thúc .
Để hiểu rõ hơn về cách sử dụng pthread_detach ( ), tất cả chúng ta sẽ lại fix lỗi trong ví dụ ở đầu bài viết bằng hàm pthread_detach ( pthread_self ( ) ) như đã lý giải ở trên thay vì dùng pthread_join ( ). Lưu ý trong chương trình, hàm usleep ( 10 ) dùng để cho tiến trình ngủ 10 micro second, mục tiêu là để kernel có thời hạn quét dọn buffer và tịch thu tài nguyên của thread đã kết thúc .

#include 
#include 
#include 
#include 

void *thread_start(void *args)
{
	if(pthread_detach(pthread_self()))
	{
		printf("pthread_detach error\n");
	}
	pthread_exit(NULL);
}

int main (void)
{
	pthread_t threadID;
	int ret;
	long count = 0;

	while(1)
	{
		if(ret = pthread_create(&threadID, NULL, thread_start, NULL))
		{
			printf("pthread_create() fail ret: %d\n", ret);
			perror("Fail reason:");
			break;
		}
		usleep(10);
		count++;
		if (0 == count %10000)
		{
			printf("Number of threads are created:%ld\n", count);
		}
	}
	printf("Number of threads are created:%ld\n", count);
	return 0;
}

Bây giờ, lại compile và chạy chương trình, bạn cũng sẽ thấy chương trình không còn bị lỗi dù bao nhiêu thread được tạo ra và kết thúc :

Kết luận

Một thread hoàn toàn có thể ở một trong hai trạng thái là joinable ( mặc định trong mạng lưới hệ thống ) hoặc detached. Người lập trình phải sử dụng những Posix API tương ứng để giải phóng tài nguyên của thread khi nó kết thúc, tránh để thread thành zombie gây tiêu tốn lãng phí tài nguyên của tiến trình. Thông thường, chiêu thức pthread_join ( ) sẽ được sử dụng phổ cập hơn trong lập trình Linux. Bài học tiếp theo sẽ ra mắt về chính sách đồng điệu trong thread, để những thread hoàn toàn có thể đồng thời thực thi trong tiến trình .

Tài liệu tìm hiểu thêm

The Linux Programming Interface – Michael Kerrisk – Chapter 29
Linux System Programming – Robert Love – Chapter 7