临界区的互斥控制

来源:互联网 发布:淘宝个人主页怎么设置 编辑:程序博客网 时间:2024/05/16 23:40
 一、前言
我正在研究线程的通讯,无奈有关这方面的资料实在太少,没办法我只好去啃MSDN,但是MSDN好像说得也不太清楚。所以那我就写了这么一个例子,以望对学习多线程编程起到引玉抛砖的作用。有个易懂的例子学起来总是容易很多。近来我正在复习那几个排序算法,于是就把这些算法写到了这里来作为线程的例子。同时也对几个通用的排序算法思想作了一些说明。
这个例子利用多线程使用不同的排序算法对数据进行排序,每一个线程使用不同的算法。主线程里使用快速排序QuickSort,其他四个算法分别建立四个子线程,在子线程中进行排序。因为每一个线程都要调用函数PrintResult把结果输出到显示器上,所以不同的线程就会争夺着向显示器输出,这样,不同线程的输出就会混合在一起,所以呢必须让线程一个接着一个输出。也就是必须对PrintResult进行互斥控
制。要进行互斥控制,则必须用到Event、Mutex、CrititicalSection、Semaphore等互斥控制量。这个例子可以使用Event、Mutex、CrititicalSection,你可以根据提示修改代码使用其中的一种互斥量进行测试。 我所写的例子没有使用MFC,用的都是SDK的WINAPI,如果使用MFC时有些许差别,但原理是一样的。而且MFC还把线程分成用户界面线程和工作者线程,实质上用户界面线程跟工作者线程的差别是,用户界面线程要继承的基类已经实现了消息循环,MFC帮你做了很多的消息处理和界面控制的工作。

一、WINAPI线程控制函数简介:有关详细说明请查看MSDN

1.1 线程建立函数

HANDLE CreateThread(    LPSECURITY_ATTRIBUTES lpThreadAttributes,     // 安全属性结构指针,可为NULL    DWORD dwStackSize,     // 线程栈大小,若为0表示使用默认值    LPTHREAD_START_ROUTINE lpStartAddress,     // 指向线程函数的指针    LPVOID lpParameter,     // 传递给线程函数的参数,可以保存一个指针值    //所以,线程函数的参数只能是一个32位值    //而且线程函数返回值也有规定,必须是unsigned long    DWORD dwCreationFlags,     // 线程建立是的初始标记,运行或挂起    LPDWORD lpThreadId     // 指向接收线程号的DWORD变量);



1.2 临界资源控制函数:

1)事件对象的创建
事件对象的作用是为线程传送一个公共的事件信号,使用CreateEvent函数创建:

HANDLE CreateEvent(    LPSECURITY_ATTRIBUTES lpEventAttributes,    // 安全属性结构指针,可为NULL    BOOL bManualReset,     // 手动清除信号标记,TRUE在WaitForSingleObject后必须手动调    //用RetEvent清除信号。    //若为FALSE则在WaitForSingleObject后,系统自动清除事件信号    BOOL bInitialState, // 初始状态,TRUE有信号,FALSE无信号    LPCTSTR lpName // 信号量的名称,字符数不可多于MAX_PATH    //如果遇到同名的其他信号量函数就会失败,如果遇到同类信号同名    //也要注意变化);

2)互斥量的创建
互斥量的作用是保证每次只能有一个线程获得互斥量而得以继续执行,使用CreateMutex函数创建:
HANDLE CreateMutex(    LPSECURITY_ATTRIBUTES lpMutexAttributes,    // 安全属性结构指针,可为NULL    BOOL bInitialOwner, // 当前建立互斥量是否占有该互斥量    //TRUE表示占有,这样其他线程就不能获得此互斥量也就无法进入由    //该互斥量控制的临界区。FALSE表示不占有该互斥量    LPCTSTR lpName // 信号量的名称,字符数不可多于MAX_PATH    //如果遇到同名的其他信号量函数就会失败,如果遇到同类信号同名    //也要注意变化);

3)临界区信号的初始化
使用前必须先初始化
VOID InitializeCriticalSection(    LPCRITICAL_SECTION lpCriticalSection // 临界区变量指针); 


4)阻塞函数
如果等待的信号量不可用,那么线程就会挂起,直到信号可用线程才会被唤醒,该函数会自动修改信号,如Event,线程被唤醒之后
Event信号会变得无信号,Mutex、Semaphore等也会变。我们使用WaitForSingleObject函数等待信号,如果要等待多个信号可以使用WaitForMutipleObject函数。

DWORD WaitForSingleObject(    HANDLE hHandle, // 等待对象的句柄    DWORD dwMilliseconds // 等待毫秒数,INFINITE表示无限等待);


 

二、实例讲解
下面我们结合本文的示例代码进行具体的讲解:

2.1 函数、变量的申明

#include "stdafx.h"#include "stdlib.h"#include "memory.h"HANDLE evtTerminate; //事件信号,标记是否所有子线程都执行完

下面使用了三种控制方法,你可以注释其中两种,使用其中一种。注意修改时要连带修改临界区PrintResult里的相应控制语句
HANDLE evtPrint; //事件信号,标记事件是否已发生//CRITICAL_SECTION csPrint; //临界区//HANDLE mtxPrint; //互斥信号,如有信号表明已经有线程进入临界区并拥有此信号static long ThreadCompleted = 0; /*ThreadCompleted用来标记四个子线程中已完成线程的个数,当一个子线程完成时就对ThreadCompleted进行加一操作, 

要使用InterlockedIncrement(long* lpAddend)和InterlockedDecrement(long* lpAddend)进行加减操作*/
下面的结构是用于传送排序的数据给各个排序子线程
struct MySafeArray{    long* data;    int iLength;};
打印每一个线程的排序结果
void PrintResult(long* Array, int iLength, const char* HeadStr = "sort");


排序函数

int QuickSort(long* Array, int iLow, int iHigh); //快速排序unsigned long __stdcall BubbleSort(void* theArray); //冒泡排序unsigned long __stdcall SelectSort(void* theArray); //选择排序unsigned long __stdcall HeapSort(void* theArray); //堆排序unsigned long __stdcall InsertSort(void* theArray); //插入排序


以上四个函数的声明必须乎合作为一个线程函数的必要条件才可以使用CreateThread建立一个线程。
(1)调用方法必须是__stdcall,即函数参数压栈顺序由右到左,而且由函数本身负责栈的恢复, C和C++默认是__cdecl, 所以要显式声明是__stdcall
(2)返回值必须是unsigned long
(3)参数必须是一个32位值,如一个指针值或long类型
(4)如果函数是类成员函数,必须声明为static函数,在CreateThread时函数指针有特殊的写法。如下(函数是类CThreadTest的成员函数中):

static unsigned long _stdcall MyThreadFun(void* pParam);handleRet = CreateThread(NULL, 0, &CThreadTestDlg::MyThreadFun, NULL, 0, &ThreadID);

之所以要声明为static是由于该函数必须要独立于对象实例来使用,即使没有声明实例也可以使用。

2.2 具体实现代码

int main(int argc, char* argv[]){    /*    //下面的代码是为了从命令行上接收参数进行排序的    //但为了测试方便,所以就省去,改用静态数据进行排序    //排序数据在接着的data数组里静态声明    if(argc <= 1)    {         printf("Please Input Data.");         return 0;    }    int i;    long *data;    int iDataLen = argc - 1;    data = new long[argc-1];    for (i=0; i<argc-1; i++)    {         data[i] = atol(argv[i+1]);    }    */    long data[] = {123,34,546,754,34,74,3,56};    int iDataLen = 8;    //为了对各个子线程分别对原始数据进行排序和保存排序结果    //分别分配内存对data数组的数据进行复制    long *data1, *data2, *data3, *data4, *data5;    MySafeArray StructData1, StructData2, StructData3, StructData4;    data1 = new long[iDataLen];    memcpy(data1, data, iDataLen << 2); //把data中的数据复制到data1中    //内存复制 memcpy(目标内存指针, 源内存指针, 复制字节数), 因为long的长度    //为4字节,所以复制的字节数为iDataLen << 2, 即等于iDataLen*4    StructData1.data = data1;    StructData1.iLength = iDataLen;    data2 = new long[iDataLen];    memcpy(data2, data, iDataLen << 2);    StructData2.data = data2;    StructData2.iLength = iDataLen;    data3 = new long[iDataLen];    memcpy(data3, data, iDataLen << 2);    StructData3.data = data3;    StructData3.iLength = iDataLen;    data4 = new long[iDataLen];    memcpy(data4, data, iDataLen << 2);    StructData4.data = data4;    StructData4.iLength = iDataLen;    data5 = new long[iDataLen];    memcpy(data5, data, iDataLen << 2);            unsigned long TID1, TID2, TID3, TID4;     //对信号量进行初始化    evtTerminate = CreateEvent(NULL, FALSE, FALSE, "Terminate");    evtPrint = CreateEvent(NULL, FALSE, TRUE, "PrintResult");    //mtxPrint = CreateMutex(NULL, FALSE, "PrintMutex");    //InitializeCriticalSection(&csPrint);    //分别建立各个子线程     CreateThread(NULL, 0, &BubbleSort, &StructData1, NULL, &TID1);    CreateThread(NULL, 0, &SelectSort, &StructData2, NULL, &TID2);    CreateThread(NULL, 0, &HeapSort, &StructData3, NULL, &TID3);    CreateThread(NULL, 0, &InsertSort, &StructData4, NULL, &TID4);    //在主线程中执行行快速排序,其他排序在子线程中执行    QuickSort(data5, 0, iDataLen - 1);    PrintResult(data5, iDataLen, "Quick Sort");    WaitForSingleObject(evtTerminate, INFINITE); //等待所有的子线程结束    //所有的子线程结束后,主线程才可以结束     //delete[] data;    delete[] data1;    delete[] data2;    delete[] data3;    delete[] data4;    CloseHandle(evtPrint);    return 0;}


每一个线程都要使用下面这个函数进行输出,而且只有一个显示器,产生多个线程竞争对控制台的使用权。

//*****************************临界区***************************************// void PrintResult(long* Array, int iLength, const char* HeadStr){    WaitForSingleObject(evtPrint, INFINITE); //等待事件有信号    //EnterCriticalSection(&csPrint); //标记有线程进入临界区    //WaitForSingleObject(mtxPrint, INFINITE); //等待互斥量空置(没有线程拥有它)    int i;    printf("%s: ", HeadStr);    for (i=0; i<iLength-1; i++)    {        printf("%d,", Array[i]);        Sleep(100); //延时(可以去掉)        /*只是使得多线程对临界区访问的问题比较容易看得到        如果你把临界控制的语句注释掉,输出就会变得很凌乱,各个排序的结果会        分插间隔着输出,如果不延时就不容易看到这种不对临界区控制的结果        */    }    printf("%d\n", Array[i]);    SetEvent(evtPrint); //把事件信号量恢复,变为有信号    //LeaveCriticalSection(&csPrint); //标记线程离开临界区    //ReleaseMutex(mtxPrint); //释放对互斥量的占有}//*************************************************************************** 


 

三、排序思想与具体算法

3.1 冒泡排序思想(升序,降序同理,后面的算法一样都是升序):
从头到尾对数据进行两两比较进行交换,小的放前大的放后。这样一次下来,最大的元素就会被交换的最后,然后下一次循环就不用对最后一个元素进行比较交换了,所以呢每一次比较交换的次数都比上一次循环的次数少一,这样N次之后数据就变得升序排列了

unsigned long __stdcall BubbleSort(void* theArray){    long* Array = ((MySafeArray*)theArray)->data;    int iLength = ((MySafeArray*)theArray)->iLength;    int i, j=0;    long swap;    for (i = iLength-1; i > 0; i--)    {      for(j = 0; j < i; j++)      {        if(Array[j] > Array[j+1]) //前比后大,交换        {        swap = Array[j];        Array[j] = Array[j+1];        Array[j+1] = swap;        }      }    }    PrintResult(Array, iLength, "Bubble Sort"); //向控制台打印排序结果    InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1    if(ThreadCompleted == 4) SetEvent(evtTerminate);//检查是否其他线程都已执行完    //若都执行完则设置程序结束信号量    return 0;}

3.2 选择排序思想:
每一次都从无序的数据中找出最小的元素,然后和前面已经有序的元素序列的后一个元素进行交换,这样整个源序列就会分成两部分,前面一部分是已经排好序的有序序列,后面一部分是无序的,用于选出最小的元素。 循环N次之后,前面的有序序列加长到跟源序列一样长,后面的无序部分长度变为0,排序就完成了。
unsigned long __stdcall SelectSort(void* theArray){    long* Array = ((MySafeArray*)theArray)->data;    int iLength = ((MySafeArray*)theArray)->iLength;    long lMin, lSwap;    int i, j, iMinPos;    for(i=0; i < iLength-1; i++)    {      lMin = Array[i];      iMinPos = i;      for(j=i + 1; j <= iLength-1; j++) //从无序的元素中找出最小的元素      {        if(Array[j] < lMin)        {         iMinPos = j;         lMin = Array[j];        }      }      //把选出的元素交换拼接到有序序列的最后      lSwap = Array[i];      Array[i] = Array[iMinPos];      Array[iMinPos] = lSwap;    }    PrintResult(Array, iLength, "Select Sort"); //向控制台打印排序结果    InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1    if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完    //若都执行完则设置程序结束信号量    return 0;}

3.3 堆排序思想:
堆:数据元素从1到N排列成一棵二叉树,而且这棵树的每一个子树的根都是该树中的元素的最小或最大的元素这样如果一个无序数据集合是一个堆那么,根元素就是最小或最大的元素 ,堆排序就是不断对剩下的数据建堆,把最小或最大的元素析透出来。
下面的算法,就是从最后一个元素开始,依据一个节点比父节点数值大的原则对所有元素进行调整,这样调整一次就形成一个堆,第一个元素就是最小的元素。然后再对剩下的无序数据再进行建堆,注意这时后面的无序数据元素的序数都要改变,如第一次建堆后,第二个元素就会变成堆的第一个元素。
unsigned long __stdcall HeapSort(void* theArray){    long* Array = ((MySafeArray*)theArray)->data;    int iLength = ((MySafeArray*)theArray)->iLength;    int i, j, p;    long swap;    for(i=0; i<iLength-1; i++)    {      for(j = iLength - 1; j>i; j--) //从最后倒数上去比较字节点和父节点      {        p = (j - i - 1)/2 + i; //计算父节点数组下标        //注意到树节点序数跟数组下标不是等同的,因为建堆的元素个数逐个递减        if(Array[j] < Array[p]) //如果父节点数值大则交换父节点和字节点        {          swap = Array[j];          Array[j] = Array[p];          Array[p] = swap;        }      }    }    PrintResult(Array, iLength, "Heap Sort"); //向控制台打印排序结果     InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1    if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完    //若都执行完则设置程序结束信号量    return 0;}


3.4 插入排序思想:
把源数据序列看成两半,前面一半是有序的,后面一半是无序的,把无序的数据从头到尾逐个逐个的插入到前面的有序数据中,使得有序的数据的个数不断增大,同时无序的数据个数就越来越少,最后所有元素都会变得有序。
unsigned long __stdcall InsertSort(void* theArray){    long* Array = ((MySafeArray*)theArray)->data;    int iLength = ((MySafeArray*)theArray)->iLength;    int i=1, j=0;    long temp;    for(i=1; i<iLength; i++)    {      temp = Array[i]; //取出序列后面无序数据的第一个元素值      for(j=i; j>0; j--) //和前面的有序数据逐个进行比较找出合适的插入位置      {        if(Array[j - 1] > temp) //如果该元素比插入值大则后移          Array[j] = Array[j - 1];        else //如果该元素比插入值小,那么该位置的后一位就是插入元素的位置          break;       }      Array[j] = temp;    }    PrintResult(Array, iLength, "Insert Sort"); //向控制台打印排序结果    InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1    if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完    //若都执行完则设置程序结束信号量    return 0;}


3.5 快速排序思想:
快速排序是分治思想的一种应用,它先选取一个支点,然后把小于支点的元素交换到支点的前边,把大于支点的元素交换到支点的右边。然后再对支点左边部分和右边部分进行同样的处理,这样若干次之后,数据就会变得有序。
下面的实现使用了递归
建立两个游标:iLow,iHigh;iLow指向序列的第一个元素,iHigh指向最后一个先选第一个元素作为支点,并把它的值存贮在一个辅助变量里。那么第一个位置就变为空并可以放置其他的元素。 这样从iHigh指向的元素开始向前移动游标iHigh查找比支点小的元素,如果找到,则把它放置到空置了的位置(现在是第一个位置)然后iHigh游标停止移动,这时iHigh指向的位置被空置,然后移动iLow游标寻找比支点大的元素放置到iHigh指向的空置的位置,如此往复直到iLow与iHigh相等。最后使用递归对左右两部分进行同样处理.
int QuickSort(long* Array, int iLow, int iHigh){    if(iLow >= iHigh) return 1; //递归结束条件    long pivot = Array[iLow];    int iLowSaved = iLow, iHighSaved = iHigh; //保未改变的iLow,iHigh值保存起来    while (iLow < iHigh)    {        while (Array[iHigh] >= pivot && iHigh > iLow) //寻找比支点大的元素        iHigh -- ;        Array[iLow] = Array[iHigh]; //把找到的元素放置到空置的位置        while (Array[iLow] < pivot && iLow < iHigh) //寻找比支点小的元素          iLow ++ ;        Array[iHigh] = Array[iLow]; //把找到的元素放置到空置的位置    }    Array[iLow] = pivot; //把支点值放置到支点位置,这时支点位置是空置的    //对左右部分分别进行递归处理    QuickSort(Array, iLowSaved, iHigh-1);    QuickSort(Array, iLow+1, iHighSaved);    return 0;}

原创粉丝点击