Quản lý Posix thread

Bài học trước đã giới thiệu về Posix thread (pthread), cách tạo và kết thúc một pthread. Trong bài này, chúng ta sẽ tìm hiểu thêm một số kỹ thuật quản lý tài nguyên và các 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 đích 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, chúng ta đã biết mỗi thread có không gian 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 thu hồi 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à chúng ta đã học ở trong các bài về tiến trình (có thể goi là zombie thread). Việc này gây ra lãng phí tài nguyên bộ nhớ vì tài nguyên này không thể tái sử dụng cho các thread khác. Bên cạnh đó nếu số lượng zombie thread quá lớn, hệ thống không thể 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 <stdio.h>
#include <pthread.h>
#include <errno.h>

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 thể cấp phát 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à thu hồi tài nguyên khi kết thúc, tránh việc tạo ra zombie thread gây lãng phí tài nguyên của tiến trình. Sau đây chúng ta sẽ tìm hiểu 2 kỹ thuật quản lý việc thu hồi tài nguyên thread là joinable thread và detached thread.

Joinable Thread

Xét về khía cạnh quản lý và thu hồi tài nguyên, một thread được tạo ra có thể được chia thành 2 loại: thread được kernel theo dõi trạng thái kết thúc và thu hồi tài nguyên gọi là joinable thread, và thread tự động bị thu hồi tài nguyên sau khi kết thúc mà không quan tâm đế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à thu hồi 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 <pthread.h>

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ư 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() 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 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 cụ thể, không có khái niệm join với tất cả các 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 <stdio.h>
#include <pthread.h>
#include <errno.h>

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 quan tâm đến trạng thái kết thúc của nó. Khác với joinable thread, kernel sẽ tự động thu hồi 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, có thể dùng hàm pthread_detach() với prototype như sau:

#include <pthread.h>

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 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, chúng ta sẽ không thể truy vấn trạng thái kết thúc của thread đó, cũng không thể chuyển thread đó thành joinable thread nữa. Lưu ý rằng detached thread chỉ xác định cách hành xử của hệ thống khi thread đó kết thúc, nó không thể 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(), 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ư đã giải thích ở 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 đích là để kernel có thời gian dọn dẹp buffer và thu hồi tài nguyên của thread đã kết thúc.

#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>

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 có thể ở một trong hai trạng thái là joinable (mặc định trong hệ thống) hoặc detached. Người lập trình phải sử dụng các 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 lãng phí tài nguyên của tiến trình. Thông thường, phương pháp pthread_join() sẽ được sử dụng phổ biến hơn trong lập trình Linux. Bài học tiếp theo sẽ giới thiệu về cơ chế đồng bộ trong thread, để các thread có thể đồng thời thực thi trong tiến trình.

Tài liệu tham khảo

The Linux Programming Interface - Michael Kerrisk - Chapter 29

Linux System Programming - Robert Love - Chapter 7

Luyện tập cho bài học này

Khóa học liên quan

** Nếu bạn muốn viết các nội dung đặt biệt thì hãy làm theo hướng dẫn sau

Xem thêm 10 bình luận
Viết blog mới của bạn
Báo lỗi trang
Đang tải