多线程
多线程概念:一个程序中,程序把 "自己" 分成几个 "执行线程" 并行运行
在代码角度:一个程序首先执行主线程(WinMain),程序一旦执行,会进行一个系统调用(CreateThread),创建新的线程。Window 会在这些线程中作抢占式切换(就像在进程中切换一样)
线程与消息队列的关系:每个线程都可以创建或不创建消息队列,如果想在线程中创建窗口,则要创建消息队列。如果要做数据运算或图形理,则不用创建消息队列。因此,要学会把程序分成一个消息队列线程(该线程用于创建所有窗口并给他们发送消息)和一个或多个非消息队列线程(用于数据运算或后台任务)。
例如:
主线程创建所有的窗口,窗口过程,处理窗口的消息。其他线程负责简单的后台任务、数据运算,除了通过和主线程通信外,不和用户打道。
多线程需要注意的地方
由于一个程序里面的线程是以抢占式运行,线程共享同一个进程的变量和内存。当多个线程公用同一个数据时,Window 在一个线程更新并使用该数据前中断线程,启动另一个线程使用该数据,则这两个线程所 “看到” 的数据可能不一致,引起程序崩毁(Window 是在机器指令之间切换代码)
例如:
假设一个多线程的程序,线程A负责准备数据,线程B需要使用这些数据。如果线程B在线程A还没开始运行时就已经开始运行,则会出现常,因此,为了让线程之间协同运行,Window 提供各种方式如:
1、“信号灯”(Semaphore)让程序在代码的特定点暂停一个线程,直到另一个线程发信号让它继续(就像交通灯指挥交通一样),但 “信号灯”又会造成一种常见的问题: “死锁” ,即两个线程已经中断了彼此的运行,只有它们继续运行才能解开对方中断的代码(就像过独木桥,双方都在桥上两头,两个人都过不去)
2、“临界区”(Critical Section)指定该代码段不能被中断。临界区通过调用 EnterCriticalSection 和 LeaveCriticalSection 来定义,一个线程进入临界区,另一个线程就不能进入(必须等我过了,你才能通过),直到前一个线程调用 LeaveCriticalSection
注意事项:
尽量不要在线程中共享 GDI 对象(画笔、画刷、字体、位图。。。),这样有可能一个线程销毁 GDI 对象,另一个线程调用该对象,就算需要共享同一个 GDI 对象,也最好放在临界区
使用临界区
使用临街区共四个函数
1、定义一个类型为 CRITRICAL_SECTION cs 的全局变量
CRITRICAL_SECTION cs;
2、该变量在一个线程中进行如下初始化
InitializeCriticalSrction(&cs);(该变量不能被修改,移动)
3、变量被初始化后,在该线程中调用函数进入临界区
EnterCriticalSection(&cs);(调用成功后,该线程就进入临界区,如果下一个线程要进入同一个临界区,调用 EnterCriticalSection 进入同一个临界区时将被挂起)
4、多个线程进入同一个临界区时,第一个线程运行完后调用该函数,推出临界区,使 Window 执行下个线程
LeaveCriticalSection(&cs);(第一个线程调用该函数,离开临界区,下一个线程才能执行)
这里有两个线程函数分别是 ThreadPrco1 和 ThreadProc2,ThreadProc1 负责打印数字 1,ThreadProc2 负责打印数字 2,在不使用 “临界区” 的情况下,效果如下:
int flag = 1;
void ThreadProc1(void *p)
{
HDC hdc;
static int Lines = 0;
while(Lines < 20)
{
hdc = GetDC(hwnd);
TextOut(hdc, 10, Lines * ychar, TEXT("1"), 1);
Lines++;
ReleaseDC(hwnd, hdc);
}
_endthread();
}
void ThreadProc2(void *p)
{
HDC hdc;
static int iLines = 0;
while(iLines < 20)
{
hdc = GetDC(hwnd);
TextOut(hdc, 60, iLines * ychar, TEXT("2"), 1);
iLines++;
ReleaseDC(hwnd, hdc);
}
_endthread();
}
可以看出线程打印出来的数字基本是同时进行的
这里也是 ThreadProc1 打印数字 1,ThreadProc2 打印数字 2,但使用了 “临界区”
CRITICAL_SECTION cs;
int flag = 1;
void ThreadProc1(void *p)
{
HDC hdc;
static int Lines = 0;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
while(Lines < 20)
{
hdc = GetDC(hwnd);
TextOut(hdc, 10, Lines * ychar, TEXT("1"), 1);
Lines++;
ReleaseDC(hwnd, hdc);
}
LeaveCriticalSection(&cs);
_endthread();
}
void ThreadProc2(void *p)
{
HDC hdc;
static int iLines = 0;
EnterCriticalSection(&cs);
while(iLines < 20)
{
hdc = GetDC(hwnd);
TextOut(hdc, 60, iLines * ychar, TEXT("2"), 1);
iLines++;
ReleaseDC(hwnd, hdc);
}
_endthread();
}
这里是先运行 ThreadProc1 把所有的 1 打印出来,再运行 ThreadProc2 线程函数
关于临界区的一个问题:
当多个(多于一个)线程进入同一个临界区时,线程的执行顺序时怎样的?
创建多线程
事件对象
如果一个程序每次运行相同的任务都为该任务重新创建新的线程,那么将有大量无谓的开销(该开销来自线程的创建和销毁)。那么有没有一种方法让二级线程完成一个任务后,停止运行(而不是销毁线程),等主线程再次发送信号的时候,二级线程再次运行呢?答案肯定是有,那就是事件对象
// 定义一个事件对象
HANDLE hEvent;
<pre name="code" class="cpp">void ThreadProc(void *p)
{
BOOL *ThreadClose = (BOOL *)p;while(1){// 该变量用于判断是否关闭线程if(ThreadClose)_endthread();
// 直到事件对象被激活,函数才返回
WaitForSingleObject(hEvent, INFINITE);
// 函数主体
}
}
LRESULT CALLBACK WinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
// 省略通常的变量定义
// 定义一个变量,用于关闭线程
BOOL ThreadClose = 0;
switch(message)
{
case WM_CREATE:
// 在这里创建一个事件对象,初始化事件对象为未激活状态
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
// 创建线程
_beginthread(ThreadProc, 0, (void *)&ThreadClose);
return 0;
case WM_LBUTTONDOWN:
// 当鼠标左键时,激活事件对象
SetEvent(hEvent);
return 0;
case WM_RBUTTONDOWN:
// 当鼠标右键时,事件对象回未激活状态
ResetEvent(hEvent);
return 0;
case WM_DESTROY:
// 销毁事件对象
DeleteObject(hEvent);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
程序定义一个全局变量对象 hEvent ,调用 CreateEvent() 函数创建事件对象(第三个参数初始化对象状态为未激活状态)。当鼠标左键点击时,激活事件对象。当鼠标右击时,把事件对象设置为未激活对象。
WaitForSingleObject( HANDLE hEvent, LONG dwtime) 函数:
第二个参数:设置数值时,表示该函数等待指定的时间才返回(以毫秒为单位,但如果事件对象为激活状态,该函数立即返回)。设置为 INFINITE 时,表示函数等待事件对象激活为止。
CreateEvent( &sa, fManual, fInitial, pszName) 函数:
该函数返回一个事件对象
第一个参数:指向 SECURITY_ATTRIBUTES 结构的指针(只有事件对象在进程间共享时才有意义)
第二个参数:设置为 FALSE时,每次事件对象被激活后都自动设置为未激活状态
第三个参数:初始化事件对象的状态,为 TRUE 时,对象处于激活状态。为 FALSE 时,对象处于未激活状态
第四个参数:指向事件对象名的指针(只有时间对象在进程间共享时才有意义)
SetEvent( HANDLE hEvent) 函数:
设置事件对象为激活状态
ResetEvent( HANDLE hEvent) 函数:
设置事件对象为未激活状态
线程本地存储:
由于同一进程中的线程共享全局变量,而使用同一函数的线程共享函数内的静态局部变量(以上变量都为公用变量)。
函数内的局部变量在线程中被分配到堆栈,是线程的私有变量,但当线程退出该函数时,函数中的局部变量的数据将被清除(变量为线程私有变量,但函数退后后自动清除)
那么,怎样设置一种变量,让这种变量即使线程的私有变量,又可以在函数退出后被存储起来?那种变量就是线程本地存储(Thread-Local_Storage TLS)
在标准的 C 语言中并不支持这种数据,但在 WIndow 的 API 函数中,支持这种机制
1、定义一个含有所有数据的数据结构
typedef struct
{
int a;
........(其他数据)
}DATA, PDATA*;
2、在主线程中调用 API 函数中的 TlsAlloc 函数,返回一个指针,通过参数传递给线程
HANDLE Index;
Index = TlsAlloc();
3、分配全局变量,绑定该指针
TlsSetValue( Index, GlobalAlloc( GPTR, sizeof(DATA)));
4、在线程中使用该指针指向的内容
ThreadProc()
{
PDATA pdata;
pdata = (PDATA) TlsGetValue( Index);
}
typedef struct
{
int a;
int b;
int c
}DATA, PDATA *;
void ThreadProc(LPVOID p)
{
PDATA pdata;
pdata = (PDATA)TlsGetValue( (PDATA)p);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, LPARAM lParam, WPARAM wParam)
{
HADNLE Index;
........
case WM_CREATE:
index = TlsAlloc();
TlsSetValue(index, GlobalAlloc(sizeof(DATA)));
_beginthread(ThreadProc1, 0, (LPVOID)Index);
break;
}
// 线程本地局部变量
_declspec (thread) int iGlobal = 1;
// 线程本地静态变量
_declspec (thread) static int iLocal = 2;