ทดลองเขียนโปรแกรมสร้าง Thread (Visual C++)
ในการเขียนโปรแกรมเพื่อสร้าง Thread ให้ทำงานบางอย่าง ท่านสามารถทำได้กับโปรแกรมที่เป็น Dialog-based หรือ SDI/MDI ก็ได้ แต่ต้องเป็น MFC นะครับ เราจะมาดูตัวอย่างโค้ดที่เขียนโดยใช้โปรเจ็กต์แบบ Dialog-based กัน เพราะจะดูเข้าใจง่ายกว่าแบบ SDI (มีคลาสแค่ 2 คลาส) โดยตัวอย่างที่เราจะใช้กันนี้ เราตั้งชื่อโปรเจ็กต์ว่า ThreadMFC ซึ่งใน Project เราจะมีคลาสอยู่ด้วยกันทั้งหมด 2 คลาสคือ
- 1. CThreadMFCApp สืบทอดจาก CWinApp โค้ดจะอยู่ในไฟล์ ThreadMFC.h และ ThreadMFC.cpp
- 2. CThreadMFCDlg สืบทอดจาก CDialog โค้ดอยู่ในไฟล์ ThreadMFCDlg.h และ ThreadMFCDlg.cpp
เราจะแก้ไข Project นี้ ให้มีโครงสร้างการทำงานของ Thread เอาไว้ก่อน เมื่อท่านดูโครงสร้างเข้าใจแล้ว เราก็จะมาเขียนโค้ดให้ทำงานใน Thread นั้นกัน ดังนี้
ขั้นตอนที่ 1 ประกาศตัวแปรและฟังก์ชั่นต้นแบบที่จำเป็น
เราจะเปิดไฟล์ที่มีการประกาศคลาสไดอะล็อกขึ้นมา (ThreadMFCDlg.h) เขียนโค้ดประกาศโครงสร้างและตัวแปรต่างๆ ลงไป ดังนี้
#define WM_THREADCOMPLETE (WM_USER+1)
struct JOB
{
HWND mainwindow;
};
UINT ThreadProc(LPVOID pParam);
class CThreadMFCDlg : public CDialog
{
// Construction
public:
CThreadMFCDlg(CWnd* pParent = NULL);
void OnThreadComplete(WPARAM wParam, LPARAM lParam);
JOB myjob;
CWinThread *thread;
// Dialog Data
//{{AFX_DATA(CThreadMFCDlg)
enum { IDD = IDD_THREADMFC_DIALOG };
//}}AFX_DATA
………………….
จากโค้ด เราได้ประกาศค่าคงที่ WM_THREADCOMPLETE ให้มีค่าคงที่เป็น WM_USER+1 ซึ่งเป็นค่าคงที่ของสัญญาณที่เราจะให้ Threadของเราตอบกลับมาเมื่อทำงานเสร็จแล้ว และเราก็ได้ประกาศโครงสร้าง JOB ซึ่งเป็นโครงสร้างที่เราจะส่งให้กับ Thread ในเวลาที่เราสร้าง Thread นั้นขึ้นมา เราจะเอาค่าของหน้าต่างหลัก (Handle of Main Window) ส่งไปให้กับ Thread พร้อมๆ กับค่าที่ต้องการให้ Threadประมวลผล อันนี้เดี๋ยวเราจะมาเขียนเพิ่มเข้าไปภายหลัง
จากนั้นเราก็ได้ประกาศต้นแบบฟังก์ชั่น ThreadProc ซึ่งเป็นฟังก์ชั่นที่จะให้ Thread ของเราเข้าไปทำงานที่นี่.... สำหรับโค้ดในส่วนของคลาส เราก็ได้สร้างตัวแปรโครงสร้าง myjob และ object ของ CWinThread ที่เป็น Pointer ตั้งชื่อว่า thread และยังมีฟังก์ชั่น OnThreadComplete ซึ่งเป็นฟังก์ชั่นที่เราจะใช้ตอบรับเมสเสจ WM_THREADCOMPLETE ซึ่งเราจะต้องแมปเมสเสจตัวนี้กับฟังก์ชั่นนี้ในหัวข้อต่อไป
ขั้นตอนที่ 2 แมสเสจเสจ WM_THREADCOMPLETE
ให้ทำการแมปเมสเสจนี้กับฟังก์ชั่น OnThreadComplete ซึ่งจะเป็นฟังก์ชั่นที่ใช้ในการตอบรับเมื่อ Thread ทำงานเสร็จแล้ว เราจะต้องไปเขียนโค้ดเพื่อแมปเอาไว้ที่ไฟล์ ThreadMFCDlg.cpp ดังนี้
BEGIN_MESSAGE_MAP(CThreadMFCDlg, CDialog)
ON_MESSAGE(WM_THREADCOMPLETE,OnThreadComplete)
//{{AFX_MSG_MAP(CThreadMFCDlg)
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
ขั้นตอนที่ 3 สร้างฟังก์ชั่น ThreadProc และ OnThreadComplete
ตอนแรก เราได้ประกาศต้นแบบฟังก์ชั่น ThreadProc เอาไว้ในไฟล์ .h แล้วใช่มั้ยครับ เราจะมาเขียนตัว body ของฟังก์ชั่นเอาไว้ในไฟล์ ThreadMFCDlg.cpp ดังนี้
UINT ThreadProc(LPVOID pParam)
{
JOB *jj=(JOB*)pParam;
// Processing your job
::PostMessage( jj->mainwindow , WM_THREADCOMPLETE,0,0);
return 0;
}
สำหรับฟังก์ชั่น OnThreadComplete ก็เขียนต่อไปได้เลย ดังนี้
void CThreadMFCDlg::OnThreadComplete(WPARAM wParam, LPARAM lParam)
{
TerminateThread(thread,0);
}
จากฟังก์ชั่น จะเห็นได้ว่าฟังก์ชั่นจะรับพารามิเตอร์ 1 ตัว ซึ่งก็คือ “ตัวงาน” นั่นเอง โดยเจ้าตัวงานนี้ เราจะส่งมาให้ในตอนที่เราสร้าง Thread ดังนั้น ฟังก์ชั่นนี้ จึงต้องเขียนรองรับเอาไว้ก่อน เพื่อเอางานไปใช้ โดยเราก็ได้สร้างโครงสร้างที่ชื่อ JOB ขึ้นมาในขั้นตอนที่ 1 และในโครงสร้าง JOB นี้ ก็มีตัวแปร mainwindow ซึ่งเป็น HWND นั่นก็คือ Handle ของหน้าต่าง เพราะเมื่อเวลาที่ฟังก์ชั่น ThreadProc จบการทำงานแล้ว เราจะต้องส่งสัญญาณตีกลับไปที่หน้าต่างหลัก ตรงนี้ล่ะครับ เราจึงต้องใช้ตัวแปร mainwindow ที่ส่งมาพร้อมกับ “งาน” อ้างถึงหน้าต่างหลักที่เป็นเจ้าของ Thread นี้ เพื่อบอกว่างานที่สั่งให้ทำนั้นสำเร็จลุล่วงด้วยดีแล้ว นั่นเอง โดยฟังก์ชั่น PostMessage เป็นฟังก์ชั่นที่ใช้ในการส่งสัญญาณเมสเสจตอบกลับไป มันจะส่งเมสเสจ WM_THREADCOMPLETE ไปให้หน้าต่าง jj->mainwindow พร้อมกับค่า LPARAM และ WPARAM เป็น 0 หมด เมื่อหน้าต่างหลักได้รับเมสเสจนี้ มันก็จะดูว่ามีการแมปเมสเสจนี้เอาไว้หรือเปล่า ถ้ามีมันก็จะเรียกฟังก์ชั่น OnThreadComplete ทันที และในฟังก์ชั่นนี้ก็จะทำการสิ้นสุดการทำงานของ Thread ไปเลย
ขั้นตอนที่ 4 เขียนโปรแกรมให้เริ่มการทำงานของ Thread
เราจะลองเขียนโปรแกรมให้เริ่มการทำงานของ Thread แบบง่ายๆ ดู โดยเราจะแก้ไขโครงสร้าง JOB ให้เป็นดังนี้
struct JOB
{
int number[1000];
int total;
HWND mainwindow;
};
จากนั้น ให้ท่านแก้ไขไดอะล็อกให้เป็นแบบนี้
ช่อง Edit Control ที่ท่านสร้าง ให้กำหนด ID เป็น IDC_SUM และสร้างตัวแปร DDX แบบ int ขึ้นมาตั้งชื่อว่า m_sum เอาไว้ จากนั้นให้ทำการแมปเมสเสจ BN_CLICKED ให้กับปุ่ม OK และเขียนโค้ดลงไปดังนี้
void CThreadMFCDlg::OnOK()
{
for (int i=0;i<1000;i++)
myjob.number[i]=rand()%4000+2;
myjob.mainwindow=m_hWnd;
myjob.total=0;
thread = AfxBeginThread(ThreadProc,&myjob);
}
จากโค้ด เราได้ทำให้ปุ่ม OK เป็นปุ่มที่เริ่มการทำงานของ Thread ดูให้ดีนะครับ มันจะชิ่งกันสนุกมาก ดูตามนะครับ.... เริ่มต้น เมื่อกด OKมันจะสุ่มค่าไปเก็บไว้ใน myjob.number ที่เราประกาศเอาไว้ โอเคนะครับ................
จากนั้นมันก็จะกำหนดค่า Handle ของหน้าต่างหลัก โดยใช้ตัวแปร m_hWnd ซึ่งเป็นตัวแปรที่ระบุถึงหน้าต่างหลักของโปรแกรมเรา เก็บค่านี้ไว้ใน myjob.mainwindow อันนี้ควรจะทำนะครับ จะไม่ทำก็ได้ แต่เราจะไม่มีทางรู้เลยว่าเมื่อใดที่ Thread ของเราทำงานเสร็จแล้ว เพราะมันไม่มีอะไรแจ้งกลับมาเลย ถูกมั้ยครับ.... ดังนั้น เราก็เรียกฟังก์ชั่น AfxBeginThread พร้อมกับกำหนด ThreadProc เพื่อบอกว่าฟังก์ชั่นอะไรเป็นฟังก์ชั่นที่จะให้ Thread ไปทำ และส่งค่า myjob ซึ่งส่งแบบ address ไปให้ เพื่อเป็นการให้ Thread มาใช้ข้อมูลที่อยู่ใน memory ตัวเดียวกันนั่นเอง เมื่อเป็นแบบนี้แล้ว ฟังก์ชั่นที่จะทำงานต่อไปก็คือฟังก์ชั่น ThreadProc .... ให้เขียนโค้ดลงไปในฟังก์ชั่นนี้ดังนี้
UINT ThreadProc(LPVOID pParam)
{
JOB *jj=(JOB*)pParam;
for (int i=0;i<1000;i++)
{
jj->total+=jj->number[i];
}
PostMessage( jj->mainwindow , WM_THREADCOMPLETE,0,0);
return 0;
}
ในฟังก์ชั่นนี้ ดูให้ดีนะครับ จะเห็นว่าเราได้ประกาศ *jj ขึ้นมา และให้ jj นี้รับค่ามาจาก pParam ซึ่ง pParam นี้ล่ะ คือ &myjob ที่ส่งมาจาก AfxBeginThread นั่นเอง ตอนส่ง เราส่งมาเป็น &myjob ดังนั้น ตอนรับ เราจึงต้องเอา pointer มารับไงครับ......... และยังต้องรับโดยใช้บล็อกโครงสร้างเดียวกันด้วย เพราะ myjob ที่ส่งมานั้นเป็นโครงสร้าง JOB เวลารับ เราก็ต้องรับด้วยตัวแปรโครงสร้างที่เป็นแบบJOB เหมือนกัน
ตัวแปรโครงสร้าง jj นี้ ไม่ใช่ตัวแปรใหม่นะครับ แต่มันจะเข้าไปอยู่ที่แอดเดรสเดียวกันกับ mydata ให้ท่านพิจารณาตรงนี้ให้ดีๆ พอยเตอร์จะเข้าไปสิงตัวแปรบริเวณนั้นและสามารถประมวลผลในแอดเดรสตรงนั้นได้เลย ซึ่งก็คือสิ่งที่ jj กำลังทำอยู่นั่นเอง เมื่อ jj เข้าไปอยู่ในตำแหน่งเดียวกับ mydata แล้ว มันก็จะทำการรวมค่าที่อยู่ในอาเรย์แต่ละช่อง number[0]… number[999] และเก็บเอาไว้ใน total เมื่อเสร็จกระบวนการนี้แล้ว มันก็จะทำการ PostMessage กลับไปให้กับหน้าต่างหลักประมาณว่า “นี่ๆ ๆ ๆ งานเสร็จแล้ว” โดยการส่งเมสเสจWM_THREADCOMPLETE กลับไป โดยจะต้องระบุ Handle ของหน้าต่างหลักที่เป็นคนสั่งให้ Thread นี้ทำงาน นั่นก็คือ m_hWnd ซึ่งเราก็ได้ระบุลงไปแล้วไงครับ ในตัวแปร jj->mainwindow นั่นเอง ดังนั้น เราจึง PostMessage กลับไปได้ โดยเขียนโค้ดดังนี้
PostMessage( jj->mainwindow , WM_THREADCOMPLETE,0,0);
เจ้าตัวเลข 0,0 สองตัวข้างหลังนั้นก็คือ LPARAM กับ WPARAM ถ้าท่านต้องการส่งค่าอะไรกลับไปให้ทางนี้ด้วยก็ได้ครับ แต่ในกรณีนี้ เราต้องการเพียงแค่ “การสะกิด” ไปที่หน้าต่างหลักเท่านั้น การใช้ฟังก์ชั่น PostMessage สิ่งที่ต้องการก็คือ Handle ของหน้าต่างหลัก ฟังก์ชั่น PostMessage นี้เป็นฟังก์ชั่นของ Win32 API ที่ใช้ในการส่งเมสเสจ มีรูปแบบดังนี้
BOOL PostMessage(
HWND hWnd, // handle of destination window
UINT Msg, // message to post
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
ฟังก์ชั่นนี้จะต้องระบุว่าผู้รับเมสเสจ คือ หน้าต่างไหน เราก็บอกหน้าต่างที่เราเก็บเอาไว้ใน jj->mainwindow ไป ดังนั้น เมื่อเมสเสจนี้เกิดขึ้นที่โปรแกรมของเรา ฟังก์ชั่น OnThreadComplete ที่เราแมปเอาไว้ก็จะทำงาน เราก็จะเขียนโค้ดดักเอาไว้ดังนี้
void CThreadMFCDlg::OnThreadComplete(WPARAM wParam, LPARAM lParam)
{
m_sum=myjob.total;
UpdateData(FALSE);
TerminateThread(thread,0);
}
ดูนะครับ เมื่อฟังก์ชั่นนี้ถูกเรียกโดยแมสเสจ มันก็จะแสดงค่า myjob.total ออกมาที่ช่อง IDC_SUM นั่นเอง จากนั้นก็ปิดการทำงานของ Thread ไปเลย พอเรารันโปรแกรมก็จะได้ดังนี้
จากตัวอย่าง เรามาสรุปขั้นตอนกันนะครับ เมื่อกด OK แล้วจะเกิดอะไรขึ้น
1. เจ้า Thread จะถูกสร้าง
2. ฟังก์ชั่น ThreadProc ถูกเรียกให้ทำงาน โดยจะรับงานมาจาก mydata และทำการหาผลรวมของตัวเลข 1000 ตัว เก็บไว้ในตัวแปร mydata.total
3. เมื่องานเสร็จแล้ว มันก็จะส่งเมสเสจ WM_THEADCOMPLETE กลับไปให้หน้าต่างหลัก
4. ที่หน้าต่างหลัก มีการเขียนโค้ดดักเมสเสจ WM_THEADCOMPLETE นี้เอาไว้แล้ว ฟังก์ชั่น OnThreadComplete จึงถูกเรียก เพื่อแสดงค่าออกที่ไดอะล็อกและก็จบการทำงานของ Thread นั้นไป