Nội dung bài viết
Phan Tấn Phúc Bài viết giúp người dùng nắm được cơ bản về thread, cách thức sử dụng thread với std::thread trong C++ (C++11). Bài viết này sẽ đưa đến cho người đọc kiến thức cơ bản để có thể lập trình đa luồng, từ đó có thể tự mình phát triển các kỹ thuật chuyên sâu hơn.

Giới thiệu

Khi quy mô của phần mềm bạn lập trình càng lớn thì vấn đề sử dụng tài nguyên sao cho hiệu quả càng quan trọng. Một trong những phương pháp để thực hiện điều này là thông qua việc sử dụng nhiều luồng chạy khác nhau, từ đó ta có thể hiện thực nhiều công việc đồng thời thay vì tuần tự như cách truyền thống. Bài viết này sẽ đưa đến cho người đọc kiến thức cơ bản để có thể lập trình đa luồng, từ đó có thể tự mình phát triển các kỹ thuật chuyên sâu hơn.

Tiền đề bài viết

Bài viết ra đời nhằm làm tăng nguồn tài liệu tham khảo cho học viên STDIO Training và lập trình viên đang trong quá trình tìm hiểu lập trình căn bản C++ (C++11).

Đối tượng hướng đến

Lập trình viên C++, đã nắm tốt cơ bản lập trình và có kiến thức về hệ điều hành.

Nhắc lại thread

Thread là gì?

Hiểu đơn thuần thread là một luồng chạy trong 1 chương trình (process), ta chỉ định công việc và hệ điều hành sẽ tự xử lý và thực hiện công việc đồng thời với luồng chạy chính.

Cách tạo 1 thread và thực thi trong C++

Tham khảo đoạn codes sau:

#include <stdio.h>
#include <thread>

void SaySomething(const char* msg)
{
	printf("MSG = %s\n", msg);
}

int main()
{
	std::thread task(SaySomething, "Hello STDIO");
	task.join();

	return 0;
}

Đoạn code này định nghĩa một hàm là void SaySomething(const char* msg) sau đó hàm sẽ cho chạy thread vừa tạo thông qua dòng std::thread task(SaySomething, "Hello STDIO") trong đó SaySomething là tên hàm vừa tạo và "Hello STDIO" là tham số truyền vào. Và cuối cùng là hàm join được gọi, hàm này có chức năng tạm dừng chương trình lại và chờ đợi thread thực hiện xong. Nếu như bạn không muốn đợi thread thì bạn có thể thay hàm join bằng hàm detach, lúc này thread sẽ trở thành độc lập và tự bản thân hoạt động.

Ví dụ về thread

Tham khảo đoạn code sau về khả năng tính toán của thread:

#include <thread>
#include <Windows.h>
#include <random>
#include <time.h>

#define MAT_SIZE 1000000
#define ROW_SIZE 1000
#define TRACK true

void calc(int* Result_ptr, int* M2_T, int* M1_row_ptr, int start, int end) {
	for (int M1_r = start; M1_r < end; ++M1_r) {
		if (TRACK)
			printf("calculating row %d\n", M1_r);
		
		int* M2_col_ptr = M2_T;
		for (int M2_c = 0; M2_c < ROW_SIZE; ++M2_c) {


			int result = 0;
			for (int i = 0; i < ROW_SIZE; ++i) {
				result += M1_row_ptr[i] * M2_col_ptr[i];
			}
			*Result_ptr++ = result;
			M2_col_ptr += ROW_SIZE;
		}
		M1_row_ptr += ROW_SIZE;

	}
}

void main() {

	int* M1 = new int[MAT_SIZE]; // 1000 x 1000
	int* M2_T = new int[MAT_SIZE]; // Ma tran chuyen vi cua M2 | 1000 x 1000
	int* M_result_sequencep = new int[MAT_SIZE];
	int* M_result_thread = new int[MAT_SIZE];

	////////////////////////////////////////////////////////
	////////// INIT ////////////////////////////////////////
	////////////////////////////////////////////////////////

	srand(0);
	int* M1_ptr = M1;
	int* M2_ptr = M2_T;
	for (int i = 0; i < MAT_SIZE; ++i) {
		*M1_ptr++ = rand() % 1000;
		*M2_ptr++ = rand() % 1000;
	}

	////////////////////////////////////////////////////////
	////////// SEQUENCE CALCULATION ////////////////////////
	////////////////////////////////////////////////////////

	int* M1_row_ptr = M1;
	int* M2_col_ptr = M2_T;
	int* Result_ptr = M_result_sequencep;

	int start = clock();
	for (int M1_r = 0; M1_r < ROW_SIZE; ++M1_r) {
		if (TRACK)
			printf("calculating row %d\n", M1_r);
		M2_col_ptr = M2_T;
		for (int M2_c = 0; M2_c < ROW_SIZE; ++M2_c) {
			int result = 0;
			for (int i = 0; i < ROW_SIZE; ++i) {
				result += M1_row_ptr[i] * M2_col_ptr[i];
			}
			*Result_ptr++ = result;
			M2_col_ptr += ROW_SIZE;

		}
		M1_row_ptr += ROW_SIZE;
		
	}
	int end = clock();

	/////////////////////////
	printf("----\n");
	////////////////////////////////////////////////////////
	////////// THREAD CALCULATION //////////////////////////
	////////////////////////////////////////////////////////
	M1_row_ptr = M1;
	M2_col_ptr = M2_T;
	Result_ptr = M_result_thread;

	int start_thread = clock();

	//calc(int* Result_ptr, int* M2_T, int* M1_row_ptr, int start, int end)
	std::thread t1(calc, Result_ptr + ROW_SIZE * ROW_SIZE / 4 * 0, M2_T, M1 + ROW_SIZE * ROW_SIZE / 4 * 0, ROW_SIZE / 4 * 0, ROW_SIZE / 4 * 1);
	std::thread t2(calc, Result_ptr + ROW_SIZE * ROW_SIZE / 4 * 1, M2_T, M1 + ROW_SIZE * ROW_SIZE / 4 * 1, ROW_SIZE / 4 * 1, ROW_SIZE / 4 * 2);
	std::thread t3(calc, Result_ptr + ROW_SIZE * ROW_SIZE / 4 * 2, M2_T, M1 + ROW_SIZE * ROW_SIZE / 4 * 2, ROW_SIZE / 4 * 2, ROW_SIZE / 4 * 3);
	std::thread t4(calc, Result_ptr + ROW_SIZE * ROW_SIZE / 4 * 3, M2_T, M1 + ROW_SIZE * ROW_SIZE / 4 * 3, ROW_SIZE / 4 * 3, ROW_SIZE / 4 * 4);
	t1.join();
	t2.join();
	t3.join();
	t4.join();

	int end_thread = clock();

	////////////////////////////////////////////////////////
	////////// OUTPUT CALCULATION //////////////////////////
	////////////////////////////////////////////////////////

	printf("Sequence time: %d\n", end - start);
	printf("thread time: %d\n", end_thread - start_thread);
	
	return 0;
}

Đoạn code trên chính là chương trình nhân hai ma trận, kích thước của ma trận là 1000 x 1000 (lưu ý rằng để thuận tiện và tăng tốc độ cho cả 2 cách, tôi đã tổ chức M1 là ma trận hàng và M2 là ma trận cột).

Khi chạy thử đoạn code, ta có thể thấy rằng với cách tính tuần tự, để nhân hai ma trận thì ta mất tầm 3.2 giây (tôi sử dụng máy dell core i3 – 2.4GHz, RAM 8GB), còn sử dụng 4 thread thì thời gian chỉ mất tầm 1.6 giây. Khi tăng kích thước ma trận lên 2000 x 2000 thì tính toán tuần tự mất 25.5 giây, còn tính toán sử dụng thread mất chỉ 11.7 giây.

Một lưu ý khi sử dụng thread là ta không nên quá lạm dụng sử dụng thread do chi phí để tạo một thread là khá lớn, 1600 thread trống mất 1 giây, vì vậy khi tôi thử tạo một triệu thread khác nhau để nhân ma trận 1000 x 1000 thì lại mất thời gian khá lớn (gần 5 phút).

THẢO LUẬN
ĐÓNG