侯捷老师分别从两个方面介绍线程:①从操作系统层面看线程。②从程序设计角度看线程。
下面我们分别介绍:
一、从操作系统层面看线程
1.我们可以用Win32的CreateThread()函数创建一个线程。CreateThread()函数的有6个参数介绍如下:
CreateThread()函数在processthreadapi.h的原型如下:
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ __drv_aliasesMem LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
参数1:是线程的安全防护属性,默认为NULL。参数2:线程堆栈的大小。参数3:线程函数的地址(执行事实)。参数4:传递给线程函数的参数,用户可自定义。参数5:线程创建的标记。默认为0,表示线程立即执行。也可以是CREATE_SUSPENED,表示挂起(暂停运行),后面使用ResumeThread()可恢复运行。参数6:线程的ID号。关于该函数的详细说明可以查看微软官方文档CreateThread的说明
2.接下来,提出了几个问题:
①操作系统如何造成这种多任务并行的现象?
答:
多个CPU情况:
如果在多个CPU的情况下,并且当前的操作系统要支持多任务才行(比如Windows NT) ,那么一个CPU就可以分到一个线程,多个线程就可以真正的同时执行,真正的做到多任务。这种操作系统特性成为Symmetric Mulitprocessing( SMP)。
一个CPU情况:
多个线程“同时执行”的幻觉是靠调度器来完成的。它在不同的线程之间来回切换。以Windows 95 和Windows NT 而言,一般情况下每一个线程的时间片一般都是20ms。
②线程对于操作系统的意义是什么?
多线程程序可以提高程序的执行效率。相比较于创建多个进程,在一个进程创建多个线程的速度更快,占用的资源更少。
③系统如何维护多个线程?
答:操作系统以一个数据结构Thread Database(TDB)来记录每一个线程的所有数据。包括线程的局部存储空间Thread Local Storage(TLS),消息队列,handle表格,地址空间,Memory Context等等。
④线程与其父亲大人(进程)的关系是如何维持?
对于这个问题,首先介绍3个概念:模块,线程,进程。
模块:一个DLL是一个模块,一个exe是一个模块。具体定义:一段可执行的程序,包括exe和DLL。其程序代码,数据,资源被加载到内存中,由系统建置一个数据结构来管理它,就是一个模块。
进程:进程强调的是拥有权(ownership)的集合。进程拥有地址空间由(Memory Context 决定),动态配置而来的内存,文件,线程等一系列模块。操作系统使用一个叫做Process Database(PDB)来维护一个进程。
线程:线程强调的是“执行事实”。
答:一开始进程是以一个主线程(Primary Thread)开始的,进程可以创建多个线程,让CPU在不同的时间内执行不同的代码。
⑤CPU只有一个,线程有多个如何摆平优先级与调度问题?
调度器切换线程的是通过线程的优先级来进行调度的。线程的优先级越高,被调度的几率越高,不是说优先级最低的线程永远不会被调用,优先级强调的是被调用的几率。为了避免优先级低的线程永远不会调用的情况发生,操作系统会动态调整线程的优先级,使优先级较低的线程获得被调用的几率增大。如果某个线程会获取消息,但是使用GetMessage()获取消息一直为空,即使他的优先级很高那么他也会被挂起。
在进程方面补充一幅图:
3.Thread ConText(线程上下文):什么是线程上下文,侯捷老师给出的解释是一组缓存器值(包括指令IP)。原因是由于线程经常被暂停执行,会把CPU资源让出来。因此需要记录当前线程的状态,以便在后面恢复。
二、从程序设计角度看线程
1.首先介绍与线程有关的Win32API:
// AttachThreadInput:将某个线程的输入导向另一个线程
BOOL AttachThreadInput([in] DWORD idAttach,[in] DWORD idAttachTo,[in] BOOL fAttach);
// CreateThread:产生一个线程
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ __drv_aliasesMem LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
// ExitThread:结束一个线程
WINBASEAPI
DECLSPEC_NORETURN
VOID
WINAPI
ExitThread(
_In_ DWORD dwExitCode
);
// GetCurrentThread:取得当前线程的Handle
WINBASEAPI
HANDLE
WINAPI
GetCurrentThread(
VOID
);
// GetCurrentThreadId:取得当前线程的Id
WINBASEAPI
DWORD
WINAPI
GetCurrentThreadId(
VOID
);
// GetExitCodeThread:取得某以线程的结束代码(可用以决定线程是否已结束)
WINBASEAPI
_Success_(return != 0)
BOOL
WINAPI
GetExitCodeThread(
_In_ HANDLE hThread,
_Out_ LPDWORD lpExitCode
);
// GetPriorityClass:取得某一进程的优先级等级
WINBASEAPI
DWORD
WINAPI
GetPriorityClass(
_In_ HANDLE hProcess
);
// GetQueueStatus: 传回某以一线程的消息队列状态
WINUSERAPI
DWORD
WINAPI
GetQueueStatus(
_In_ UINT flags);
// GetThreadContext: 获取某一线程的context
WINBASEAPI
BOOL
WINAPI
GetThreadContext(
_In_ HANDLE hThread,
_Inout_ LPCONTEXT lpContext
);
// GetThreadDesktop:获取某一线程的Desktop对象
WINUSERAPI
HDESK
WINAPI
GetThreadDesktop(
_In_ DWORD dwThreadId);
// GetThreadPriority:获取某一线程的优先级
WINBASEAPI
int
WINAPI
GetThreadPriority(
_In_ HANDLE hThread
);
// GetThreadSelectorEntry:调试器专用,传回指定之线程的某个
// selector 的LDT记录项
WINBASEAPI
BOOL
WINAPI
GetThreadSelectorEntry(
_In_ HANDLE hThread,
_In_ DWORD dwSelector,
_Out_ LPLDT_ENTRY lpSelectorEntry
);
// ResumeThread:将某个冻结的线程恢复运行
WINBASEAPI
DWORD
WINAPI
ResumeThread(
_In_ HANDLE hThread
);
// SetPriorityClass:设定优先级等级
WINBASEAPI
BOOL
WINAPI
SetPriorityClass(
_In_ HANDLE hProcess,
_In_ DWORD dwPriorityClass
);
// SetThreadPriority:设定线程优先级
WINBASEAPI
BOOL
WINAPI
SetThreadPriority(
_In_ HANDLE hThread,
_In_ int nPriority
);
// Sleep:将某个线程暂时冻结,其他线程将获得执行权
WINBASEAPI
VOID
WINAPI
Sleep(
_In_ DWORD dwMilliseconds
);
// SuspendThread:冻结(挂起)某个线程
WINBASEAPI
DWORD
WINAPI
SuspendThread(
_In_ HANDLE hThread
);
// TerminateThread:结束某个线程
WINBASEAPI
BOOL
WINAPI
TerminateThread(
_In_ HANDLE hThread,
_In_ DWORD dwExitCode
);
// TlsAlloc:配置一个TLS(Thread Local Storage)
WINBASEAPI
DWORD
WINAPI
TlsAlloc(
VOID
);
// TlsFree:释放一个TLS
WINBASEAPI
BOOL
WINAPI
TlsFree(
_In_ DWORD dwTlsIndex
);
// TlsGetValue:取得某个TLS的内容
WINBASEAPI
LPVOID
WINAPI
TlsGetValue(
_In_ DWORD dwTlsIndex
);
// TlsSetValue:设定某个TLS的内容
WINBASEAPI
BOOL
WINAPI
TlsSetValue(
_In_ DWORD dwTlsIndex,
_In_opt_ LPVOID lpTlsValue
);
// WaitForInputIdle:等待,直到不再有消息进入到某个线程中
WINUSERAPI
DWORD
WINAPI
WaitForInputIdle(
_In_ HANDLE hProcess,
_In_ DWORD dwMilliseconds);
2.线程在MFC框架中又分为Worker Thread 和UI Thread的介绍
Worker Thread (工作线程):与使用者界面无关。使用CreateThread()函数创建的线程是一个工作线程。但是如果在往该线程中输入了消息,那么该线程就变成UI线程了。
UI Thread(界面线程):与使用者界面有关。如果线程代码中带有一个消息循环,那么该线程就是一个UI Thread。
注意:在创建线程时,把所有的UI(User Interface) 操作都放在主线程中,其他的各种复杂的运算都放在Worker Thread。
3.探索CWinThread
就像CWinApp代表一个程序本身一样,CWinThread 也代表着一个线程本身。我个人的理解是,CWinApp继承于CWinThread,那么程序本身就有一个主线程来维护了。
注意:一个程序只能有一个CWinApp对象,即theApp。但是并不代表一个程序只能有一个CWinThread对象。每当我们需要一个额外的线程时,推荐做法是:我们应该自定义继承自CWinThread的类,然后由该类的对象调用成员函数CreateThread()或者全局函数AfxBeginThread()产生一个线程。当然也可以直接使用::CreateThread()和_beginthreadex()函数产生一个线程。但是为什么推荐使用CWinThread对象呢,因为在CWinThread的成员函数CreateThread()内部完成了一些初始化。多说一句:实际上在CreateThread()函数和AfxBeginThread()函数内部都调用了_beginthreadex()函数。
4.Worker Thread和UI Thread产生的步骤:
①Worker Thread:不涉及操作界面。我们使用AfxBeginThread()函数来创建线程。AfxBeginThread()函数的声明如下:
我们使用第一种方式来创建Worker Thread线程。
CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0,
DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);
CWinThread* AFXAPI AfxBeginThread(CRuntimeClass* pThreadClass,
int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0,
DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);
由 AFX_THREADPROC 函数指针 定义线程入口函数的形式:
UINT ThreadFunc(LPVOID LParam);
②UI Thread:UI线程可不能关由线程函数来表示,因为他要处理消息,需要消息循环。我们应该先自定义继承自CWinThread的类。然后再由AfxBeginThread()函数,上面函数重载。直接使用类向导产生MFC类如下:
CMyThread.h
// CMyThread
class CMyThread : public CWinThread
{
DECLARE_DYNCREATE(CMyThread)
protected:
CMyThread(); // 动态创建所使用的受保护的构造函数
virtual ~CMyThread();
public:
virtual BOOL InitInstance();
virtual int ExitInstance();
protected:
DECLARE_MESSAGE_MAP()
};
CMyThread.cpp
// CMyThread
IMPLEMENT_DYNCREATE(CMyThread, CWinThread)
CMyThread::CMyThread()
{
}
CMyThread::~CMyThread()
{
}
BOOL CMyThread::InitInstance()
{
// TODO: 在此执行任意逐线程初始化
return TRUE;
}
int CMyThread::ExitInstance()
{
// TODO: 在此执行任意逐线程清理
return CWinThread::ExitInstance();
}
BEGIN_MESSAGE_MAP(CMyThread, CWinThread)
END_MESSAGE_MAP()
// CMyThread 消息处理程序
// 使用AfxBeginThread函数重载产生CWinThread 对象
CWinThread* pThread = AfxBeginThread(RUNTIME_CLASS(CMyThread));
5.线程的结束
Worker Thread:①既然线程函数代表的是线程,执行到return语句即代表线程结束。
②调用AfxEndThread()函数也可以结束线程。
UI Thread:①因为有消息循环的关系,只有获取到WM_QUIT消息才能推出消息循环。我们可以使用::PostQuitMessage()函数发送WM_QUIT。
②在线程函数中调用AfxBeginThread()函数也可以退出线程。
其实AfxEndBeginThread()函数也是对_endThreadex()函数的封装。
6.线程与同步控制
讲了这么多,创建几个线程是很简单的事情,难的是实现线程之间的同步。多个线程之间的运行次序是未知的,就会产生所谓的race condition(资源竞争)。侯捷在书中讲了一个例子。
假设有两个线程A和线程B,线程A负责读取全局变量X的值,线程B负责写入X的值。如果B写完后,A再去读取。这样是对的,但是如果A先读取,B再写就不行了,X值是未知。
如下图所示:
要解决这些问题,必须协调各个线程的执行顺序。Windows中提供了4种办法。
1.Critical Section(临界区)。
2.Semaphore(信号量)
3.Event(事件对象)
4.Mutex(互斥量)
MFC也提供了4个对应的类
7.下面直接放上程本章的程序示例:
新建MFC多文档应用程序。mltithrd.cpp文件添加如下代码:
// 声明线程入口函数
UINT ThreadFunc(LPVOID ThreadArg);
// 声明5个UI线程
CWinThread* _pThread[5];
// 定义一个线程优先级的数组
DWORD _ThreadArg[5] = { HIGHEST_THREAD, // 0x00
ABOVE_AVE_THREAD, // 0x3F
NORMAL_THREAD, // 0x7F
BELOW_AVE_THREAD, // 0xBF
LOWEST_THREAD // 0xFF
};
在CMltithrdApp::InitInstance()函数中return 语句前添加如下代码。新建5个线程,5个线程使用同一个入口函数。并且 设置线程的优先级。
int i;
for (i= 0; i< 5; i++)
{
_pThread[i] = AfxBeginThread(ThreadFunc,
&_ThreadArg[i],
THREAD_PRIORITY_NORMAL,
0,
0,
NULL);
}
// setup relative priority of threads
_pThread[0]->SetThreadPriority(THREAD_PRIORITY_HIGHEST);
_pThread[1]->SetThreadPriority(THREAD_PRIORITY_ABOVE_NORMAL);
_pThread[2]->SetThreadPriority(THREAD_PRIORITY_NORMAL);
_pThread[3]->SetThreadPriority(THREAD_PRIORITY_BELOW_NORMAL);
_pThread[4]->SetThreadPriority(THREAD_PRIORITY_LOWEST);
最后定义线程入口函数:ThreadFunc()函数如下:
UINT ThreadFunc(LPVOID ThreadArg)
{
CMainFrame* pFrame = (CMainFrame*)AfxGetApp()->m_pMainWnd;
DWORD dwArg = *(DWORD*)ThreadArg;
CRect rect;
HDC hDC;
HANDLE hBrush, hOldBrush;
int nThreadNo;
char szBuf[80];
DWORD dwThreadHits = 0;
hDC = GetDC(pFrame->m_hWnd);
::GetClientRect(pFrame->m_hWnd,&rect);
hBrush = CreateSolidBrush(RGB(dwArg, dwArg, dwArg));
hOldBrush = ::SelectObject(hDC, hBrush);
switch (dwArg)
{
case HIGHEST_THREAD: nThreadNo = 0; break;
case ABOVE_AVE_THREAD: nThreadNo = 1; break;
case NORMAL_THREAD: nThreadNo = 2; break;
case BELOW_AVE_THREAD: nThreadNo = 3; break;
case LOWEST_THREAD: nThreadNo = 4; break;
}
wsprintf(szBuf, _T("T%d"), nThreadNo);
TextOut(hDC,dwArg, rect.bottom - 150,szBuf,lstrlen(szBuf));
wsprintf(szBuf, _T("P%d"), _ThreadArg[nThreadNo]);
TextOut(hDC, dwArg, rect.bottom - 130, szBuf, lstrlen(szBuf));
do
{
dwThreadHits++;
Rectangle(hDC, dwArg, rect.bottom - (dwThreadHits / 10),
dwArg + 0x40, rect.bottom);
Sleep(10);
} while (dwThreadHits < 1000);
hBrush = SelectObject(hDC, hOldBrush);
DeleteObject(hBrush);
ReleaseDC(pFrame->m_hWnd,hDC);
return 0;
}
这一章的代码示例,大家可以使用书中的代码示例。