1.多线程
1.基本知识
一个进程的多个线程通过不同轮流获得CPU的时间片运行,使得系统能够在多线程下运行
每个线程必须有一个进入点函数,主线程的入口函数是main
其他线程函数 DWORD WINAPI ThreadProc(LPVOID lpParam);
其中#define WINAPI __stdcall
stdcall 和cdecl区别:
1)采用__cdecl约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。
2)采用__stdcall约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n指令直接清理传递参数的堆栈。
如果没有显示的说明的话,一般的函数都是__cdecl
API说明:
#include <stdio.h> #include <windows.h> // 线程函数 DWORD WINAPI ThreadProc(LPVOID lpParam) { int i = 0; while(i < 20) { printf(" I am from a thread, count = %d \n", i++); } return 0; } int main(int argc, char* argv[]) { HANDLE hThread; DWORD dwThreadId; // 创建一个线程 hThread = ::CreateThread ( NULL, // 默认安全属性 NULL, // 默认堆栈大小 ThreadProc, // 线程入口地址(执行线程的函数) NULL, // 传给函数的参数 0, // 指定线程立即运行,如果设置成CREATE_SUSPEND,则这个线程不会立即执行,会等到ResumeThread的时候再执行 &dwThreadId); // 返回线程的ID号 printf(" Now another thread has been created. ID = %d \n", dwThreadId); /* ::WaitForSingleObject 继续执行的两个条件 1.句柄受信 2.时间到了 */ // 等待新线程运行结束INFINITE,只有在句柄受信的情况下,才会继续执行 ::WaitForSingleObject (hThread, INFINITE); ::CloseHandle (hThread); return 0; } /*::CreateThread 第一个参数详解 typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; //数据结构的大小 LPVOID lpSecurityDescriptor; BOOL bInheritHandle; //句柄是否可以被继承 }SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES; */
2.什么是线程内核对象?
基本成员
1.CONTEXT
线程上下文是保存线程拥有的寄存器
2.使用计数器
CreateThread会使计数器为1,赋值个hThread,相当于又打开了一次此时,计数器为2,
线程结束的时候会将减1,然后closehandle变为0.此时系统才会释放内核对象
还有函数OpenThread也会加1,有些函数虽然也能获得句柄但是是伪句柄,计数器不会加1,所以对这些伪句柄close的话,只会返回false,不会减1
这样的函数有GetCurrentProcess,GetCurrentThread
3.暂停次数
CreateThread的时候先创建线程内核对象,然后暂停计数器变为1,接着初始化内核对象,最后判断CreateThread的参数是否指定CREATE_SUSPEND,如果不是则计数器变为0.
这样这个线程就可以调度,然后CPU每隔20ms左右会扫描可调度的线程,然后按照优先级分配cpu时间片
4.退出码
退出码就是线程函数的返回值,如果还在运行期间,这个返回码的值是STILL_ACTIVE.
5.是否受信
在线程运行期间,受信状态一直是false,结束的时候将受信状态变为哦true,会使得等待在这个受信状态的线程,继续
3.线程的优先级
#include <stdio.h> #include <windows.h> DWORD WINAPI ThreadIdle(LPVOID lpParam) { int i = 0; while(i++<10) printf("Idle Thread is running \n"); return 0; } DWORD WINAPI ThreadNormal(LPVOID lpParam) { int i = 0; while(i++<10) printf(" Normal Thread is running \n"); return 0; } int main(int argc, char* argv[]) { DWORD dwThreadID; HANDLE h[2]; // 创建一个优先级为Idle的线程 h[0] = ::CreateThread(NULL, 0, ThreadIdle, NULL, CREATE_SUSPENDED, &dwThreadID); ::SetThreadPriority(h[0], THREAD_PRIORITY_IDLE); ::ResumeThread(h[0]); // 创建一个优先级为Normal的线程 h[1] = ::CreateThread(NULL, 0, ThreadNormal, NULL, 0, &dwThreadID); // 等待两个线程内核对象都变成受信状态 ::WaitForMultipleObjects( 2, // DWORD nCount 要等待的内核对象的数量 h, // CONST HANDLE *lpHandles 句柄数组 TRUE, // BOOL bWaitAll 指定是否等待所有内核对象变成受信状态 INFINITE); // DWORD dwMilliseconds 要等待的时间 ::CloseHandle(h[0]); ::CloseHandle(h[1]); return 0; } /* HANDLE h[2]; h[0] = hThread1; h[1] = hThread2; DWORD dw = ::WaitForMultipleObjects(2, h, FALSE, 5000); switch(dw) { case WAIT_FAILED: // 调用WaitForMultipleObjects函数失败(句柄无效?) break; case WAIT_TIMEOUT: // 在5秒内没有一个内核对象受信 break; case WAIT_OBJECT_0 + 0: // 句柄h[0]对应的内核对象受信 break; case WAIT_OBJECT_0 + 1: // 句柄h[1]对应的内核对象受信 break; } */
4.线程同步1.使用临界区对象
a)定义一个全局的临界区对象 CRITICAL_SECTION g_cs;
b)初始化临界区对象::InitializeCriticalSection(&g_cs);
c)进入临界区::EnterCriticalSection(&g_cs);
d)离开临界区::LeaveCriticalSection(&g_cs);
e)销毁临界区::DeleteCriticalSection(&g_cs);
2.互锁函数
一些线程共享变量需要一些简单的++.或者--,可以通过互锁函数代替.这样既可以保证是同步,可以实现++或者--
::InterlockedIncrement((long*)&g_nCount1);实现对g_nCount1 + 1
3.事件内核对象
#include <stdio.h> #include <windows.h> #include <process.h> HANDLE g_hEvent; UINT __stdcall ChildFunc(LPVOID); int main(int argc, char* argv[]) { HANDLE hChildThread; UINT uId; // 创建一个自动重置的(auto-reset events),未受信的(nonsignaled)事件内核对象 /*::CreateEvent 第一个参数:安全属性 第二个参数:是否手动重置,手动:当所有等待在这个event上的线程都运行.自动: 仅允许一个线程可调度,并且自动重置为未受信 第三个参数:初始受信状态 第四个参数:对象名称 */ g_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL); hChildThread = (HANDLE)::_beginthreadex(NULL, 0, ChildFunc, NULL, 0, &uId); // 通知子线程开始工作 printf("Please input a char to tell the Child Thread to work: \n"); getchar(); //::ResetEvent设为未受信 //设置受信 ::SetEvent(g_hEvent); // 等待子线程完成工作,释放资源 ::WaitForSingleObject(hChildThread, INFINITE); printf("All the work has been finished. \n"); ::CloseHandle(hChildThread); ::CloseHandle(g_hEvent); return 0; } UINT __stdcall ChildFunc(LPVOID) { //等待g_hEvent受信 ::WaitForSingleObject(g_hEvent, INFINITE); printf(" Child thread is working...... \n"); ::Sleep(5*1000); // 暂停5秒,模拟真正的工作 return 0; }
4.信号量内核对象允许多个线程同时访问同一个共享变量,但是可以设置同时允许访问的线程个数
HANDLE WINAPI CreateSemaphore( __in LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, __in LONG lInitialCount, __in LONG lMaximumCount, __in LPCTSTR lpName );
第二个参数是当前可进入的线程数
第三个参数是总共有的资源
WaitForSingleObject 进入共享资源等待信号量
进入后资可用资源数减1
结束时候
BOOL WINAPI ReleaseSemaphore( __in HANDLE hSemaphore, __in LONG lReleaseCount, __out LPLONG lpPreviousCount );使得可用计数器加1
5.互斥内核对象
用法同信号量,但是只是运行一个线程
HANDLE WINAPI CreateMutex( __in LPSECURITY_ATTRIBUTES lpMutexAttributes, __in BOOL bInitialOwner, __in LPCTSTR lpName );
BOOL WINAPI ReleaseMutex( __in HANDLE hMutex );
5.线程局部存储
如果多个线程的入口函数相同,每个线程对于同一个变量应该有不同的值,所以就需要线程局部存储这个概念
#include <stdio.h> #include <windows.h> #include <process.h> // 利用TLS记录线程的运行时间 DWORD g_tlsUsedTime; void InitStartTime(); DWORD GetUsedTime(); UINT __stdcall ThreadFunc(LPVOID) { int i; // 初始化开始时间 InitStartTime(); // 模拟长时间工作 i = 10000*10000; while(i--) { } // 打印出本线程运行的时间 printf(" This thread is coming to end. Thread ID: %-5d, Used Time: %d \n", ::GetCurrentThreadId(), GetUsedTime()); return 0; } int main(int argc, char* argv[]) { UINT uId; int i; HANDLE h[10]; // 通过在进程位数组中申请一个索引,初始化线程运行时间记录系统 g_tlsUsedTime = ::TlsAlloc(); // 令十个线程同时运行,并等待它们各自的输出结果 for(i=0; i<10; i++) { h[i] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId); } for(i=0; i<10; i++) { ::WaitForSingleObject(h[i], INFINITE); ::CloseHandle(h[i]); } // 通过释放线程局部存储索引,释放时间记录系统占用的资源 ::TlsFree(g_tlsUsedTime); return 0; } // 初始化线程的开始时间 void InitStartTime() { // 获得当前时间,将线程的创建时间与线程对象相关联 DWORD dwStart = ::GetTickCount(); ::TlsSetValue(g_tlsUsedTime, (LPVOID)dwStart); } // 取得一个线程已经运行的时间 DWORD GetUsedTime() { // 获得当前时间,返回当前时间和线程创建时间的差值 DWORD dwElapsed = ::GetTickCount(); dwElapsed = dwElapsed - (DWORD)::TlsGetValue(g_tlsUsedTime); return dwElapsed; }
实际开发中,会用这个方法保存指针然后指针分别指向各个内存
TLS实现原理:
线程局部位数组标志这个每个线程对应的index是否已经被分配,如果没有分配发挥index,也就是代码中的g_tlsUsedTime,
比如:线程2要存储变量,位数组中index2为空闲,(表明所有线程的index都未空闲)g_tlsUsedTime为index2,然后在线程中讲值存入index2,不同线程存入不同的数组内的index2