线程的三种通信方法与三种同步方式

线程的三种通信方法与三种同步方式

2023年6月22日发(作者:)

线程的三种通信⽅法与三种同步⽅式  ⼀、线程之间的通信

  通常情况下,⼀个次级线程要为主线程完成某种特定类型的任务,这就隐含着表⽰在主线程和次级线程之间需要建⽴⼀个通信的通道。⼀般情况下,有下⾯的⼏种⽅法实现这种通信任务:使⽤全局变量(上⼀节的例⼦其实使⽤的就是这种⽅法)、使⽤事件对象、使⽤消息。这⾥我们主要介绍后两种⽅法。

  (⼀) 利⽤⽤户定义的消息通信

  在Windows程序设计中,应⽤程序的每⼀个线程都拥有⾃⼰的消息队列,甚⾄⼯作线程也不例外,这样⼀来,就使得线程之间利⽤消息来传递信息就变的⾮常简单。⾸先⽤户要定义⼀个⽤户消息,如下所⽰:#define WM_USERMSG WMUSER+100;在需要的时候,在⼀个线程中调⽤::PostMessage((HWND)param,WM_USERMSG,0,0)或CwinThread::PostThradMessage()来向另外⼀个线程发送这个消息,上述函数的四个参数分别是消息将要发送到的⽬的窗⼝的句柄、要发送的消息标志符、消息的参数WPARAM和LPARAM。下⾯的代码是对上节代码的修改,修改后的结果是在线程结束时显⽰⼀个对话框,提⽰线程结束:

UINT ThreadFunction(LPVOID pParam){ while(!bend) {  Beep(100,100);  Sleep(1000); } ::PostMessage(hWnd,WM_USERMSG,0,0); return 0;}WM_USERMSG消息的响应函数为OnThreadended(WPARAM wParam,LPARAM lParam)LONG CTestView::OnThreadended(WPARAMwParam,LPARAM lParam){ AfxMessageBox("Threadended."); Retrun 0;}

  上⾯的例⼦是⼯作者线程向⽤户界⾯线程发送消息,对于⼯作者线程,如果它的设计模式也是消息驱动的,那么调⽤者可以向它发送初始化、退出、执⾏某种特定的处理等消息,让它在后台完成。在控制函数中可以直接使⽤::GetMessage()这个SDK函数进⾏消息分检和处理,⾃⼰实现⼀个消息循环。GetMessage()函数在判断该线程的消息队列为空时,线程将系统分配给它的时间⽚让给其它线程,不⽆效的占⽤CPU的时间,如果消息队列不为空,就获取这个消息,判断这个消息的内容并进⾏相应的处理。   (⼆)⽤事件对象实现通信

  在线程之间传递信号进⾏通信⽐较复杂的⽅法是使⽤事件对象,⽤MFC的Cevent类的对象来表⽰。事件对象处于两种状态之⼀:有信号和⽆信号,线程可以监视处于有信号状态的事件,以便在适当的时候执⾏对事件的操作。上述例⼦代码修改如下:

Cevent threadStart ,threadEnd;UINT ThreadFunction(LPVOID pParam){ ::WaitForSingleObject(threadStart.m_hObject,INFINITE); AfxMessageBox("Threadstart."); while(!bend) {  Beep(100,100);  Sleep(1000);  Intresult=::WaitforSingleObject(threadEnd.m_hObject,0);  //等待threadEnd事件有信号,⽆信号时线程在这⾥悬停  If(result==Wait_OBJECT_0)   Bend=TRUE; } ::PostMessage(hWnd,WM_USERMSG,0,0); return 0;}/Void CtestView::OninitialUpdate(){ hWnd=GetSafeHwnd(); nt();//threadStart事件有信号 pThread=AfxBeginThread(ThreadFunction,hWnd);//启动线程 pThread->m_bAutoDelete=FALSE; Cview::OnInitialUpdate();}Void CtestView::OnDestroy(){ nt(); WaitForSingleObject(pThread->m_hThread,INFINITE); deletepThread; Cview::OnDestroy();}  运⾏这个程序,当关闭程序时,才显⽰提⽰框,显⽰"Thread ended"。⼆、线程之间的同步

  前⾯我们讲过,各个线程可以访问进程中的公共变量,所以使⽤多线程的过程中需要注意的问题是如何防⽌两个或两个以上的线程同时访问同⼀个数据,以免破坏数据的完整性。保证各个线程可以在⼀起适当的协调⼯作称为线程之间的同步。前⾯⼀节介绍的事件对象实际上就是⼀种同步形式。Visual C++中使⽤同步类来解决操作系统的并⾏性⽽引起的数据不安全的问题,MFC⽀持的七个多线程的同步类可以分成两⼤类:同步对象(CsyncObject、Csemaphore、Cmutex、CcriticalSection和Cevent)和同步访问对象(CmultiLock和CsingleLock)。本节主要介绍临界区(critical section)、互斥(mutexe)、信号量(semaphore),这些同步对象使各个线程协调⼯作,程序运⾏起来更安全。

  (⼀) 临界区

  临界区是保证在某⼀个时间只有⼀个线程可以访问数据的⽅法。使⽤它的过程中,需要给各个线程提供⼀个共享的临界区对象,⽆论哪个线程占有临界区对象,都可以访问受到保护的数据,这时候其它的线程需要等待,直到该线程释放临界区对象为⽌,临界区被释放后,另外的线程可以强占这个临界区,以便访问共享的数据。临界区对应着⼀个CcriticalSection对象,当线程需要访问保护数据时,调⽤临界区对象的Lock()成员函数;当对保护数据的操作完成之后,调⽤临界区对象的Unlock()成员函数释放对临界区对象的拥有权,以使另⼀个线程可以夺取临界区对象并访问受保护的数据。同时启动两个线程,它们对应的函数分别为WriteThread()和ReadThread(),⽤以对公共数组组array[]操作,下⾯的代码说明了如何使⽤临界区对象:

#include "afxmt.h"int array[10],destarray[10];CCriticalSection Section;UINT WriteThread(LPVOID param){ (); for(intx=0;x<10;x++)  array[x]=x; ();}UINT ReadThread(LPVOID param){ (); For(intx=0;x<10;x++)  Destarray[x]=array[x];  ();}

  上述代码运⾏的结果应该是Destarray数组中的元素分别为1-9,⽽不是杂乱⽆章的数,如果不使⽤同步,则不是这个结果,有兴趣的读者可以实验⼀下。

  (⼆)互斥

  互斥与临界区很相似,但是使⽤时相对复杂⼀些,它不仅可以在同⼀应⽤程序的线程间实现同步,还可以在不同的进程间实现同步,从⽽实现资源的安全共享。互斥与Cmutex类的对象相对应,使⽤互斥对象时,必须创建⼀个CSingleLock或CMultiLock对象,⽤于实际的访问控制,因为这⾥的例⼦只处理单个互斥,所以我们可以使⽤CSingleLock对象,该对象的Lock()函数⽤于占有互斥,Unlock()⽤于释放互斥。实现代码如下:

#include "afxmt.h"int array[10],destarray[10];CMutex Section;

UINT WriteThread(LPVOID param){ CsingleLocksinglelock; singlelock(&Section); (); for(int x=0;x<10;x++)  array[x]=x; ();}

UINT ReadThread(LPVOID param){ CsingleLocksinglelock; singlelock(&Section); (); For(intx=0;x<10;x++)  Destarray[x]=array[x];  ();}

  (三)信号量

  信号量的⽤法和互斥的⽤法很相似,不同的是它可以同⼀时刻允许多个线程访问同⼀个资源,创建⼀个信号量需要⽤Csemaphore类声明⼀个对象,⼀旦创建了⼀个信号量对象,就可以⽤它来对资源的访问技术。要实现计数处理,先创建⼀个CsingleLock或CmltiLock对象,然后⽤该对象的Lock()函数减少这个信号量的计数值,Unlock()反之。下⾯的代码分别启动三个线程,执⾏时同时显⽰⼆个消息框,然后10秒后第三个消息框才得以显⽰。

/Csemaphore *semaphore;Semaphore=new Csemaphore(2,2);HWND hWnd=GetSafeHwnd();AfxBeginThread(threadProc1,hWnd);AfxBeginThread(threadProc2,hWnd);AfxBeginThread(threadProc3,hWnd);UINT ThreadProc1(LPVOID param){ CsingleLocksingelLock(semaphore); (); Sleep(10000); ::MessageBox((HWND)param,"Thread1had access","Thread1",MB_OK); return 0;}UINT ThreadProc2(LPVOID param){ CSingleLocksingelLock(semaphore); (); Sleep(10000); ::MessageBox((HWND)param,"Thread2had access","Thread2",MB_OK); return 0;}

UINT ThreadProc3(LPVOID param){ CsingleLocksingelLock(semaphore); (); Sleep(10000); ::MessageBox((HWND)param,"Thread3had access","Thread3",MB_OK); return 0;}

  ⼆、 编程步骤

  1、 启动Visual C++6.0,⽣成⼀个32位的控制台程序,将该程序命名为"sequence"

  2、 输⼊要排续的数字,声明四个⼦线程;

  3、 输⼊代码,编译运⾏程序。三、 程序代码

//// : Defines the entry pointfor the console application./*主要⽤到的WINAPI线程控制函数,有关详细说明请查看MSDN;线程建⽴函数:HANDLE CreateThread( LPSECURITY_ATTRIBUTESlpThreadAttributes, // 安全属性结构指针,可为NULL; DWORDdwStackSize, // 线程栈⼤⼩,若为0表⽰使⽤默认值; LPTHREAD_START_ROUTINElpStartAddress, // 指向线程函数的指针; LPVOID lpParameter,// 传递给线程函数的参数,可以保存⼀个指针值; DWORDdwCreationFlags, // 线程建⽴是的初始标记,运⾏或挂起; LPDWORDlpThreadId // 指向接收线程号的DWORD变量;); 对临界资源控制的多线程控制的信号函数:

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

HANDLE CreateMutex( LPSECURITY_ATTRIBUTESlpMutexAttributes, // 安全属性结构指针,可为NULL BOOLbInitialOwner, // 当前建⽴互斥量是否占有该互斥量TRUE表⽰占有, //这样其他线程就不能获得此互斥量也就⽆法进⼊由 //该互斥量控制的临界区。FALSE表⽰不占有该互斥量 LPCTSTRlpName // 信号量的名称,字符数不可多于MAX_PATH如果 //遇到同名的其他信号量函数就会失败, //如果遇到同类信号同名也要注意变化;);

//初始化临界区信号,使⽤前必须先初始化VOID InitializeCriticalSection( LPCRITICAL_SECTIONlpCriticalSection // 临界区变量指针);

//阻塞函数//如果等待的信号量不可⽤,那么线程就会挂起,直到信号可⽤//线程才会被唤醒,该函数会⾃动修改信号,如Event,线程被唤醒之后//Event信号会变得⽆信号,Mutex、Semaphore等也会变。DWORD WaitForSingleObject( HANDLEhHandle, // 等待对象的句柄 DWORDdwMilliseconds // 等待毫秒数,INFINITE表⽰⽆限等待);//如果要等待多个信号可以使⽤WaitForMutipleObject函数*/

#include "stdafx.h"#include "stdlib.h"#include "memory.h"HANDLE evtTerminate; //事件信号,标记是否所有⼦线程都执⾏完/*下⾯使⽤了三种控制⽅法,你可以注释其中两种,使⽤其中⼀种。注意修改时要连带修改临界区PrintResult⾥的相应控制语句*/HANDLE evtPrint; //事件信号,标记事件是否已发⽣//CRITICAL_SECTION csPrint; //临界区//HANDLE mtxPrint; //互斥信号,如有信号表明已经有线程进⼊临界区并拥有此信号static long ThreadCompleted = 0;/*⽤来标记四个⼦线程中已完成线程的个数,当⼀个⼦线程完成时就对ThreadCompleted进⾏加⼀操作, 要使⽤InterlockedIncrement(long*lpAddend)和InterlockedDecrement(long*lpAddend)进⾏加减操作*/

//下⾯的结构是⽤于传送排序的数据给各个排序⼦线程struct MySafeArray{ long* data; int iLength;};

//打印每⼀个线程的排序结果void PrintResult(long* Array, int iLength,const char* HeadStr = "sort");

//排序函数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 _stdcallMyThreadFun(void* pParam);handleRet = CreateThread(NULL, 0,&CThreadTestDlg::MyThreadFun, NULL, 0, &ThreadID);之所以要声明为static是由于,该函数必须要独⽴于对象实例来使⽤,即使没有声明实例也可以使⽤。*/

int QuickSort(long* Array, int iLow, intiHigh); //快速排序

int main(int argc, char* argv[]){ long data[] ={123,34,546,754,34,74,3,56}; int iDataLen= 8; //为了对各个⼦线程分别对原始数据进⾏排序和保存排序结果 //分别分配内存对data数组的数据进⾏复制 long *data1,*data2, *data3, *data4, *data5; MySafeArrayStructData1, StructData2, StructData3, StructData4; data1 = newlong[iDataLen]; memcpy(data1,data, iDataLen << 2); //把data中的数据复制到data1中 //内存复制 memcpy(⽬标内存指针, 源内存指针, 复制字节数), 因为long的长度 //为4字节,所以复制的字节数为iDataLen<< 2, 即等于iDataLen*4 = data1; h= iDataLen; data2 = newlong[iDataLen]; memcpy(data2,data, iDataLen << 2); = data2; h= iDataLen; data3 = newlong[iDataLen]; memcpy(data3,data, iDataLen << 2); = data3; h= iDataLen; data4 = newlong[iDataLen]; memcpy(data4,data, iDataLen << 2); = data4; h= iDataLen; data5 = newlong[iDataLen]; memcpy(data5,data, iDataLen << 2); unsigned longTID1, TID2, TID3, TID4; //对信号量进⾏初始化 evtTerminate= CreateEvent(NULL, FALSE, FALSE, "Terminate"); evtPrint =CreateEvent(NULL, FALSE, TRUE, "PrintResult"); //分别建⽴各个⼦线程 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[]data1; delete[]data2; delete[]data3; delete[]data4; CloseHandle(evtPrint); return 0;}

/*冒泡排序思想(升序,降序同理,后⾯的算法⼀样都是升序):从头到尾对数据进⾏两两⽐较进⾏交换,⼩的放前⼤的放后。这样⼀次下来,最⼤的元素就会被交换的最后,然后下⼀次循环就不⽤对最后⼀个元素进⾏⽐较交换了,所以呢每⼀次⽐较交换的次数都⽐上⼀次循环的次数少⼀,这样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;}

/*选择排序思想:每⼀次都从⽆序的数据中找出最⼩的元素,然后和前⾯已经有序的元素序列的后⼀个元素进⾏交换,这样整个源序列就会分成两部分,前⾯⼀部分是已经排好序的有序序列,后⾯⼀部分是⽆序的,⽤于选出最⼩的元素。循环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;}

/*堆排序思想:堆:数据元素从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;ii; 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;}

/*插⼊排序思想:把源数据序列看成两半,前⾯⼀半是有序的,后⾯⼀半是⽆序的,把⽆序的数据从头到尾逐个逐个的插⼊到前⾯的有序数据中,使得有序的数据的个数不断增⼤,同时⽆序的数据个数就越来越少,最后所有元素都会变得有序。*/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;i0; 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;}

/*快速排序思想:快速排序是分治思想的⼀种应⽤,它先选取⼀个⽀点,然后把⼩于⽀点的元素交换到⽀点的前边,把⼤于⽀点的元素交换到⽀点的右边。然后再对⽀点左边部分和右边部分进⾏同样的处理,这样若⼲次之后,数据就会变得有序。下⾯的实现使⽤了递归建⽴两个游标:iLow,iHigh;iLow指向序列的第⼀个元素,iHigh指向最后⼀个先选第⼀个元素作为⽀点,并把它的值存贮在⼀个辅助变量⾥。那么第⼀个位置就变为空并可以放置其他的元素。这样从iHigh指向的元素开始向前移动游标,iHigh查找⽐⽀点⼩的元素,如果找到,则把它放置到空置了的位置(现在是第⼀个位置),然后iHigh游标停⽌移动,这时iHigh指向的位置被空置,然后移动iLow游标寻找⽐⽀点⼤的元素放置到iHigh指向的空置的位置,如此往复直到iLow与iHigh相等。最后使⽤递归对左右两部分进⾏同样处理*/

int QuickSort(long* Array, int iLow, intiHigh){ 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;}

//每⼀个线程都要使⽤这个函数进⾏输出,⽽且只有⼀个显⽰器,产⽣多个线程//竞争对控制台的使⽤权。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

发布者:admin,转转请注明出处:http://www.yc00.com/news/1687426579a9201.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信