Làm quen với Multithreading Trong C++

Dạo này loanh quanh nghe thấy từ multipe thread hơi nhiều. Từ những bài seminar đến trong project hiện tại cũng có nhiều vấn đề liên quan. Về cơ bản mình cũng không phải cao thủ gì, tuỳ nhiên cũng muốn lọ mọ 1 chút gọi là góp vui 😄. Mình làm về mobile, chủ yếu là 2 mảng Android + IOS. Nên đương nhiên là dù có viết gì, tìm hiểu cái gì thì mục đích chính vẫn là phục vụ cho 2 nền tảng này. Và về cơ bản thì thằng nào cũng có cái gốc gác từ thằng C, vì thế quyết định chiến từ gốc lên cho máu. Đáng lẽ ra thì phải viết về posix threads Bởi nó mới là thằng phục vụ chính cho các platform. Tuy nhiên code C thì có vẻ khó đọc hơn C++, hơn nữa Ubuntu với Mac là đủ để demo, mà về cơ bản thì cơ chế của nó cũng vậy. Khi các bạn đọc hiểu được std:thread thì pThread cũng hoàn toàn có các hàm tương ứng. Kỳ sau sẽ giải thích rõ hơn về vấn đề này.

Multithreading (đa luồng) là gì?

Trước hết chúng ta cùng tìm hiểu xem MultiThread là gì? Về cơ bản Multi Thread là một khả năng của một nền tảng (hệ điều hành, máy ảo vv) hoặc các ứng dụng để tạo ra một quá trình bao gồm nhiều Thread được thực thi. Một Thread thực hiện là chuỗi nhỏ nhất của hướng dẫn lập trình có thể được quản lý một cách độc lập bởi một lscheduler . Những Thread có thể chạy song song và nó có thể làm tăng hiệu quả của chương trình.

Trong các hệ thống đa lõi và đa xử lý thì đa luồng tức là các thread được thực hiện cùng lúc trên lõi hoặc bộ vi xử lý khác nhau.

Đối với hệ thống lõi đơn thì đa luồng chia thời gian giữa các thread. System sẽ gửi 1 số lượng nhất định các hướng dẫn từ mỗi Thread để xử lý. Các Thread không được thực hiện đồng thời. System chỉ mô phỏng thực hiện đồng thời của chúng. Tính năng này của System được gọi là đa luồng.

Multithreading được sử dụng khi thực hiện song song 1 số nhiệm vụ dẫn đến việc tận dụng hiệu quả hơn các tài nguyên của hệ thống.

Trong C++11 được xây dưng header thread.h để tạo ra các multithreaded C++ programs.

How to create a thread?

Đầu tiên dĩ nhiên cần include header thread vào class.

#include <thread>

Khi muốn khởi tạo 1 thread, bạn tạo 1 thread object:

thread t_empty;

Như bạn thấy, hàm khởi tạo mặc định của thread class được sử dụng. Chúng ta không chuyền bất cứ 1 thông tin nào vào thread. Tức là không có gì được chạy trong thread này. Chúng ta phải khởi tạo thread. Nó có thể được hoàn thành bằng cách khác. Khi bạn tạo 1 thread, bạn có thể truyền 1 con trỏ hàm vào khởi tạo của nó. 1 thread được khởi tạo, function sẽ bắt đầu chạy, nó chạy trong 1 thread riêng biệt:

#include <iostream>
#include <thread>
using namespace std;
void threadFunc()
{
	cout << "Welcome to Multithreading" << endl;

}
int main()
{
//truyền 1 function tới thread
    thread funcTest1(threadFunc);
}

Bạn có thể thử chạy đoạn code trên, biên dịch không có vấn đề gì, tuy nhiên bạn sẽ lập tức đối mặt với 1 runtime error: WHYYYYYY?

Thực tế thì câu trả lời khá đơn giản: Main thread tạo 1 thread mới là funcTest1 với paramaters threadFunc. Main thread không đợi funcTes1 huỷ. Nó tiếp tục hoạt động và sẽ kết thúc, nhưng funcTest1 vẫn chạy. Nó sẽ dẫn đễn lỗi. TẤT CẢ CÁC THREAD PHẢI HUỶ TRƯỚC KHI MAIN THREAD HUỶ. Vậy làm các nào khắc phục vấn đề này?????

Join threads

Thread class cung cấp method join(), hàm này chỉ return khi tất cả các thread kết thúc, điều đó có nghĩa là main thread sẽ đợi đến khi tất cả các thread con hoàn thành công việc của nó. Add thêm đoạn call hàm joind vào sample trên và chạy lại

//truyền 1 function tới thread
thread funcTest1(threadFunc);
//main sẽ block đến khi funcTest1 kết thúc
funcTest1.join();

Bây giờ khi run lại chương trình sẽ không còn lỗi nữa (ngon)

Joinable and not Joinable threads

Sau khi hàm join return, thread trở lên không thể join lại. 1 joinable thread là 1 thread mà đại diện cho 1 execution mà chưa join.

1 thread không là joinable khi nó được khởi tạo mặc định hoặc được moved/assigned tới 1 thread khác hoặc joind hoặc detach hàm đã được họi Not joinable thread có thể huỷ 1 cách an toàn. Hàm joinable để checks thread có là joinable thread hay không.

bool joinable()

Nên sử dụng hàm này trước khi call hàm join();

This function returns true if the thread is joinable and false otherwise. It’s better to check if the thread is joinable before join() function is called:

//truyền 1 function tới thread
thread funcTest1(threadFunc);
//check if thread is joinable
if (funcTest1.joinable())
{
//main is blocked until funcTest1 is not finished
    funcTest1.join();
}

Detaching thread

Như đã nhắc ở trên, thread trở thành not joinable sau khi hàm detach được gọi.

void detach()

Hàm này tách 1 thread từ 1 thread cha, nó cho phép thread cha và thread con được chạy ngay lập tức từ cái còn lại. Sau khi call detach functon, các thread sẽ không đồng bộ trong bất kỳ cách nào.

//detach funcTest1 from main thread
funcTest1.detach();
if (funcTest1.joinable())
{
	//main is blocked until funcTest1 is not finished
	funcTest1.join();
}
else
{
	cout << "functTest1 is detached" << endl;
}

Bạn sẽ nhận thấy mainthread không đợi các thread con bị huỷ!

Initializing thread with an object

Bạn có thể khởi tạo thread không chỉ với 1 function mà có thể dùng với 1 object hoặc 1 function của class.

class myFunctor
{
public:
	void operator()()
	{
		cout << "This is my function object" << endl;
	}
};

Bây giờ bạn có thể khởi tạo thread bằng cách truyền object của class myFunctor vào hàm khởi tạo của thread:

myFunctor myFunc;
thread functorTest(myFunc);
if (functorTest.joinable())
functorTest.join();

Nếu bạn muốn khởi tạo thread với 1 public function của class, bạn phải định nghĩa function và truyền object của class định nghĩa function đó:

void publicFunction()
{
	cout << "public function of myFunctor class is called" << endl;
}

Bây giờ có thể khởi tạo thread với hàm publicFunction của myFunctor class:

myFunctor myFunc;
//initializing thread with member function of myFunctor class
thread functorTest(&myFunctor::publicFunction,myFunc);
if (functorTest.joinable())
	functorTest.join();

Passing arguments to thread

Trong ví dụ trên chúng ta chỉ sử dụng hàm và đối tượng mà không phải truyền thêm các argyments vào các hàm và object. Chúng ta có thể sử dụng function vớ các paramaters cho khác hàm khởi tạo thread. Vd:

void printSomeValues(int val, char* str, double dval)
{
	cout << val << " " << str <<" " << dval << endl;

}

Có thruyền pointer tới function. Để truyền các arguments Để thấy function này có 3 arguments. Nếu bạn muốn khởi tạo với function này, trước hết bạn phải truyền con trỏ tới hàm.

char* str = "Hello";
//5, str and 3.2 are passed to printSomeValues function
thread paramPass(printSomeValues, 5, str, 3.2);
if (paramPass.joinable())
paramPass.join();

Khi bạn khởi tạo 1 thread với 1 object có params, chúng ta phải add list các param tương ứng vào. When you want to initialize a thread with an object with parameters, we have to add corresponding parameter list to the overloading version of operator ():

class myFunctorParam
{
public:
	void operator()(int* arr, int length)
	{
		cout << "An array of length " << length << "is passed to thread" << endl;
		for (int i = 0; i != length; ++i)
			cout << arr[i] << " " << endl;
		cout << endl;
	}
};

Bạn có thể thấy, operator () có 2 paramaters:

void operator()(int* arr, int length)

Khởi tạo thread với 1 object trong trường hợp này giống như sử dụng hàm với các paramater:

//these parameters will be passed to thread
int arr[5] = { 1, 3, 5, 7, 9 };
myFunctorParam objParamPass;
thread test(objParamPass, arr, 5);
if (test.joinable())
	test.join();

Có thể sử dụng 1 hàm của class như params của thread. Add 1 public function vào myFunctorParam class:

void changeSign(int* arr, int length)
{
	cout << "An arrray of length " << length << "is passed to thread" << endl;
	for (int i = 0; i != length; ++i)
		cout << arr[i] << " ";
	cout << "Changing sign of all elements of initial array" << endl;
	for (int i = 0; i != length; ++i)
	{
		arr[i] *= -1;
		cout << arr[i] << " ";
	}
}

Truyền argument vào member function:

int arr2[5] = { -1, 3, 5, -7, 0 };
//initialize thread with member function
thread test2(&myFunctorParam::changeSign, &objParamPass, arr2, 5);
if (test2.joinable())
	test2.join();

... Đến đây tạm thời kết thúc phần 1: Các bạn đã hiểu cơ bản về các khái niệm khởi tạo, sử dụng thread. Trong phần tiếp theo chúng ta sẽ lần lượt về Thread ID, name space, Concurrent , mutex... Và các cách quản lý thread trong Android/ IOS.