Tài liệu thực hành môn Mạng máy tính - Bài: Lập trình đa tiểu trình - Thread
Bộ môn Mạng máy tính và Viễn thông
Khoa CNTT – Trường ĐH KHTN Tp. HCM
Lập trình đa tiểu trình - Thread
1.
2.
Mục tiêu
SV tiếp cận bài toán đa tiểu trình và các thao tác trên tiểu trình.
Đặt vấn đề
Giả sử ta có n (n ³ 2) công việc và muốn thực hiện n công việc này đồng thời. Có
nhiều cách giải quyết vấn đề này, trong bài này sẽ giới thiệu một giải pháp là dử dụng
thread, mỗi thread sẽ thực thi một công việc và thực thi song song với các công việc
còn lại.
3.
Tạo và hủy tiểu trình
1.1. Giới thiệu bài toán
Hãy viết chương trình theo kiểu bấm giờ bằng cách mỗi lần nhấn một phím thì sẽ
in ra số nhịp đã trôi qua từ khi chương trình bắt đầu chạy, với mỗi nhịp bằng 100ms,
chương trình sẽ kết thúc khi nhấn ESC. Với hàm sử dụng là hàm _getch, quyền điều
khiển sẽ không được trả lại cho đến khi có một phím được nhấn, ta sẽ không thể thực
hiện việc tăng biến đếm sau mỗi nhịp.
Có nhiều cách tiếp cận để giải quyết yêu cầu. Có thể dùng cơ chế Timer nhưng
vẫn cần phải có một luồng (tiểu trình) khác để nhận WM_TIMER. Hoặc có thể kiểm tra
chỉ khi nào phím đã được nhấn mới gọi hàm _getch, cách này khá phức tạp và không hiệu
quả khi yêu cầu bài toán không chỉ là đếm giờ mà còn phải đọc đĩa, tương tác với mạng...
1.2. Tiếp cận bài toán theo hướng đa luồng
Giả sử ta có 2 luồng thực hiện 2 công việc riêng biệt (giống như 2 người làm 2
nhiệm vụ riêng biệt) như sau:
+ Luồng CounterThread: thực hiện tăng biến đếm nCount, delay trong
100ms, rồi lại tăng biến đếm nCount...
while (!bESCPressed)
{
nCount++;
Sleep(100);
}
+ Luồng GetKeyThread: nhận một phím bằng hàm _getch, in giá trị biến
đếm nCount, rồi lại nhận một phím _getch... cho đến khi phím ESC được nhấn.
do {
bESCPressed = (_getch() == 27);
printf("%d\n", nCount);
- 1 -
Bộ môn Mạng máy tính và Viễn thông
Khoa CNTT – Trường ĐH KHTN Tp. HCM
} while (!bESCPressed);
Rõ ràng với việc sử dụng 2 luồng cho bài toán trên, vấn đề trở nên đơn giản.
1.3. Điều kiện để một chương trình VC++ chạy được ở chế độ đa luồng
Nhấn Alt-F7 để vào trong Project\Settings...
Nếu như chế độ Use run-time library: đang để ở Single-Threaded hoặc Debug
Single-Threaded thì phải chuyển sang chế độ Multithreaded tương ứng là Multithreaded
và Debug Multithreaded.
chuyển
thành ==>
Cách tạo một tiểu trình mới trong Visual C++
Trong Windows để khởi động một luồng mới, thì ta phải cài đặt luồng đó trong
một hàm, khi hàm kết thúc thì luồng được tạo cũng tự kết thúc.
Để tạo một luồng mới ta dùng hàm CreateThread
HANDLE CreateThread
- 2 -
Bộ môn Mạng máy tính và Viễn thông
Khoa CNTT – Trường ĐH KHTN Tp. HCM
{
SEC_ATTRS SecurityAttributes,
ULONG StackSize,
SEC_THREAD_START StartFunction,
PVOID ThreadParameter,
ULONG CreationFlags,
PULONG ThreadId
};
Trong đó:
SecurityAttributes: trỏ đến cấu trúc SECURITY_ATTRIBUTES dùng để xác định
handle của tiến trình mới tạo có được kế thừa bởi các tiến trình con không. Chú ý: trong
Windows 9x nếu tham số này mang giá trị NULL thì handle này không thể kế thừa bởi
tiến trình con khác, trong Windows NT nếu mang giá trị NULL có nghĩa là tiểu trình này
sử dụng chế độ an toàn mặc định.
dwStackSize: kích thước ban đầu của stack cục bộ của tiểu trình.Nếu có giá trị 0
hay nhỏ hơn kích thước mặc định thì hệ thống dùng kích thước của tiểu trình tạo và tự
động tăng kích thước khi cần. Nếu không đủ bộ nhớ thì hàm tạo tiểu trình mới sẽ thất bại.
Vùng stack này tự động được giải phóng khi tiểu trình kết thúc.
lpStartAddress: tên hàm mô tả công việc mà tiểu trình cần thực hiện. Hàm này
được khai báo như sau:
DWORD WINAPI ThreadProc(LPVOID lpParameter);
lpParameter: tham số được truyền cho tiểu trình. Lưu ý: tham số truyền vào phải
ép kiểu thành LPVOID sau đó vô hàm ép kiểu ngược trở lại.
dwCreationFlags: qui định trạng thái tiểu trình khi mới tạo lập. Nếu mang giá trị 0,
tiểu trình tạo ra sẽ thực thi ngay; nếu là CREATE_SUSPENDED , tiểu trình sẽ chờ cho
đến khi gọi hàm ResumeThread.
lpThreadId: con trỏ nhận giá trị trả về là ID của tiểu trình.
Nếu thành công, nhận giá trị trả về là handle của tiểu trình. Ngược lại nhận giá trị
NULL.
Mỗi tiểu trình sau khi được tạo đều có cùng độ ưu tiên mặc định là
THREAD_PRIORITY_NORMAL. Ta có thể thay đổi hay xác định bằng hàm
SetThreadPriority và GetThreadPriority.
hoặc sử dụng hàm:
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
- 3 -
Bộ môn Mạng máy tính và Viễn thông
Khoa CNTT – Trường ĐH KHTN Tp. HCM
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
CWinThread* AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
Trong đó:
pfnThreadProc: tên hàm mô tả công việc mà tiểu trình cần thực hiện.
UINT MyControllingFunction( LPVOID pParam );
pParam: tham số được truyền cho tiểu trình.
nPriority: độ ưu tiên của thread.
pThreadClass: tên lớp kế thừa từ CWinThread.
Hàm này trả về con trỏ kiểu CWinThread nếu thành công, ngược lại trả về NULL
Ví dụ: khi muốn tạo tiểu trình CouterThread, ta thực hiện
Cách 1:
Hàm CounterThread được cài đặt như sau:
DWORD WINAPI CounterThread(LPVOID /*pParam*/)
{
while (!bESCPressed) {
nCounter++;
Sleep(100);
}
return 0;
}
do pParam ít sử dụng nên ta không cần khai báo để tránh bị lỗi warning
// tạo thread
DWORD dwThreadIdCounter;
HANDLE hThreadCounter;
hThreadCounter = CreateThread(NULL, 0,
CounterThread, NULL, 0, &dwThreadIdCounter);
Cách 2:
// hàm xử lý thread cài đặt giống cách 1
// tạo thread
CWinThread *h1;
h1 = AfxBeginThread((AFX_THREADPROC) CounterThread, NULL,
THREAD_PRIORITY_NORMAL,0,0,NULL);
- 4 -
Bộ môn Mạng máy tính và Viễn thông
Khoa CNTT – Trường ĐH KHTN Tp. HCM
Cách 3: tạo 1 lớp kế thừa từ CWinThread: CCounterThread, khi đó hàm Run (add
virtual function) sẽ chứa đoạn code xử lý thread
// Hàm Run của lớp CCounterThread được cài đặt như sau:
int CCounterThread::Run()
{
// TODO: Add your specialized code here and/or call the base class
while (!bESCPressed)
{
nCounter++;
Sleep(100);
}
return CWinThread::Run();
}
// tạo thread
CCounterThread* h1;
h1 = (CCounterThread*)AfxBeginThread(RUNTIME_CLASS(CCounterThread),
THREAD_PRIORITY_NORMAL,0,0,NULL);
- 5 -
Bộ môn Mạng máy tính và Viễn thông
Khoa CNTT – Trường ĐH KHTN Tp. HCM
1.4. Thay đổi trạng thái của tiểu trình
( 2 )
( 3 )
( 1 )
( 4 )
Môùi taïo
Running
( 6 )
Keát thuùc
Ready
( 5 )
Sleeping
Waiting
( 8 )
( 7 )
( 10 )
( 9 )
Suspended
( 11 )
1.4.1. Hàm tạm dừng 1 tiểu trình
DWORD
SuspendThread
{
HANDLE hThread
};
Trong đó:
hThread: handle của tiểu trình muốn tạm dừng
Hàm SuspendThread tạm dừng hoạt động của 1 tiểu trình. Giá trị trả về số lần yêu cầu
tạm dừng (suspend count) trước khi tăng hoặc 0xFFFFFFFF nếu có lỗi. (suspend count sẽ
được tăng lên 1 mỗi lần yêu cầu tạm dừng tiểu trình).
1.4.2. Hàm Huỷ trạng thái tạm dừng của 1 tiều trình
DWORD
{
ResumeThread
HANDLE hThread
};
- 6 -
Bộ môn Mạng máy tính và Viễn thông
Khoa CNTT – Trường ĐH KHTN Tp. HCM
Trong đó:
hThread: handle của tiểu trình muốn huỷ trạng thái tạm dừng
Hủy bỏ trạng thái tạm dừng của 1 tiểu trình bằng cách kiểm tra giá trị của số lần
yêu cầu tạm dừng (suspend count), nếu suspend count > 0 , thì sẽ giảm đi 1 để huỷ bỏ 1
lần yêu cầu tạm dừng tiểu trình tương ứng, khi suspend count = 0, ResumeThread sẽ khôi
phục lại hoạt động của tiểu trình. Giá trị trả về của hàm này là suspend count lúc chưa
giảm hoặc 0xFFFFFFFF nếu có lỗi.
1.5. Kết thúc một tiểu trình
Ngoài việc một tiểu trình tự kết thúc khi hàm cài đặt của tiểu trình kết thúc, ta còn
có thể yêu cầu tiểu trình kết thúc tức thời. Tuy nhiên, việc kết thúc hàm theo kiểu “thô
bạo” như vậy sẽ dẫn đến việc không hủy bỏ đúng đắn những tài nguyên đã được cấp như
mở file, kết nối mạng...
Kết thúc tiểu trình hiện hành: dùng hàm ExitThread(dwExitCode) trong đó
dwExitCode là mã lỗi trả về, thường là bằng 0.
Để kết thúc một tiểu trình khác ta phải có handle của tiểu trình đó (hThread) được
trả về khi tạo tiểu trình, khi đó dùng hàm TerminateThread(hThread, dwExitCode) để
kết thúc một tiểu trình.
1.6. Các hàm khác
GetThreadTimes
BOOL
{
GetThreadTimes
HANDLE
hThread,
LPFILETIME
LPFILETIME
LPFILETIME
LPFILETIME
lpCreationTime,
lpExitTime,
lpKernelTime,
lpUserTime
};
Trả về thời điểm tạo lập tiểu trình lpCreationTime, thời điểm kết thúc tiểu trình
lpExitTime, thời gian tiểu trình hoạt động trong chế độ User lpUserTime, trong chế độ
Kernel lpKernelTime của tiểu trình hThread.
Hàm GetExitCodeThread
BOOL
{
GetExitCodeThread
HANDLE hThread,
- 7 -
Bộ môn Mạng máy tính và Viễn thông
Khoa CNTT – Trường ĐH KHTN Tp. HCM
LPDWORD lpExitCode
};
Xác định trạng thái kết thúc của tiểu trình hThread. Nếu tiểu trình đang hoạt động,
ExitCode nhận giá trị STILL_ACTIVE.
Hàm GetCurrendThreadId
HANDLE GetCurrentThreadId (void);
Trả về định danh duy nhất của tiểu trình đang hoạt động.
Hàm GetCurrentThread
HANDLE GetCurrentThread (void);
Trả về handle của tiểu trình đang hoạt động.
Lưu ý:
§ Đối với lớp CWinThread, các hàm trên đều là hàm thuộc lớp cWinThread chỉ có
hàm Terminate là không có. Tuy nhiên, có thể thao tác bình thường với các thread
đó thong qua hThread trong lớp CWinThread.
§ Trong môi trường C#, các bạn sẽ sử dụng lớp Thread trong
System.Threading.Thread, sử dụng các hàm: Start, ThreadStart, Resume, Suspend,
Abort…. (tham khảo thêm VD trong MSDN)
4.
Bài tập
Bài tập 1: Viết chương trình tăng giảm số. Chương trình gồm có 2 tiểu trình, tiểu
trình thứ nhất thực hiện việc tăng giá trị của biến x, trình thứ hai thực hiện việc giảm giá
trị của biến x. Sau khi tăng giảm xong xuất nội dung của biến đó ra màn hình.
Bài tập 2: Viết chương trình chương trình hiển thị 10 ký tự ngẫu nhiên trên màn
hình, mỗi ký tự được quản lý bởi một tiểu trình và di chuyển liên tục với tốc độ tùy ý.
Khi di chuyển nếu chạm biên màn hình thì ký tự sẽ xuất hiện lại tại giữa màn hình.
Chương trình kết thúc khi người dùng nhấn phím bất kỳ.
Bài tập 3: Viết chương trình tạo ra 3 tiểu trình, mỗi tiểu trình quản lý một ô vuông
có màu khác nhau di chuyển tự do trên màn hình. Chương trình cho phép start, stop,
suspend, resume, set priority từng tiểu trình.
Bài tập 4: Viết chương trình có 1 dấu * di chuyển chéo trên màn hình, khi gặp
biên dấu * này sẽ dội lại (phản xạ). Tốc độ di chuyển giữa 2 ô kế nhau là 500ms. Khi
người dùng nhấn các phím ↑↓←→ hoặc các phím W, Z, A, S thì ngay lập tức dấu * sẽ di
chuyển lên trên, xuống dưới, sang trái, sang phải. Chương trình kết thúc khi người dùng
nhấn phím ESC.
- 8 -
Bộ môn Mạng máy tính và Viễn thông
Khoa CNTT – Trường ĐH KHTN Tp. HCM
Bài tập 5: Viết chương trình cuộn chuỗi. Chương trình có hai tiến trình thực hiện
thao tác sau: tiến trình thứ nhất thực hiện thao tác cuộn chuỗi, tiến trình thứ hai xuất
chuỗi đang cuộn ra màn hình.
Lưu ý:
§ Các bài tập không đặt nặng giao diện, chỉ yêu cầu hiểu được cách tạo thread
và các thao tác trên thread.
§ Đối với bài 1, 5: các bạn nên làm trên Console.
§ Đối với các bài tập 2,3,4 nếu các bạn làm trên Console, các bạn xem thêm
file hướng dẫn console.doc để biết cách xuất 1 ký tự ra màn hình tại 1 toạ
độ xy. Nếu làm trên MFC, các bạn làm dạng View (Single Document)
Các câu hỏi thường gặp khi tạo thread:
1. Trong console, tạo thread nhưng thread không chạy.
Trả lời: do hàm main sau khi tạo thread xong kết thúc luôn chương trình è tất cả
các tiểu trình đều kết thúc.
2. Truyền nhiều tham số vào trong 1 thread.
Trả lời: nếu muốn truyền nhiều tham số vào trong 1 thread thì phải tạo 1 struct
chứa các tham số đó sau đó truyền struct đó vào hàm.
MyStruct b;
b = *(MyStruct*)lp; // chỉ cần lấy nội dung của tham số truyền vào
AfxBeginThread((AFX_THREADPROC)CounterThread,(LPVOID)&b,
THREAD_PRIORITY_NORMAL,0,0,NULL);
Nếu sử dụng lớp CWinThread, bạn nên add các biến thành viên cho lớp và gán trị
trược tiếp. Khi đó, bạn cần tạo thread với cờ là CREATE_SUNSPENDED sau đó gọi
hàm ResumeThread
- 9 -
Bạn đang xem tài liệu "Tài liệu thực hành môn Mạng máy tính - Bài: Lập trình đa tiểu trình - Thread", để tải tài liệu gốc về máy hãy click vào nút Download ở trên
File đính kèm:
tai_lieu_thuc_hanh_mon_mang_may_tinh_bai_lap_trinh_da_tieu_t.pdf