C++ 多线程编程 & OpenMP

多线程编程浅析(1)——概述

线程,是在进程内部又细分的一个执行单元,他们都是由操作系统来进行划分控制的。系统创建好进程后,实际上就启动执行了该进程的主执行线程,主执行线程以函数地址形式,比如说main或WinMain函数,将程序的启动点提供给Windows系统。主执行线程终止了,进程也就随之终止。每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。一个进程中的所有线程都在该进程的虚拟地址空间中,共同使用这些虚拟地址空间、全局变量和系统资源,所以线程间的通讯非常方便,多线程技术的应用也较为广泛。

我们要操作线程,就必须依赖于操作系统提供的接口。下面我们来简要介绍一下操作系统提供的跟线程相关的API函数:

HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);

该函数在其调用进程的进程空间里创建一个新的线程,并返回已建线程的句柄,其中各参数说明如下:
lpThreadAttributes:指向一个 SECURITY_ATTRIBUTES 结构的指针,该结构决定了线程的安全属性,一般置为 NULL;
dwStackSize:指定了线程的堆栈深度,一般都设置为0;
lpStartAddress:表示新线程开始执行时代码所在函数的地址,即线程的起始地址。一般情况为(LPTHREAD_START_ROUTINE)ThreadProcess,ThreadProcess 是线程函数名;
lpParameter:指定了线程执行时传送给线程的32位参数,即线程函数的参数;
dwCreationFlags:控制线程创建的附加标志,可以取两种值。如果该参数为0,线程在被创建后就会立即开始执行;如果该参数为CREATE_SUSPENDED,则系统产生线程后,该线程处于挂起状态,并不马上执行,直至函数ResumeThread被调用;
lpThreadId:该参数返回所创建线程的ID;
如果创建成功则返回线程的句柄,否则返回NULL。

DWORD SuspendThread(HANDLE hThread);

该函数用于挂起指定的线程,如果函数执行成功,则线程的执行被终止。

DWORD ResumeThread(HANDLE hThread);

该函数用于结束线程的挂起状态,执行线程。

VOID ExitThread(DWORD dwExitCode);

该函数用于线程终结自身的执行,主要在线程的执行函数中被调用。其中参数dwExitCode用来设置线程的退出码。

BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);

一般情况下,线程运行结束之后,线程函数正常返回,但是应用程序可以调用TerminateThread强行终止某一线程的执行。各参数含义如下:
hThread:将被终结的线程的句柄;
dwExitCode:用于指定线程的退出码。
使用TerminateThread()终止某个线程的执行是不安全的,可能会引起系统不稳定;虽然该函数立即终止线程的执行,但并不释放线程所占用的资源。因此,一般不建议使用该函数。

BOOL PostThreadMessage(DWORD idThread,
UINT Msg,
WPARAM wParam,
LPARAM lParam);

该函数将一条消息放入到指定线程的消息队列中,并且不等到消息被该线程处理时便返回。
idThread:将接收消息的线程的ID;
Msg:指定用来发送的消息;
wParam:同消息有关的字参数;
lParam:同消息有关的长参数;
调用该函数时,如果即将接收消息的线程没有创建消息循环,则该函数执行失败。


下面我给出一个简单的建立线程的例子:
<span style="font-size: 14px; font-weight: normal;">#include "stdafx.h"
#include "windows.h"
#include "stdio.h"

void ThreadProcess()
{
    for(int i=0; i<6; i++)
    {
        Sleep(1000);
        printf("Sub Thread Tick %d! \n",i*1000);
    }
    printf("Exit Sub Thread!\n");
    
}

int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE hThread;
    DWORD ThreadID;
    hThread=CreateThread(NULL,
                     0,
                     (LPTHREAD_START_ROUTINE)ThreadProcess,
                     NULL,
                     0,
                     &ThreadID);
    
    for(int i=0; i<10; i++)
    {
        Sleep(600);
        printf("Main Thread Tick %d! \n", i*600);
    }
    printf("Main Thread Loop Finished! \n");
    system("pause");
    return 0;
}</span>




多线程编程浅析(2)——线程间通信

上文我们介绍了如何建立一个简单的多线程程序,多线程之间不可避免的需要进行通信。相比于进程间通信来说,线程间通信无疑是相对比较简单的。

首先我们来看看最简单的方法,那就是使用全局变量(静态变量也可以)来进行通信,由于属于同一个进程的各个线程是处于同一个进程空间中的,并且它们共享这个进程的各种资源,因此它们都可以毫无障碍的访问这个进程中的全局变量。当需要有多个线程来访问一个全局变量时,通常我们会在这个全局变量前加上volatile声明,来告诉编译器这个全局变量是易变的,让编译器不要对这个变量进行优化(至于编译器到底有没有按照你的要求来对volatile进行处理这个暂且不理)。

下面贴出一段简单的示例代码:

<span style="font-size: 14px; font-weight: normal;">#include "stdafx.h"
#include "windows.h"
#include "stdio.h"

volatile int ThreadData = 0;

void ThreadProcess()
{
    for(int i=0; i<6; i++)
    {
        ThreadData += 1000;
        Sleep(1000);
        printf("Sub  Thread Tick %5d! %5d\n",(i+1)*1000, ThreadData);
    }
    printf("Exit Sub Thread!\n");
    
}

int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE hThread;
    DWORD ThreadID;
    hThread=CreateThread(NULL,
                     0,
                     (LPTHREAD_START_ROUTINE)ThreadProcess,
                     NULL,
                     0,
                     &ThreadID);
    
    for(int i=0; i<10; i++)
    {
        ThreadData -= 600;
        Sleep(600);
        printf("Main Thread Tick %5d! %5d\n", (i+1)*600, ThreadData);
    }
    printf("Main Thread Loop Finished! \n");
    system("pause");
    return 0;
}</span>



除了全局变量之外,还有其他的方法,比如利用消息机制等来实现线程间通信。这个就不详细解释了,关于消息机制,详情请看Windows消息机制概述

下面,关于多线程中的全局变量,我来介绍点有点偏题的东西:
线程局部存储(TLS)
进程中的全局变量与函数内定义的静态(static)变量,是各个线程都可以访问的共享变量。在一个线程修改的内存内容,对所有线程都生效。这是一个优点也是一个缺点。说它是优点,线程的数据交换变得非常快捷。说它是缺点,一个线程死掉了,其它线程也性命不保; 多个线程访问共享数据,需要昂贵的同步开销,也容易造成同步相关的BUG。
  如果需要在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量(被称为static memory local to a thread 线程局部静态变量),就需要新的机制来实现。这就是TLS。
  线程局部存储在不同的平台有不同的实现,可移植性不太好。 
  方法一:每个线程创建时系统给它分配一个LPVOID指针的数组(叫做TLS数组),这个数组从C编程角度是隐藏着的不能直接访问,需要通过一些C API函数调用访问。首先定义一些DWORD线程全局变量或函数静态变量,准备作为各个线程访问自己的TLS数组的索引变量。一个线程使用TLS时,第一步在线程内调用TlsAlloc()函数,为一个TLS数组索引变量与这个线程的TLS数组的某个槽(slot)关联起来,例如获得一个索引变量:
  global_dwTLSindex=TLSAlloc();
  注意,此步之后,当前线程实际上访问的是这个TLS数组索引变量的线程内的拷贝版本。也就说,不同线程虽然看起来用的是同名的TLS数组索引变量,但实际上各个线程得到的可能是不同DWORD值。其意义在于,每个使用TLS的线程获得了一个DWORD类型的线程局部静态变量作为TLS数组的索引变量。C/C++原本没有直接定义线程局部静态变量的机制,所以在如此大费周折。
  第二步,为当前线程动态分配一块内存区域(使用LocalAlloc()函数调用),然后把指向这块内存区域的指针放入TLS数组相应的槽中(使用TlsValue()函数调用)。
  第三步,在当前线程的任何函数内,都可以通过TLS数组的索引变量,使用TlsGetValue()函数得到上一步的那块内存区域的指针,然后就可以进行内存区域的读写操作了。这就实现了在一个线程内部这个范围处处可访问的变量。
  最后,如果不再需要上述线程局部静态变量,要动态释放掉这块内存区域(使用LocalFree()函数),然后从TLS数组中放弃对应的槽(使用TlsFree()函数)。
  方法二:
  直接声明这个变量是各个线程有自己拷贝的线程局部静态变量:
  __declspec( thread ) int var_name; 

多线程编程浅析(3)——多线程的互斥与同步

在进行多线程编程时,难免还要碰到两个问题,那就线程间的互斥与同步:
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步(下文统称为同步)。

线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
下面我们来分别看一下这些方法:

原子操作(全局变量):
<span style="font-size: 14px; font-weight: normal;">#include "stdafx.h"
#include "windows.h"
#include "stdio.h"

volatile int ThreadData = 1;

void ThreadProcess()
{
    for(int i=0; i<6; i++)
    {
        Sleep(1000);
        printf("Sub  Thread Tick %5d!\n",(i+1)*1000);
    }
    ThreadData = 0;
    printf("Exit Sub Thread!\n");

}

int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE hThread;
    DWORD ThreadID;
    hThread=CreateThread(NULL,
        0,
        (LPTHREAD_START_ROUTINE)ThreadProcess,
        NULL,
        0,
        &ThreadID);

    while (ThreadData)
    {
        printf("Main Thread is waiting for Sub Thread!\n");
        Sleep(600);
    }

    printf("Main Thread Finished! \n");
    system("pause");
    return 0;
}</span>



在上面的程序中,我利用了全局变量ThreadData来进行线程间的同步,当子线程结束时改变该值,而父线程则循环判断该值来确认子线程是否已经结束,当子线程结束时,父线程才继续进行下面的操作。

临界区(Critical Section)

保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

临界区包含两个操作原语:
EnterCriticalSection() 进入临界区
LeaveCriticalSection() 离开临界区

EnterCriticalSection()语句执行后代码将进入临界区以后无论发生什么,必须确保与之匹配的LeaveCriticalSection()都能够被执行到。否则临界区保护的共享资源将永远不会被释放。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。


事件(Event)

事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。

信号量包含的几个操作原语:
  CreateEvent() 创建一个信号量
  OpenEvent() 打开一个事件
  SetEvent() 回置事件
  WaitForSingleObject() 等待一个事件
  WaitForMultipleObjects()  等待多个事件

WaitForMultipleObjects 函数原型:
  WaitForMultipleObjects(
  IN DWORD nCount, // 等待句柄数
  IN CONST HANDLE *lpHandles, //指向句柄数组
  IN BOOL bWaitAll, //是否完全等待标志
  IN DWORD dwMilliseconds //等待时间
  )

参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回WAIT_TIMEOUT。

事件可以实现不同进程中的线程同步操作,并且可以方便的实现多个线程的优先比较等待操作,例如写多个WaitForSingleObject来代替WaitForMultipleObjects从而使编程更加灵活。

互斥量(Mutex)

互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。

  互斥量包含的几个操作原语:
  CreateMutex() 创建一个互斥量
  OpenMutex() 打开一个互斥量
  ReleaseMutex() 释放互斥量
  WaitForMultipleObjects() 等待互斥量对象

信号量(Semaphores)

信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。

PV操作及信号量的概念都是由荷兰科学家E.W.Dijkstra提出的。信号量S是一个整数,S大于等于零时代表可供并发进程使用的资源实体数,但S小于零时则表示正在等待使用共享资源的进程数。

P操作申请资源:
  (1)S减1;
  (2)若S减1后仍大于等于零,则进程继续执行;
  (3)若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。

V操作释放资源:
  (1)S加1;
  (2)若相加结果大于零,则进程继续执行;
  (3)若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。

  信号量包含的几个操作原语:
  CreateSemaphore() 创建一个信号量
  OpenSemaphore() 打开一个信号量
  ReleaseSemaphore() 释放信号量
  WaitForSingleObject() 等待信号量

信号量的使用特点使其更适用于对Socket(套接字)程序中线程的同步。例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这时可以为每一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。

因为它们的使用方法都很类似,下面我结合起来给出一个简单的示例:

<span style="font-size: 14px; font-weight: normal;">#include "stdafx.h"
#include "windows.h"
#include "stdio.h"

volatile int ThreadData = 1;

CRITICAL_SECTION csPrint; // 临界区
//HANDLE evtPrint; // 事件信号,标记事件是否已发生
//HANDLE mtxPrint; // 互斥信号,如有信号表明已经有线程进入临界区并拥有此信号
//HANDLE smphPrint; // 信号量,表示是否已经达到允许的最大线程数

void Print(char *str)
{
    EnterCriticalSection(&csPrint); // 进入临界区
    //WaitForSingleObject(evtPrint, INFINITE); // 等待事件有信号
    //WaitForSingleObject(mtxPrint, INFINITE); // 等待互斥量空置(没有线程拥有它)
    //WaitForSingleObject(smphPrint, INFINITE); // 等待对共享资源请求被通过 等于 P操作 

    for (;*str != '\0';str++)
    {
        Sleep(50);
        printf("%c",*str);
    }
    printf("\n");

    LeaveCriticalSection(&csPrint); // 退出临界区
    //SetEvent(evtPrint); // 把事件信号量恢复,变为有信号
    //ReleaseMutex(mtxPrint); // 释放对互斥量的占有
    //ReleaseSemaphore(smphPrint, 1, NULL); // 释放共享资源 等于 V操作 

}

void ThreadProcess()
{
    for(int i=0; i<6; i++)
    {
        Sleep(1000);
        Print("Sub  Thread is running!");
    }
    ThreadData = 0;

}

int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE hThread;
    DWORD ThreadID;

    InitializeCriticalSection(&csPrint); // 初始化临界区
    //evtPrint = CreateEvent(NULL, FALSE, TRUE, L"PrintEvent"); // 初始化事件
    //mtxPrint = CreateMutex(NULL, FALSE, L"PrintMutex"); // 初始化互斥量
    //smphPrint= CreateSemaphore(NULL, 1, 1, L"PrintSemaphore"); // 设置信号量1个资源,因此同时只可以有一个线程访问 

    hThread=CreateThread(NULL,
        0,
        (LPTHREAD_START_ROUTINE)ThreadProcess,
        NULL,
        0,
        &ThreadID);

    while (ThreadData)
    {
        Print("Main Thread is waiting for Sub Thread!");
        Sleep(600);
    }

    printf("Main Thread Finished!");
    system("pause");
    return 0;
}</span>



综上所述:当在同一进程中的多线程同步时,临界区是效率最最高,基本不需要什么开销。而内核对象由于要进行用户态和内核态的切换,开销较大,但是内核对象由于可以命名,因此它们同时可以用于进程间的同步。另外,值得一提的是,信号量可以设置允许访问资源的线程或进程个数,而不仅仅是只允许单个线程或进程访问资源。





OpenMP 参考链接:

(1)http://blog.csdn.net/augusdi/article/details/8807972
(2)http://www.cnblogs.com/yangyangcv/archive/2012/03/23/2413335.html
(3)http://blog.csdn.net/gengshenghong/article/details/7000979

你可能感兴趣的:(多线程,数据结构,C++,算法)