2、多线程及多线程间同步执行

来源:互联网 发布:合肥晨飞网络待遇 编辑:程序博客网 时间:2024/05/22 04:55

    多线程

        多线程概念:一个程序中,程序把 "自己" 分成几个 "执行线程" 并行运行

        在代码角度:一个程序首先执行主线程(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 线程函数

关于临界区的一个问题:

       当多个(多于一个)线程进入同一个临界区时,线程的执行顺序时怎样的?

    创建多线程

       1、 调用 API 函数创建一个线程的函数 CreateThread:
        HANDLE hThread;
        hThread = CreateThread( &security_attributes,  dwStackSize,  ThreadPrco, pParam,  dwFlags,  &idThreadID);

        DWORD WINAPI ThreadProc(  LPVOID pParam);
        第一个参数:指向 SECURITY_ATTRIBUTES 结构的指针
        第二个参数:线程的初始化大小,一般设置为 0 取默认值
        第三个参数:指向函数的指针,指向线程要运行的函数地址
        第四个参数:指向任意类型的指针,传递给线程函数的数据
        第五个参数:设置为 0 表示线程创建后执行,设置为 CREATE_SUSPENDED 表示线程创建后被挂起,直到调用 ResumeThread 函数才开始执行
        第六个参数:指向新线程ID的指针

        2、调用 C运行库的函数 _beginthread(在头文件 PROCESS.H 中定义):
        hThread = _beginthread( ThreadProc, uiStackSize, pParam);

        void _cdecl ThreadProc( void * pParam);
        该线程函数返回类型是 VOID,参数也是一个 VOID 类型的指针,所以线程函数不用这个指针传递数据

        注意事项

        不同线程使用同一个函数时,函数内的局部变量是每个线程的私有数据,但静态局部变量是所有线程共享的

      事件对象

        如果一个程序每次运行相同的任务都为该任务重新创建新的线程,那么将有大量无谓的开销(该开销来自线程的创建和销毁)。那么有没有一种方法让二级线程完成一个任务后,停止运行(而不是销毁线程),等主线程再次发送信号的时候,二级线程再次运行呢?答案肯定是有,那就是事件对象

// 定义一个事件对象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;}


        另外微软扩充了C语言,提供一个简单的方法,直接在线程函数的变量加前缀:

        //  线程本地局部变量

        _declspec (thread) int iGlobal = 1;

        //  线程本地静态变量

        _declspec (thread) static int iLocal = 2;




0 0