线程基础
windows线程是可以执行的代码的实例。系统是以线程为单位调度程序 。一个程序当中可以有多个线程,实现多任务的处理。
windows线程的特点:
每个线程都具有一个ID
每个线程都具有自己的内存栈(其余内存空间都是共享的)
同一进程中的线程使用同一个地址空间
线程的调度
操作系统将CPU的执行时间划分成时间片,依次根据时间片执行不同的线程。
线程轮询:线程A->线程B-> 线程A。。。。
创建一个线程:
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, // 线程的创建方式 1) 0 立即执行方式,创建后立即执行 2) CREATE_SUSPEND 挂起方式,创建后先处于休眠,唤醒后继续执行
_Out_opt_ LPDWORD lpThreadId // 创建成功,返回线程的ID
);
定义线程处理函数
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(
LPVOID lpThreadParameter
);
线程挂起
DWORD SuspendThread(HANDLE hThread) // handle to thread
唤醒线程
DWORD ResumeThread( HANDLE hThread );
结束指定线程(线程外结束)
BOOL TerminateThread(
HANDLE hThread,
DWORD dwExitCode
);
结束函数所在的线程(线程内结束)
VOID ExitThread( DWORD dwExitCode)
线程相关操作
// 获取当前线程的ID
GetCurrentThreadId()
// 获取当前线程的句柄
GetCurrentThread()
// 等候单个句柄有信号(很重要)
// 必须是可等候句柄
VOID WaitForSingleObject(
HANDLE handle, // 句柄BUFF的地址
DWORD dwMilliseconds // 等候时间ms级别 INFINITE无限时间
)
// 等候多个句柄有信号
DWORD WaitForMultipleObjects(
DWORD nCount, // 句柄数量
CONST HANDLE *lpHandles, // 句柄的地址
BOOL bWaitAll, // 等候方式 是否等待全部有信号还是非等待全部有信号就结束等候
DWORD dwMilliseconds // 等候时间 INFINITE 永无超时
)
线程同步
原因:操作临界资源,多个线程同时操作。
原子锁
相关问题:多个线程对同一个数据进行原子操作,会产生结果丢失。
比如执行++运算时。
错误代码分析:当线程A执行g_value++时,如果线程切换时间正好是在线程A将值保存到g_value之前,线程B继续执行g_value++,那么当线程A再次被切换回来之后,会将原来线程A保存的值保存到g_value上,线程B进行的加法操作被覆盖。
使用原子锁函数,这里注意,性能会牺牲。
InterlockedIncrement
InterlockedDecrement
InterlockedCompareExchange
InterlockedExchange
....
原子锁的实现:直接对数据所在的内存操作,并且在任何一个瞬间只能有一个线程访问,也就是说直接操作内存,不涉及寄存器。
1) 尝试对数据所在内存进行加锁(独占锁)
2) 加锁成功后操作数据, 加锁失败会堵塞,这也是为什么执行速度慢一些,但是会保证结果的正确性!
示例代码:
#include <iostream>
#include <Windows.h>
long g_value = 0;
DWORD WINAPI PTHREAD_START_ROUTINE_Impl(LPVOID lpThreadParameter)
{
for (int i = 0; i < 100000; i++)
{
InterlockedIncrement(&g_value);
// g_value++;
}
return 0;
}
DWORD WINAPI PTHREAD_START_ROUTINE_Impl2(LPVOID lpThreadParameter)
{
for (int i = 0; i < 100000; i++)
{
InterlockedIncrement(&g_value);
// g_value++;
}
return 0;
}
int main()
{
LPDWORD threadid1 = 0, threadid2 = 0;
HANDLE handles[2];
handles[0] = CreateThread(NULL, 1024, PTHREAD_START_ROUTINE_Impl, 0, 0, threadid1);
handles[1] = CreateThread(NULL, 1024, PTHREAD_START_ROUTINE_Impl2, 0, 0, threadid2);
// 需要保证主线程不退出
WaitForMultipleObjects(2, handles, true, INFINITE);
printf("wait over! %d\n", g_value);
getchar();
return 0;
}
原子锁的难点:
缺点:API函数太多。局限性大,只针对操作符号。
优点:效率相对最高
互斥:多线程下代码或资源的共享使用(临界资源)
特性:1. 任何一个时间点只能有一个线程拥有互斥
2. 任何线程都没有互斥,则有信号,否则无信号
3. 谁先等互斥谁就有拥有互斥
互斥的使用
示例代码:
#include <iostream>
#include <Windows.h>
long g_value = 0;
// 定义一个互斥句柄,可等候句柄
HANDLE mutexHandle = NULL;
HANDLE threads[2];
DWORD CALLBACK PTHREAD_START_ROUTINE_Impl(LPVOID lpThreadParameter)
{
for (int i = 0; i < 100000; i++)
{
// 加独占锁
WaitForSingleObject(mutexHandle, INFINITE);
g_value++;
ReleaseMutex(mutexHandle);
}
return 0;
}
DWORD CALLBACK PTHREAD_START_ROUTINE_Imp2(LPVOID lpThreadParameter)
{
for (int i = 0; i < 100000; i++)
{
// 加独占锁
WaitForSingleObject(mutexHandle, INFINITE);
g_value++;
ReleaseMutex(mutexHandle);
}
return 0;
}
int main()
{
// 创建互斥
// 一般在主线程进行创建
// 主线程不能互斥
mutexHandle = CreateMutex(NULL, false, "testMutex");
LPDWORD id1 = 0, id2 = 0;
threads[0] = CreateThread(0, 1024, PTHREAD_START_ROUTINE_Impl, 0, 0, id1);
threads[1] = CreateThread(0, 1024, PTHREAD_START_ROUTINE_Imp2, 0, 0, id2);
WaitForMultipleObjects(2, threads, true, INFINITE);
printf("END! %d \n" , g_value);
CloseHandle(mutexHandle);
getchar();
return 0;
}
示例2:
#include <iostream>
#include <Windows.h>
long g_value = 0;
// 定义一个互斥句柄,可等候句柄
HANDLE mutexHandle = NULL;
HANDLE threads[2];
DWORD CALLBACK PTHREAD_START_ROUTINE_Impl(LPVOID lpThreadParameter)
{
while (1)
{
// 加独占锁
WaitForSingleObject(mutexHandle, INFINITE);
for (int i = 0; i < 9; ++i)
{
printf("*");
Sleep(100);
}
printf("\n");
ReleaseMutex(mutexHandle);
}
return 0;
}
DWORD CALLBACK PTHREAD_START_ROUTINE_Imp2(LPVOID lpThreadParameter)
{
while (1)
{
// 加独占锁
WaitForSingleObject(mutexHandle, INFINITE);
for (int i = 0; i < 9; ++i)
{
printf("-");
Sleep(100);
}
printf("\n");
ReleaseMutex(mutexHandle);
}
return 0;
}
int main()
{
// 创建互斥
// 一般在主线程进行创建
// 主线程不能互斥
mutexHandle = CreateMutex(NULL, false, "testMutex");
LPDWORD id1 = 0, id2 = 0;
threads[0] = CreateThread(0, 1024, PTHREAD_START_ROUTINE_Impl, 0, 0, id1);
threads[1] = CreateThread(0, 1024, PTHREAD_START_ROUTINE_Imp2, 0, 0, id2);
WaitForMultipleObjects(2, threads, true, INFINITE);
printf("END! %d \n" , g_value);
CloseHandle(mutexHandle);
getchar();
return 0;
}
事件
相关问题:线程之间的通知的问题
创建线程
HANDLE
WINAPI
CreateEventA(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性 设置为0即可
_In_ BOOL bManualReset, // 手工重置
_In_ BOOL bInitialState, // 初始创建 true有信号
_In_opt_ LPCSTR lpName \\ 事件名
);
事件的其它操作
信号量
类似于事件,解决通知的相关问题。但提供一个计数器,可以设置次数。
创建信号量
HANDLE
WINAPI
CreateSemaphoreA(
_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性
_In_ LONG lInitialCount, // 初始化值,每次等待只要大于0就有信号,通过一次就减一
_In_ LONG lMaximumCount, // 最大值
_In_opt_ LPCSTR lpName // 信号量名称
);
信号量其它操作
对比以上,原子锁、互斥、事件、信号量如何选择呢?
原子锁和互斥主要解决临界资源问题,互斥应用更广,但是效率稍微低。
事件和信号量都解决线程同步问题,但是信号量具有计数功能,可以对资源数量进行控制,比如限制http服务器的访问客户数量。