本博文由CSDN博主zuishikonghuan所作,版权归zuishikonghuan所有,转载请注明出处:http://blog.csdn.net/zuishikonghuan/article/details/48208357
多线程:一个进程创建时,默认情况下系统会为它创建一个主线程,(如果使用Native API创建的线程就没有主线程,是空的,必须自己创建主线程),应用程序可以自己创建线程,还有以前写过的一篇“DLL注入技术”,就是远程在其他进程中创建线程,然后让远程线程load我们的dll。
系统是如何实现多线程的?其实,对于单CPU单核心的设备上,在一个确定的时刻,只能执行内存中的一个指令。所谓的“多任务抢占式操作系统”,其实是将CPU划分了“时间段”,并分配给每一个线程,系统的任务调度程序会根据时间段切换线程上下文和进程上下文(比如线程的寄存器和状态等等信息就存储在上下文中),这个时间段不能太短,否则一个线程还没干什么事呢就切换走了,浪费效率,更不能太长,否则用户就感觉程序不是同时运行的。
创建一个线程,标准的Win32 API是CreateThread。
CreateThread函数:
HANDLE WINAPI CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
第1个参数:线程内核对象的安全属性,一般置NULL,使用默认设置。
第2个参数:线程栈空间大小。0表示使用默认大小。
第3个参数:新线程所执行的线程函数地址。
线程函数原型:
DWORD WINAPI ThreadProc( _In_ LPVOID lpParameter );
lpParameter:通过CreateThread传人的第四个参数。
第4个参数:传给线程函数的参数。
第5个参数:为0表示线程创建之后直接运行,CREATE_SUSPENDED表示线程创建后暂停运行,通过调用ResumeThread事线程运行。
第6个参数:返回线程的ID。
返回值:成功返回新线程的句柄,失败返回NULL。
特别说明:CreateThread后,如果不需要操作线程,可以直接CloseHandle掉这个线程句柄,关闭这个线程句柄不会影响线程运行。
关于_beginthreadex函数:
另外还有一个_beginthreadex函数,也是用来创建线程的
很多资料都一再强调“应该使用_beginthreadex函数,不要使用CreateThread函数”,这样说其实是有道理的。
MSDN上说:
A thread in an executable that calls the C run-time library (CRT) should use the _beginthreadex and _endthreadex functions for thread management rather than CreateThread and ExitThread; this requires the use of the multithreaded version of the CRT. If a thread created using CreateThread calls the CRT, the CRT may terminate the process in low-memory conditions.
因为_beginthreadex设计出来的目的就是为了使C/C++的运行时函数支持多线程的,它先构建了一个C/C++的运行时函数的环境,之后从内部调用了CreateThread。因此如果不需要使用C/C++的运行时函数,那么,建议使用CreateThread而不要使用_beginthreadex!因为_beginthreadex会造成CPU和内存资源浪费,如果不需要使用C/C++的运行时函数,在对程序效率要求很高的条件下,应该使用CreateThread。
其实,绝大多数C/C++的运行时函数都是调用了系统的API!
关于关闭一个线程:
在用户模式下(比如在Win32子系统下)关闭线程的正确做法是让线程自己返回,强制结束线程是不可取的。
在内核模式下(驱动程序),还需要做一些其他工作。
线程同步:
当我们创建了多个线程的时候,如果线程访问同一个资源,结果会如何?
假设我们一个全局变量x,创建了两个线程a,b。a和b都要读写x,结果就无法得知了,为何?因为系统会随时调度线程,而又有两个很难克服的原因:
1。对变量的操作,并非是直接访问内存,而是先把内存单元读入寄存器,修改寄存器,然后把寄存器的数据写回内存。
如果线程a刚把x读入寄存器,这时候系统把CPU调度到b上了,b改完x,回到a,a无法知道内存已经变了,于是把寄存器改了之后写入内存,这样就造成了线程b做了无用功!以后程序也可能会出现问题。
不信可以反汇编你的程序看看汇编代码。。
2。CPU读写内存不是直接读写的,其实,CPU为了提高连续读写内存的效率,引入了一个“高速缓存行”,是将一段内存读到缓存行中再写回内存,因此如果线程调度刚调度到读入缓存行时切换走线程上下文的话和上面的问题一样。
所以我们需要进行“线程同步”,实现原子访问。
用户模式下常见的线程同步的方法有:事件(Event)、互斥体(Mutex)、信号量(Semaphore)等
其中最常用的是互斥体
关于互斥体:当一个线程获取了互斥体,那么其他线程就不能获取,一个互斥体只能同时被一个线程获得,而其他试图获取互斥体的线程将会等待互斥体的释放,释放后,再有一个线程获取互斥体。
这样,我们就可以在线程访问同一个资源时获取互斥体,访问完了释放,就可以避免上面的问题了,这就是线程同步。
创建互斥体 CreateMutex:
HANDLE WINAPI CreateMutex(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner,
_In_opt_ LPCTSTR lpName
);
参数1:安全属性,NULL表示默认
参数2:是否被占有。
参数3:命名
返回值:成功返回互斥体的句柄
释放互斥体 ReleaseMutex:
BOOL WINAPI ReleaseMutex(
_In_ HANDLE hMutex
);
参数:互斥体句柄
得到互斥体 WaitForSingleObject:
DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
参数1:要等待对象的句柄(要获取的互斥体的句柄)
参数2:超时间隔,以毫秒为单位。如果指定一个非零值,则该函数等待,直到该对象处于终止状态或到达时间间隔。INFINITE表示一直等待到对象处于终止状态。
等待线程完成:使用WaitForSingleObject等待线程句柄即可。
例子:
有必要说一句,这里因为只有两个线程,每个线程只操作一次,效果不明显,你可以用for循环,让两个个线程分别输出不同的内容很多次,那么加不加线程同步效果就很明显了。
#include
#include
int x = 0;
DWORD WINAPI ThreadProc(LPVOID lpParameter){
//获取主线程传来的互斥体句柄
HANDLE* pMutex = (HANDLE*)lpParameter;
//获取互斥体,如果被其他线程获取了就一直等待下去
WaitForSingleObject(*pMutex, INFINITE);
//这些操作可视为原子访问
x++;
//释放互斥体
ReleaseMutex(*pMutex);
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
//创建互斥体
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("MyMutex1"));
//创建线程
HANDLE t1 = CreateThread(NULL, 0, ThreadProc, &hMutex, 0, NULL);
HANDLE t2 = CreateThread(NULL, 0, ThreadProc, &hMutex, 0, NULL);
//等待线程退出
WaitForSingleObject(t1, INFINITE);
WaitForSingleObject(t2, INFINITE);
//关闭句柄,释放资源
CloseHandle(hMutex);
printf("%d", x);
getchar();
return 0;
}