一、前言
我正在研究线程的通讯,无奈有关这方面的资料实在太少,没办法我只好去啃MSDN,但是MSDN好像说得也不太清楚。所以那我就写了这么一个例子,以望对学习多线程编程起到引玉抛砖的作用。有个易懂的例子学起来总是容易很多。近来我正在复习那几个排序算法,于是就把这些算法写到了这里来作为线程的例子。同时也对几个通用的排序算法思想作了一些说明。

下载本文源代码sequence.zip,大小:8K

这个例子利用多线程使用不同的排序算法对数据进行排序,每一个线程使用不同的算法。主线程里使用快速排序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); //释放对互斥量的占有
}
//***************************************************************************
posted on 2006-11-08 20:29  沧海-重庆  阅读(1111)  评论(0编辑  收藏  举报