Làm quen với Multithreading (P2)

Như trong Phần 1. Các bạn đã được làm quen với các khái niệm khi sử dụng thread như init, join, joinable, detach thread. Phần này xin được tiếp tục giới thiệu về các khái niệm tiếm theo như Thread ID, name space, Concurrent , mutex... để có 1 cái nhìn tổng quan hơn về multithreading trước khi chúng ta đi vào ứng dụng thực tế.

ThreadID

Mọi thread đều có 1 unique identifier. Và trong class thread có 1 public function cho phép get ra giá trị của thread Id.

id get_id()

giá trị trả về kiểu id được định nghĩa trong Thread class Vd:

//create 3 different threads
thread t1(showMessage);
thread t2(showMessage);
thread t3(showMessage);
//get id of all the threads
thread::id id1 = t1.get_id();
thread::id id2 = t2.get_id();
thread::id id3 = t3.get_id();
//join all the threads
if (t1.joinable())
{
	t1.join();
	cout << "Thread with id " << id1 << " is terminated" << endl;
}
if (t2.joinable())
{
	t2.join();
	cout << "Thread with id " << id2 << " is terminated" << endl;
}
if (t3.joinable())
{
	t3.join();
	cout << "Thread with id " << id3 << " is terminated" << endl;
}

Mỗi thread sẽ print out unique identifier sau khi kết thúc execution:

Thread with id 8228 is terminated

Thread with id 10948 is terminated

Thread with id 9552 is terminated

this_thread Namespace

this_thread namespace từ thread header cung cấp khả năng làm việc với thread hiện tại. Namespace này chứa 4 chứa năng hữu ích:

  1. id_get_id() – trả về id của current thread.

  2. template void sleep_until (const chrono::time_point<Clock,Duration>& abs_time) – blocks lại current thread cho đến khi abs_time không đạt được!

  3. template void sleep_for (const chrono::duration<Rep,Period>& rel_time); – thread được block lại trong khoảng thời gian quy định bởi rel_time.

  4. void yield() – Thread hiện tại cho phép implement để sắp xếp lại việc thưc hiện thread. Nó được sử dụng để tránh blocking.

Vd:

#include <iostream>
#include <iomanip>
#include <thread>
#include <chrono>
#include <ctime>

using namespace std;
using std::chrono::system_clock;
int main()
{
	cout << "The id of current thread is " << this_thread::get_id << endl;

	//sleep while next minute is not reached

	//get current time
	time_t timet = system_clock::to_time_t(system_clock::now());
	//convert it to tm struct
	struct tm * time = localtime(&timet);
	cout << "Current time: " << put_time(time, "%X") << '\n';
	std::cout << "Waiting for the next minute to begin...\n";
	time->tm_min++; time->tm_sec = 0;
	//sleep until next minute is not reached
	this_thread::sleep_until(system_clock::from_time_t(mktime(time)));
	cout << std::put_time(time, "%X") << " reached!\n";
	//sleep for 5 seconds
	this_thread::sleep_for(chrono::seconds(5));
	//get current time
	timet = system_clock::to_time_t(system_clock::now());
	//convert it to tm struct
	time = std::localtime(&timet);
	cout << "Current time: " << put_time(time, "%X") << '\n';
}

Bạn sẽ có output dựa trên current time của bạn:

The id of current thread is 009717C6

Current time: 15:28:35

Waiting for the next minute to begin...

15:29:00 reached!

Current time: 15:29:05

Concurrent access to resources

Lập trình multithreading phải đối mặt với 1 vấn đề là truy câp đồng thời đến 1 tài nguyên được chia sẻ. Việc tru cập đồng thời đến 1 nguồn tài nguyên sẽ dẫn đến nhiều lỗi cũng như sự hỗn loạn trong chương trình.

Cùng xem ví dụ dưới:

vector<int> vec;
void push()
{
	for (int i = 0; i != 10; ++i)
	{
		cout << "Push " << i << endl;
		_sleep(500);
		vec.push_back(i);
	}
}
void pop()
{
	for (int i = 0; i != 10; ++i)
	{
		if (vec.size() > 0)
		{
			int val = vec.back();
			vec.pop_back();
			cout << "Pop "<< val << endl;
		}
	_sleep(500);
	}
}
int main()
{
	//create two threads
	thread push(push);
	thread pop(pop);
	if (push.joinable())
		push.join();
	if (pop.joinable())
		pop.join();
}

Như bạn thấy, ta có 1 biến global vector vec. 2 thread push và pop cùng cố gắng truy cập vào vector này cùng lúc. Thread push cố gắng đẩy 1 element vào vector trong khi thread pop lại cố gắng lấy 1 phần tử từ vector ra. Việc truy cập vào vector không được đồng bộ, Thread đang truy cập vector không liên tục, bởi vì truy cập đồng thời vào dữ liệu được chia sẽ thì nhiều lỗi có thể xuất hiện.

Mutex

Class mutex laf 1 đồng bộ hoá nguyên thuỷ được sử dụng để bảo vệ được chia sẻ từ các simultaneous access (truy cập cùng lúc). 1 mutex có thể được khoá và mở. Nếu 1 mutex đươcj khoá, thread hiện tại chứa mutex đó cho đến khi nó chưa được unlock trở lại. Nghĩa là không 1 thread nào khác có thể thực hiện bất kỳ instructions từ các khối code được bao quanh bởi mutex cho đến khi thread hiện tại mở nó. Nếu bạn muốn sử dụng mutex, bạn cần include mutex header vào trong chương trình:

#include <mutex>

Sau đó bạn phải tạo 1 biến global kiểu mutex. Nó sẽ được sử dụng để đồng bộ các truy cập vào dữ liệu được chia sẻ: Mỗi khi bạn muốn phần nào của chương trình chỉ được sử dụng bởi 1 thread trong cùng 1 thời gian, bạn sử dụng lock trong mutex:

Đoạn code ở bên trên có thể được sửa như sau:

void push()
{
	m.lock();
		for (int i = 0; i != 10; ++i)
		{
			cout << "Push " << i << endl;
			_sleep(500);
			vec.push_back(i);
		}
	m.unlock();
}
void pop()
{
	m.lock();
	for (int i = 0; i != 10; ++i)
	{
		if (vec.size() > 0)
		{
			int val = vec.back();
			vec.pop_back();
			cout << "Pop " << val << endl;
		}
	_sleep(500);
	}
	m.unlock();
}

Như bạn thấy push và pop đã được khoá bằng mutex. Do đó nếu 1 thread chạy đoạn code được lock bởi mutext, không có thread nào có thể chạy cho đến khi mutex được mở khoá, bạn có thể run đoạn code 1 lần nữa:

//create two threads
thread push(push);
thread pop(pop);
if (push.joinable())
	push.join();
if (pop.joinable())
	pop.join();

Bây giờ vector đã được đồng bộ hoá:

Push 0

Push 1

Push 2

Push 3

Push 4

Push 5

Push 6

Push 7

Push 8

Push 9

Pop 9

Pop 8

Pop 7

Pop 6

Pop 5

Pop 4

Pop 3

Pop 2

Pop 1

Pop

Chúng ta có thể mô tả bằng 1 ví dụ khác của việc sử dụng mutex, 1 ví dụ về cuộc sống thực:

Rất nhiều người cùng tới 1 box điện thoại công cộng để gọi điện, người nào giữ cửa sẽ là người duy nhất được sử dụng điện thoại. Người đó phải giữ cánh cửa để dử dụng điện thoại, nếu không người khác sẽ mở cửa và đá đít anh ta ra ngoài để chiếm quyền dùng điện thoại 😄. Không có xếp hàng thứ tự như trong cuộc sống thực. Máy móc không có biết lịch sự =)). Khi kết thúc cuộc gọi anh ta sẽ nhả cánh cửa ra và bước ra ngoài, người tiếp theo sẽ chiếm giữ cánh cửa và sử dụng điện thoại.

Trong trường hợp này bạn có thể tưởng tượng và định nghĩa các thành phần trong việc truy cập đồng bộ dữ liệu như sau:

1 Thread là 1 người. Mutex là cánh cửa. Lock là cánh tay của người ấy. Dữ liệu chia sẻ chính là cái điện thoại.

Bất kỳ thread nào thực hiện những dòng code mà không thể được thực hiện bởi các thread khác trong cùng 1 thời điểm (giống như việc gọi điện), phải được có 1 khoá trên mutex (giữ cánh cửa). Sau đó, thread có thể thực hiện đoạn code này (gọi điện).

Khi thread kết thúc công việc của mình, nó phải giải phóng khoá trên mutex để 1 thread khác có thể có khoá trên mutex (người khác được quyền dùng điện thoại). Ví dụ trên khi viết code sẽ như sau:

std::mutex m;//door handle

void makeACall()
{
	m.lock();//person enters the call box and locks the door
	//now it can talk to his friend without any interruption
	cout << " Hello my friend, this is " << this_thread::get_id() << endl;
	//this person finished to talk to his friend
	m.unlock();//and he leaves the call box and unlock the door
}
int main()
{
	//create 3 persons who want to make a call from call box
	thread person1(makeACall);
	thread person2(makeACall);
	thread person3(makeACall);
	if (person1.joinable())
	{
		person1.join();
	}
	if (person2.joinable())
	{
		person2.join();
	}
	if (person3.joinable())
	{
		person3.join();
	}
}

Việc truy cập và thực hiện chức năng makeACall sẽ được đồng bộ như sau:

Hello my friend, this is 3636

Hello my friend, this is 5680

Hello my friend, this is 928

Như vậy với bài viết này, các bạn đã có thể có thêm những hiểu biết về thread id, thread namespace, concurent trong viêc sửa dụng share data cũng như cách sử dụng mutex để giải quyết vấn đề này. Bài viết sau sẽ lần lượt có hướng dẫn về việc thực thi những khái niệm này trong Mobile code như Java Android, IOS (Objective C or Swift code)... Cảm ơn các bạn đã đọc bài viết!