Thread Synchronization-Mutex

Trong bài trước, chúng ta đã thấy được tính chất và các ưu điểm của một thread, trong đó ưu điểm cốt lõi là các thread trong tiến trình có thể dễ dàng chia sẻ thông tin thông qua biến toàn cục. Tuy nhiên, việc này cũng dẫn đến vấn đề là các thread có thể cùng thay đổi giá trị của  biến toàn cục đó cùng 1 thời điểm; hoặc 1 thread có thể truy vấn giá trị của 1 biến toàn cục trong khi thread khác đang thay đổi nó. Tình trạng này được gọi là race condition.

Critical section

Thuật ngữ critical section (vùng trọng yếu) dùng để chỉ đoạn code mà truy cập vào tài nguyên toàn cục (global resource); đoạn code này nên được chạy hết từ đầu đến cuối bởi mỗi thread thay vì bị ngắt khi có một thread khác xen vào đọc hoặc thay đổi các biến toàn cục trong đó.

Cùng xét ví dụ dưới đây để hiểu rõ hơn về critical section, main thread tạo ra 2 thread với hàm bắt đầu là threadFunc(). Hàm threadFunc() sẽ tăng biến toàn cục “counter” lên 1 đơn vị qua dòng lệnh “counter +=1;”

#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>

#define MAX_THREAD      2

pthread_t tid[MAX_THREAD];
/*Counter la bien toan cuc duoc 2 thread su dung*/
int counter;

void *threadFunc(void *argv)
{
	counter += 1;
	printf("Thread %d has started\n", counter);

	sleep(1);
	printf("Thread %d has finished\n", counter);

	pthread_exit(NULL);
}

int main(void)
{
	int i = 0;
	int ret = 0;

	for (i = 0; i < MAX_THREAD; i++)
	{
		ret = pthread_create(&(tid[i]), NULL, threadFunc, NULL);
		if (ret != 0)
		{
			printf("Thread [%d] created error\n", i);
		}
	}

	pthread_join(tid[0], NULL);
	pthread_join(tid[1], NULL);

	return 0;
}

Compile chương trình trên với -pthread option, chạy chương trình sẽ ra được output sau:

Nhìn vào code chương trình ta thấy, mong muốn output của chương trình là in ra dòng “Thread 1 has finished” và “Thread 2 has finished”. Tuy nhiên, khi thread 1 chạy vào hàm threadFunc() tăng biến counter thành 1 và đang ngủ 1 giây thì thread 2 được lập lịch và hàm threadFunc() cũng tăng biến toàn cục counter lên thành 2 trước khi cũng đi vào ngủ 1 giây. Kết quả là thread 1 sau khi thức dậy sẽ in ra dòng "Thread 2 has finished" như trên. Ta thấy rằng các dòng code trong hàm threadFunc() thực hiện thao tác ghi và đọc giá trị của biến toàn cục counter, như vậy đoạn code trong hàm threadFunc() được gọi là critical section.

Như vậy, critical section nên được bảo vệ để khi các thread đồng thời chạy vào, nó phải chờ cho đến khi thread đang chạy thực thi xong và ra khỏi thì mới được chạy vào. Việc này gọi là đồng bộ thread (thread synchronization), một công việc bắt buộc khi lập trình đa luồng. Sau đây chúng ta sẽ đi vào tìm hiểu cách hoạt động và sử dụng 2 kỹ thuật đồng bộ trong hệ điều hành Linux là mutex và biến điều kiện.

Mutex

Kỹ thuật mutual exclusion (gọi tắt là mutex) dùng để bảo vệ tài nguyên chia sẻ (shared variable) mà chủ yếu là biến chia sẻ (shared variable) giữa các thread. Có thể hình dung cách hoạt động của mutex giống như là một cái khóa: Mỗi vùng critical section sẽ được bảo vệ trong phòng và có một cái khóa ở cửa, một thread trước khi chạy vào critical section sẽ khóa lại rồi vào làm những gì mình muốn làm; sau khi xong việc nó sẽ mở khóa để cho các thread khác truy cập. Các thread khác nếu thấy cửa đang khóa thì phải chờ (đi vào trạng thái blocked) cho đến khi thread trước mở khóa; sau đó nó tiếp tục khóa lại rồi chạy vào critical section và mở khóa ra sau khi xong việc. Cần lưu rằng chỉ có thread nào khóa mutex thì mới mở được mutex đó. Như vậy, việc thực hiện đồng bộ thread để bảo vệ tài nguyên chia sẻ gồm 3 bước rõ ràng như sau:

  • Khóa mutex trước khi vào critical section
  • Thực thi code trong critical section
  • Nhả khóa mutex sau khi kết thúc critical section

Để sử dụng mutex, trước hết chúng ta phải khai báo và khởi tạo mutex. Trong Posix thread, biến mutex là kiểu dữ liệu có dạng pthread_mutex_t và có thể được khởi tạo tĩnh sử dụng macro PTHREAD_MUTEX_INITIALIZER hoặc khởi tạo động lúc runtime.

Khởi tạo tĩnh (statically allocation)


Pthread_mutex_t mutex =  PTHREAD_MUTEX_INITIALIZER;

Khởi tạo động (dynamically initializing)

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
                             /*Trả về 0 nếu thành công, hoặc 1 số dương mã lỗi*/

Trong cách khởi tạo tĩnh, macro PTHREAD_MUTEX_INITIALIZER dùng để khởi tạo một mutex với các thuộc tính (thread attribute) mặc định. Trong khi hàm pthread_mutex_init() trong cách khởi tạo động cho phép khởi tạo và thiết lập thuộc tính cho mutex. Nếu không cần quan tâm đến thuộc tính của thread, ta có thể truyền NULL vào đối số pthread_mutexattr_t *attr. Khi khởi tạo động mutex bằng hàm pthread_mutex_init(), ta cần phải hủy mutex đó nếu không cần sử dụng nữa bằng hàm pthread_mutex_destroy() có prototype như sau (khởi tạo tĩnh bằng macro PTHREAD_MUTEX_INITIALIZER không cần destroy mutex):

int pthread_mutex_destroy(pthread_mutex_t *mutex);
      /*Return 0 nếu thành công, hoặc một số dương mã lỗi nếu không thành công*/

Lock/unlock mutex

Sau khi khởi tạo, mutex được khóa và mở khóa bởi 2 hàm sau đây:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/*Trả về 0 nếu thành công, hoặc 1 số dương mã lỗi khi xảy ra lỗi*/

Để khóa 1 mutex, ta truyền địa chỉ của mutex đó vào hàm pthread_mutex_lock(). Nếu một mutex đang ở trạng thái unlock, hàm này sẽ khóa mutex đó và return. Nếu mutex đó đã bị khóa bởi thread khác, hàm này sẽ bị block cho đến khi mutex được mở. Nếu một thread khóa một mutex mà chính nó đang giữ khóa thì sẽ xảy ra deadlock (thread rơi vào trạng thái chờ vô hạn).

Ngoài ra, chuẩn Posix còn cung cấp hai hàm lock mutex sau đây:

int pthread_mutex_trylock(pthread_mutex_t *mutex);
và
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abs_timeout);

Hàm pthread_mutex_trylock() hoạt động khác pthread_mutex_lock() ở chỗ: nếu mutex đang bị khóa, nó sẽ không block thread mà sẽ return ngay lập tức với mã lỗi là EBUSY. Còn hàm pthread_mutex_timedlock() được thêm vào đối số abs_timeout để thiết lập thời gian tối đa thread có thể chờ; nếu sau khoảng thời gian "abs_timeout " mà thread đó chưa sở hữu được mutex, nó sẽ return và trả về mã lỗi ETIMEDOUT.

Bây giờ chúng ta sẽ vận dụng cơ chế mutex để giải quyết lỗi của chương trình trên. Critical section trong hàm threadFunc() sẽ được đặt vào mutex để bảo vệ như sau:

#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>

#define MAX_THREAD      2

pthread_t tid[MAX_THREAD];
/*Counter la bien toan cuc duoc 2 thread su dung*/
int counter;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;   //khai bao mutex

void *threadFunc(void *argv)
{
	pthread_mutex_lock(&mutex);
	counter += 1;
	printf("Thread %d has started\n", counter);

	sleep(1);
	printf("Thread %d has finished\n", counter);
	pthread_mutex_unlock(&mutex);

	pthread_exit(NULL);
}

int main(void)
{
	int i = 0;
	int ret = 0;

	for (i = 0; i < MAX_THREAD; i++)
	{
		ret = pthread_create(&(tid[i]), NULL, threadFunc, NULL);
		if (ret != 0)
		{
			printf("Thread [%d] created error\n", i);
		}
	}

	pthread_join(tid[0], NULL);
	pthread_join(tid[1], NULL);

	return 0;
}

Bạn thử compile và chạy lại chương trình, ta sẽ được kết quả như mong muốn:

Mutex Deadlock

Lock và Unlock là một cơ chế rất đơn giản và hiệu quả để bảo vệ biến toàn cục được sử dụng không chỉ trong lập trình multi-thread mà còn rất nhiều tình huống khai thác chung tài nguyên chia sẻ như đa tiến trình, hoặc đọc/ghi vào cơ sở dữ liệu... Khi một thread gọi hàm yêu cầu khóa để vào critical section mà có một thread khác đang khóa, nó sẽ chờ cho đến khi thread kia mở khóa rồi chiếm quyền. Nhưng vì một tình huống nào đó mà thread kia không bao giờ mở khóa được thì sao? Trạng thái một thread khóa một mutex mà rơi vào trạng thái chờ không thể giải thoát được gọi là deadlock.

Trong lập trình multi-thread, deadlock xảy ra trong những trường hợp sau đây (giả sử có 2 thread A và B gây ra deadlock):

  • Trường hợp 1: Thread A khóa một mutex mà chính nó trước đó đã bị khóa bởi chính nó.

  • Trường hợp 2: Thread A khóa Mutex1 và vào critical section, sau đó cố gắng khóa một Mutex2 mà thread B đang giữ. Trong khi đó, thread B đang khóa Mutex2 và lại cố gắng khóa Mutex1 ở trong critical section.

Lỗi deadlock trong lập trình multi-thread rất khó debug, vì vậy bạn phải rất cẩn thận khi sử dụng khóa mutex. Trong hai trường hợp deadlock ở trên, trường hợp 1 có thể khó xảy ra vì ít ai lại làm việc ngớ ngẩn như vậy. Để tránh deadlock trong trường hợp 2, chúng ta có hai cách sau:

  • Đảm bảo sao cho tất cả các thread khóa các mutex theo một thứ tự xác định trước. Ví dụ trong trường hợp trên, Thread B cần phải khóa Mutex1 trước, sau đó mới khóa Mutex2, khi đó nếu thread A đang giữ Mutex1 thì thread B chỉ bị block cho đến khi thread A nhả khóa chứ không bị deadlock.
  • Sử dụng phương pháp "khóa thử, nếu không được thì quay lại sau" bằng cách dùng hàm pthread_mutex_trylock() để khóa mutex. Như đã giải thích ở trên, hàm pthread_mutex_trylock thay vì block thread nếu mutex đang bị khóa sẽ return ngay, qua đó tránh được việc block cũng như deadlock. Nếu pthread_mutex_trylock() return fail, thread release tất cả các mutex đang khóa và thử lại từ đầu. Cách này thường ít được sử dụng hơn cách trên vì yêu cầu vòng lặp khá tốn kém thời gian và phức tạp.

Kết luận

Mutex thread là một kỹ thuật không thể thiếu khi lập trình multi-thread giúp cho việc bảo vệ tài nguyên chia sẻ giữa các thread. Tuy nhiên, cần sử dụng mutex đúng cách để tránh trường hợp deadlock, tình huống các thread rơi vào trạng thái block mãi mãi. Vì như bài trước đã nói, nếu một thread bị crash có thể làm cả tiến trình bị crash theo. Trong bài học tiếp theo, chúng ta sẽ đi vào tìm hiểu kỹ thuật đồng bộ quan trọng tiếp theo trong Posix thread là biến điều kiện (conditional variable).

Tài liệu tham khảo

The Linux programming interface - Michael Kerrisk - Chapter 30

Programming with Posix Threads - David R. Butenhof

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