多线程编程基础知识

2024-09-25

多线程编程基础知识(精选7篇)

多线程编程基础知识 篇1

多线程编程

一、问题的提出

1.1问题的引出

编写一个耗时的单线程程序:

新建一个基于对话框的应用程序SingleThread,在主对话框IDD_SINGLETHREAD_DIALOG添加一个按钮,ID为IDC_SLEEP_SIX_SECOND,标题为“延时6秒”,添加按钮的响应函数,代码如下:

void CSingleThreadDlg::OnSleepSixSecond(){ Sleep(6000);//延时6秒 } 编译并运行应用程序,单击“延时6秒”按钮,你就会发现在这6秒期间程序就象“死机”一样,不在响应其它消息。为了更好地处理这种耗时的操作,我们有必要学习——多线程编程。

1.2多线程概述

进程和线程都是操作系统的概念。进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。

线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程,主执行线程以函数地址形式,比如说main或WinMain函数,将程序的启动点提供给Windows系统。主执行线程终止了,进程也就随之终止。

每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。一个进程中的所有线程都在该进程的虚拟地址空间中,共同使用这些虚拟地址空间、全局变量和系统资源,所以线程间的通讯非常方便,多线程技术的应用也较为广泛。

多线程可以实现并行处理,避免了某项任务长时间占用CPU时间。要说明的一点是,对于单处理器(CPU)的,为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮换方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。由此可见,如果两个非常活跃的线程为了抢夺对CPU的控制权,在线程切换时会消耗很多的CPU资源,反而会降低系统的性能。这一点在多线程编程时应该注意。

Win32 SDK函数支持进行多线程的程序设计,并提供了操作系统原理中的各种同步、互斥和临界区等操作。Visual C++中,使用MFC类库也实现了多线程的程序设计,使得多线程编程更加方便。1.3 Win32 API对多线程编程的支持

Win32 提供了一系列的API函数来完成线程的创建、挂起、恢复、终结以及通信等工作。下面将选取其中的一些重要函数进行说明。

1、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)ThreadFunc,ThreadFunc 是线程函数名;

lpParameter:指定了线程执行时传送给线程的32位参数,即线程函数的参数;

dwCreationFlags:控制线程创建的附加标志,可以取两种值。如果该参数为0,线程在被创建后就会立即开始执行;如果该参数为CREATE_SUSPENDED,则系统产生线程后,该线程处于挂起状态,并不马上执行,直至函数ResumeThread被调用;

lpThreadId:该参数返回所创建线程的ID;

如果创建成功则返回线程的句柄,否则返回NULL。

2、DWORD SuspendThread(HANDLE hThread);该函数用于挂起指定的线程,如果函数执行成功,则线程的执行被终止。

3、DWORD ResumeThread(HANDLE hThread);该函数用于结束线程的挂起状态,执行线程。

4、VOID ExitThread(DWORD dwExitCode);该函数用于线程终结自身的执行,主要在线程的执行函数中被调用。其中参数dwExitCode用来设置线程的退出码。

5、BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);

一般情况下,线程运行结束之后,线程函数正常返回,但是应用程序可以调用TerminateThread强行终止某一线程的执行。各参数含义如下: hThread:将被终结的线程的句柄;

dwExitCode:用于指定线程的退出码。

使用TerminateThread()终止某个线程的执行是不安全的,可能会引起系统不稳定;虽然该函数立即终止线程的执行,但并不释放线程所占用的资源。因此,一般不建议使用该函数。

6、BOOL PostThreadMessage(DWORD idThread,UINT Msg,WPARAM wParam,LPARAM lParam);该函数将一条消息放入到指定线程的消息队列中,并且不等到消息被该线程处理时便返回。idThread:将接收消息的线程的ID;

Msg:指定用来发送的消息;

wParam:同消息有关的字参数;

lParam:同消息有关的长参数;

调用该函数时,如果即将接收消息的线程没有创建消息循环,则该函数执行失败。

1.4.Win32 API多线程编程例程

例程1 [MultiThread1] 一个简单的线程。注意事项:

 Volatile:关键字:

volatile是要求C++编译器不要自作聪明的把变量缓冲在寄存器里.因为该变量可能会被意外的修改。(多个线程或其他原因)

如从串口读数据的场合,把变量缓冲在寄存器里,下次去读寄存器就没有意义了.因为串口的数据可能随时会改变的.加锁访问用于多个线程的场合.在进入临界区时是肯定要加锁的.volatile也加上,以保证从内存中读取变量的值. 终止线程:

Windows终止线程运行的四种方法 终止线程运行

若要终止线程的运行,可以使用下面的方法:

• 线程函数返回(最好使用这种方法)。

• 通过调用 ExitThread 函数,线程将自行撤消(最好不要使用这种方法)。

• 同一个进程或另一个进程中的线程调用 TerminateThread 函数(应该避免使用这种方法)。

• 包含线程的进程终止运行(应该避免使用这种方法)。

下面将介绍终止线程运行的方法,并且说明线程终止运行时会出现什么情况。

 线程函数返回

始终都应该将线程设计成这样的形式,即当想要线程终止运行时,它们就能够返回。这是确保所有线程资源被正确地清除的唯一办法。

如果线程能够返回,就可以确保下列事项的实现:

• 在线程函数中创建的所有 C++ 对象均将通过它们的撤消函数正确地撤消。

• 操作系统将正确地释放线程堆栈使用的内存。

• 系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。

• 系统将递减线程内核对象的使用计数。 使用 ExitThread 函数

可以让线程调用 ExitThread 函数,以便强制线程终止运行:

VOID ExitThread(DWORD dwExitCode);

该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,C++ 资源(如 C++ 类对象)将不被撤消。由于这个原因,最好从线程函数返回,而不是通过调用 ExitThread 来返回。

当然,可以使用 ExitThread 的 dwExitThread 参数告诉系统将线程的退出代码设置为什么。ExitThread 函数并不返回任何值,因为线程已经终止运行,不能执行更多的代码。 使用 TerminateThread 函数

调用 TerminateThread 函数也能够终止线程的运行:

BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);

与 ExitThread 不同,ExitThread 总是撤消调用的线程,而 TerminateThread 能够撤消任何线程。hThread 参数用于标识被终止运行的线程的句柄。当线程终止运行时,它的退出代码成为你作为 dwExitCode 参数传递的值。同时,线程的内核对象的使用计数也被递减。

注意 TerminateThread 函数是异步运行的函数,也就是说,它告诉系统你想要线程终止运行,但是,当函数返回时,不能保证线程被撤消。如果需要确切地知道该线程已经终止运行,必须调用 WaitForSingleObject 或者类似的函数,传递线程的句柄。

设计良好的应用程序从来不使用这个函数,因为被终止运行的线程收不到它被撤消的通知。线程不能正确地清除,并且不能防止自己被撤消。

注意 当使用返回或调用 ExitThread 的方法撤消线程时,该线程的内存堆栈也被撤消。但是,如果使用 TerminateThread,那么在拥有线程的进程终止运行之前,系统不撤消该线程的堆栈。Microsoft故意用这种方法来实现 TerminateThread。如果其他仍然正在执行的线程要引用强制撤消的线程堆栈上的值,那么其他的线程就会出现访问违规的问题。如果将已经撤消的线程的堆栈留在内存中,那么其他线程就可以继续很好地运行。

此外,当线程终止运行时,DLL 通常接收通知。如果使用 TerminateThread 强迫线程终止,DLL 就不接收通知,这能阻止适当的清除,在进程终止运行时撤消线程。当线程终止运行时,会发生下列操作:

• 线程拥有的所有用户对象均被释放。在 Windows 中,大多数对象是由包含创建这些对象的线程的进程拥有的。但是一个线程拥有两个用户对象,即窗口和挂钩。当线程终止运行时,系统会自动撤消任何窗口,并且卸载线程创建的或安装的任何挂钩。其他对象只有在拥有线程的进程终止运行时才被撤消。

• 线程的退出代码从 STILL_ACTIVE 改为传递给 ExitThread 或 TerminateThread 的代码。

• 线程内核对象的状态变为已通知。

• 如果线程是进程中最后一个活动线程,系统也将进程视为已经终止运行。

• 线程内核对象的使用计数递减 1。

当一个线程终止运行时,在与它相关联的线程内核对象的所有未结束的引用关闭之前,该内核对象不会自动被释放。

一旦线程不再运行,系统中就没有别的线程能够处理该线程的句柄。然而别的线程可以调用 GetExitcodeThread 来检查由 hThread 标识的线程是否已经终止运行。如果它已经终止运行,则确定它的退出代码:

BOOL GetExitCodeThread(HANDLE hThread, PDOWRD pdwExitCode);退出代码的值在 pdwExitCode 指向的 DWORD 中返回。如果调用 GetExitCodeThread 时线程尚未终止运行,该函数就用 STILL_ACTIVE 标识符(定义为 0x103)填入 DWORD。如果该函数运行成功,便返回 TRUE。

 线程的定义:

例程2[MultiThread2] 传送一个一个整型的参数到一个线程中,以及如何等待一个线程完成处理。

DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);

hHandle:为要监视的对象(一般为同步对象,也可以是线程)的句柄;

dwMilliseconds:为hHandle对象所设置的超时值,单位为毫秒;

当在某一线程中调用该函数时,线程暂时挂起,系统监视hHandle所指向的对象的状态。如果在挂起的dwMilliseconds毫秒内,线程所等待的对象变为有信号状态,则该函数立即返回;如果超时时间已经到达dwMilliseconds毫秒,但hHandle所指向的对象还没有变成有信号状态,函数照样返回。参数dwMilliseconds有两个具有特殊意义的值:0和INFINITE。若为0,则该函数立即返回;若为INFINITE,则线程一直被挂起,直到hHandle所指向的对象变为有信号状态时为止。

例程3[MultiThread3] 传送一个结构体给一个线程函数,可以通过传送一个指向结构体的指针参数来完成。补充一点:如果你在void CMultiThread3Dlg::OnStart()函数中添加/* */语句,编译运行你就会发现进度条不进行刷新,主线程也停止了反应。什么原因呢?这是因为WaitForSingleObject函数等待子线程(ThreadFunc)结束时,导致了线程死锁。因为WaitForSingleObject函数会将主线程挂起(任何消息都得不到处理),而子线程ThreadFunc正在设置进度条,一直在等待主线程将刷新消息处理完毕返回才会检测通知事件。这样两个线程都在互相等待,死锁发生了,编程时应注意避免。

例程4[MultiThread4] 测试在Windows下最多可创建线程的数目。

二、MFC中的多线程开发

2.1 MFC对多线程编程的支持

MFC中有两类线程,分别称之为工作者线程和用户界面线程。二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环。

工作者线程没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的后台打印等。用户界面线程一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消息等。但对于Win32的API编程而言,这两种线程是没有区别的,它们都只需线程的启动地址即可启动线程来执行任务。

在MFC中,一般用全局函数AfxBeginThread()来创建并初始化一个线程的运行,该函数有两种重载形式,分别用于创建工作者线程和用户界面线程。两种重载函数原型和参数分别说明如下:

(1)CWinThread* AfxBeginThread(AFX_THREADPROC pfnThreadProc,LPVOID pParam,nPriority=THREAD_PRIORITY_NORMAL,UINT nStackSize=0,DWORD dwCreateFlags=0,LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL);

PfnThreadProc:指向工作者线程的执行函数的指针,线程函数原型必须声明如下: UINT ExecutingFunction(LPVOID pParam);请注意,ExecutingFunction()应返回一个UINT类型的值,用以指明该函数结束的原因。一般情况下,返回0表明执行成功。

pParam:传递给线程函数的一个32位参数,执行函数将用某种方式解释该值。它可以是数值,或是指向一个结构的指针,甚至可以被忽略;

nPriority:线程的优先级。如果为0,则线程与其父线程具有相同的优先级;

nStackSize:线程为自己分配堆栈的大小,其单位为字节。如果nStackSize被设为0,则线程的堆栈被设置成与父线程堆栈相同大小; dwCreateFlags:如果为0,则线程在创建后立刻开始执行。如果为CREATE_SUSPEND,则线程在创建后立刻被挂起;

lpSecurityAttrs:线程的安全属性指针,一般为NULL;

(2)CWinThread* AfxBeginThread(CRuntimeClass* pThreadClass,int nPriority=THREAD_PRIORITY_NORMAL,UINT nStackSize=0,DWORD dwCreateFlags=0,LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL);

pThreadClass 是指向 CWinThread 的一个导出类的运行时类对象的指针,该导出类定义了被创建的用户界面线程的启动、退出等;其它参数的意义同形式1。使用函数的这个原型生成的线程也有消息机制,在以后的例子中我们将发现同主线程的机制几乎一样。下面对CWinThread类的数据成员及常用函数进行简要说明。

   m_hThread:当前线程的句柄;

m_nThreadID:当前线程的ID;

m_pMainWnd:指向应用程序主窗口的指针

virtual BOOL CWinThread::InitInstance();重载该函数以控制用户界面线程实例的初始化。初始化成功则返回非0值,否则返回0。用户界面线程经常重载该函数,工作者线程一般不使用InitInstance()。

virtual int CWinThread::ExitInstance();在线程终结前重载该函数进行一些必要的清理工作。该函数返回线程的退出码,0表示执行成功,非0值用来标识各种错误。同InitInstance()成员函数一样,该函数也只适用于用户界面线程。

2.2 MFC多线程编程实例

例程5 MultiThread5 为了与Win32 API对照,使用MFC 类库编程实现例程3 MultiThread3。

例程6 MultiThread6[用户界面线程]  创建用户界面线程的步骤:

1.使用ClassWizard创建类CWinThread的派生类(以CUIThread类为例)class CUIThread : public CWinThread { DECLARE_DYNCREATE(CUIThread)protected: CUIThread();// protected constructor used by dynamic creation

// Attributes public: // Operations public:

// Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CUIThread)public: virtual BOOL InitInstance();virtual int ExitInstance();//}}AFX_VIRTUAL // Implementation protected: virtual ~CUIThread();// Generated message map functions //{{AFX_MSG(CUIThread)

// NOTE-the ClassWizard will add and remove member functions here.//}}AFX_MSG

DECLARE_MESSAGE_MAP()};

2.重载函数InitInstance()和ExitInstance()。BOOL CUIThread::InitInstance(){ CFrameWnd* wnd=new CFrameWnd;wnd->Create(NULL,“UI Thread Window”);wnd->ShowWindow(SW_SHOW);wnd->UpdateWindow();m_pMainWnd=wnd;return TRUE;}

3.创建新的用户界面线程 void CUIThreadDlg::OnButton1(){

}

请注意以下两点:

A、在UIThreadDlg.cpp的开头加入语句: #include “UIThread.h” B、把UIThread.h中类CUIThread()的构造函数的特性由 protected 改为 public。CUIThread* pThread=new CUIThread();pThread->CreateThread();

用户界面线程的执行次序与应用程序主线程相同,首先调用用户界面线程类的InitInstance()函数,如果返回TRUE,继续调用线程的Run()函数,该函数的作用是运行一个标准的消息循环,并且当收到WM_QUIT消息后中断,在消息循环过程中,Run()函数检测到线程空闲时(没有消息),也将调用OnIdle()函数,最后Run()函数返回,MFC调用ExitInstance()函数清理资源。

你可以创建一个没有界面而有消息循环的线程,例如:你可以从CWinThread派生一个新类,在InitInstance函数中完成某项任务并返回FALSE,这表示仅执行InitInstance函数中的任务而不执行消息循环,你可以通过这种方法,完成一个工作者线程的功能。

三、线程间通讯

3.1通讯方式

一般而言,应用程序中的一个次要线程总是为主线程执行特定的任务,这样,主线程和次要线程间必定有一个信息传递的渠道,也就是主线程和次要线程间要进行通信。这种线程间的通信不但是难以避免的,而且在多线程编程中也是复杂和频繁的,下面将进行说明。

3.1.1使用全局变量进行通信

由于属于同一个进程的各个线程共享操作系统分配该进程的资源,故解决线程间通信最简单的一种方法是使用全局变量。对于标准类型的全局变量,建议使用volatile 修饰符,它告诉编译器无需对该变量作任何的优化,即无需将它放到一个寄存器中,并且该值可被外部改变。如果线程间所需传递的信息较复杂,可以定义一个结构,通过传递指向该结构的指针进行传递信息。

3.1.2使用自定义消息

可以在一个线程的执行函数中向另一个线程发送自定义的消息来达到通信的目的。一个线程向另外一个线程发送消息是通过操作系统实现的。利用Windows操作系统的消息驱动机制,当一个线程发出一条消息时,操作系统首先接收到该消息,然后把该消息转发给目标线程,接收消息的线程必须已经建立了消息循环。

3.2例程

例程GlobalObjectTest 该例程演示了如何利用全局变量进行通信

例程7[MultiThread7] 该例程演示了如何使用自定义消息进行线程间通信。首先,主线程向CCalculateThread线程发送消息WM_CALCULATE,CCalculateThread线程收到消息后进行计算,再向主线程发送WM_DISPLAY消息,主线程收到该消息后显示计算结果。步骤:

四、线程的同步

4.1基本概念

虽然多线程能给我们带来好处,但是也有不少问题需要解决。例如,对于像磁盘驱动器这样独占性系统资源,由于线程可以执行进程的任何代码段,且线程的运行是由系统调度自动完成的,具有一定的不确定性,因此就有可能出现两个线程同时对磁盘驱动器进行操作,从而出现操作错误;又例如,对于银行系统的计算机来说,可能使用一个线程来更新其用户数据库,而用另外一个线程来读取数据库以响应储户的需要,极有可能读数据库的线程读取的是未完全更新的数据库,因为可能在读的时候只有一部分数据被更新过。

使隶属于同一进程的各线程协调一致地工作称为线程的同步。MFC提供了多种同步对象,下面只介绍最常用的四种:

临界区(CCriticalSection)

事件(CEvent)

互斥量(CMutex)

信号量(CSemaphore)

通过这些类,可以比较容易地做到线程同步。

4.2使用 CCriticalSection 类

当多个线程访问一个独占性共享资源时,可以使用“临界区”对象。任一时刻只有一个线程可以拥有临界区对象,拥有临界区的线程可以访问被保护起来的资源或代码段,其他希望进入临界区的线程将被挂起等待,直到拥有临界区的线程放弃临界区时为止,这样就保证了不会在同一时刻出现多个线程访问共享资源。

CCriticalSection类的用法非常简单,步骤如下:

1.定义CCriticalSection类的一个全局对象(以使各个线程均能访问),如CCriticalSection critical_section;

2.在访问需要保护的资源或代码之前,调用CCriticalSection类的成员Lock()获得临界区对象: critical_section.Lock();3.在线程中调用该函数来使线程获得它所请求的临界区。如果此时没有其它线程占有临界区对象,则调用Lock()的线程获得临界区;否则,线程将被挂起,并放入到一个系统队列中等待,直到当前拥有临界区的线程释放了临界区时为止。

4.访问临界区完毕后,使用CCriticalSection的成员函数Unlock()来释放临界区:critical_section.Unlock();通俗讲,就是线程A执行到critical_section.Lock();语句时,如果其它线程(B)正在执行critical_section.Lock();语句后且critical_section.Unlock();语句前的语句时,线程A就会等待,直到线程B执行完critical_section.Unlock();语句,线程A才会继续执行。

例程8 MultiThread8 4.3使用 CEvent 类

CEvent 类提供了对事件的支持。事件是一个允许一个线程在某种情况发生时,唤醒另外一个线程的同步对象。例如在某些网络应用程序中,一个线程(记为A)负责监听通讯端口,另外一个线程(记为B)负责更新用户数据。通过使用CEvent 类,线程A可以通知线程B何时更新用户数据。每一个CEvent 对象可以有两种状态:有信号状态和无信号状态。线程监视位于其中的CEvent 类对象的状态,并在相应的时候采取相应的操作。

在MFC中,CEvent 类对象有两种类型:人工事件和自动事件。一个自动CEvent 对象在被至少一个线程释放后会自动返回到无信号状态;而人工事件对象获得信号后,释放可利用线程,但直到调用成员函数ReSetEvent()才将其设置为无信号状态。在创建CEvent 类的对象时,默认创建的是自动事件。CEvent 类的各成员函数的原型和参数说明如下:

1、CEvent(BOOL bInitiallyOwn=FALSE,BOOL bManualReset=FALSE,LPCTSTR lpszName=NULL,LPSECURITY_ATTRIBUTES lpsaAttribute=NULL);bInitiallyOwn:指定事件对象初始化状态,TRUE为有信号,FALSE为无信号;

bManualReset:指定要创建的事件是属于人工事件还是自动事件。TRUE为人工事件,FALSE为自动事件;

后两个参数一般设为NULL,在此不作过多说明。

2、BOOL CEvent::SetEvent();

将 CEvent 类对象的状态设置为有信号状态。如果事件是人工事件,则 CEvent 类对象保持为有信号状态,直到调用成员函数ResetEvent()将 其重新设为无信号状态时为止。如果CEvent 类对象为自动事件,则在SetEvent()将事件设置为有信号状态后,CEvent 类对象由系统自动重置为无信号状态。

如果该函数执行成功,则返回非零值,否则返回零。

3、BOOL CEvent::ResetEvent();

该函数将事件的状态设置为无信号状态,并保持该状态直至SetEvent()被调用时为止。由于自动事件是由系统自动重置,故自动事件不需要调用该函数。如果该函数执行成功,返回非零值,否则返回零。一般通过调用WaitForSingleObject函数来监视事件状态。前面已经介绍了该函数。由于语言描述的原因,CEvent 类的理解确实有些难度,只要通过下面例程,多看几遍就可理解。例程9 MultiThread9 仔细分析这两个线程函数, 就会正确理解CEvent 类。线程WriteD执行到 WaitForSingleObject(eventWriteD.m_hObject,INFINITE);处等待,直到事件eventWriteD为有信号该线程才往下执行,因为eventWriteD对象是自动事件,则当WaitForSingleObject()返回时,系统自动把eventWriteD对象重置为无信号状态。

4.4使用CMutex 类

互斥对象与临界区对象很像.互斥对象与临界区对象的不同在于:互斥对象可以在进程间使用,而临界区对象只能在同一进程的各线程间使用。当然,互斥对象也可以用于同一进程的各个线程间,但是在这种情况下,使用临界区会更节省系统资源,更有效率。

4.5使用CSemaphore 类

当需要一个计数器来限制可以使用某个线程的数目时,可以使用“信号量”对象。CSemaphore 类的对象保存了对当前访问某一指定资源的线程的计数值,该计数值是当前还可以使用该资源的线程的数目。如果这个计数达到了零,则所有对这个CSemaphore 类对象所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零时为止。一个线程被释放已访问了被保护的资源时,计数值减1;一个线程完成了对被控共享资源的访问时,计数值增1。这个被CSemaphore 类对象所控制的资源可以同时接受访问的最大线程数在该对象的构建函数中指定。

CSemaphore 类的构造函数原型及参数说明如下:

CSemaphore(LONG lInitialCount=1,LONG lMaxCount=1,LPCTSTR pstrName=NULL,LPSECURITY_ATTRIBUTES lpsaAttributes=NULL);lInitialCount:信号量对象的初始计数值,即可访问线程数目的初始值;

lMaxCount:信号量对象计数值的最大值,该参数决定了同一时刻可访问由信号量保护的资源的线程最大数目;

后两个参数在同一进程中使用一般为NULL,不作过多讨论;

在用CSemaphore 类的构造函数创建信号量对象时要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时,则说明当前占用资源的线程数已经达到了所允许的最大数目,不能再允许其它线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源数加1。例程10 MultiThread10 为了文件中能够正确使用同步类,在文件开头添加: #include “afxmt.h” 定义信号量对象和一个字符数组,为了能够在不同线程间使用,定义为全局变量:CSemaphore semaphoreWrite(2,2);//资源最多访问线程2个,当前可访问线程数2个

在信号量对象有信号的状态下,线程执行到WaitForSingleObject语句处继续执行,同时可用线程数减1;若线程执行到WaitForSingleObject语句时信号量对象无信号,线程就在这里等待,直到信号量对象有信号线程才往下执行。

多线程编程基础知识 篇2

线程技术(thread)早在20世纪60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期。线程就是程序中的单个顺序控制流。利用多线程进行程序设计,就是将一个程序(进程)的任务划分为执行的多个部分(线程),每一个线程为一个顺序的单控制流,而所有线程都是并发执行的,这样,多线程程序就可以实现并行计算,高效利用多处理器。线程可分为用户级线程和内核级线程两种基本类型。用户级线程不需要内核支持,可以在用户程序中实现,线程调度、同步与互斥都需要用户程序自己完成。

2 多线程的优点

和进程相比,多线程的优点之一是,它是一种非常“节俭”的多任务操作方式。我们知道,在Linux系统下,激活一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种“昂贵”的多任务工作方式。而运行一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,激活一个线程所花费的空间远远小于激活一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

多线程的优点之二是线程之间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其它一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

2.1 提高应用程序响应。

这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置一个新的线程,可以避免这种尴尬的情况。

2.2 使多CPU系统更加有效。

操作系统会保证当线程数目不大于CPU数目时,不同的线程运行不同的CPU上。

2.3 改善程序结构。

一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会便于理解和修改。

3 线程库的主要函数调用

Linux操作系统提供了Linux Threads库,它是符合POSIX1003.1c标准的内核级多线程函数库。在linuxthreads库中提供了一些多线程编程的关键函数,在多线程编程时应包括pthread.h文件,连接时需要使用库libpthread.a。

3.1 线程的创建和终止

int pthread_create(pthread_t*pthread,const pthread_attr_t*attr,void*(*start_routine(*void)),void*arg);调用此函数可以创建一个新的线程,新线程创建后执行start_routine指定的程序。其中参数attr是用户希望创建线程的属性,当为NULL时表示以默认的属性创建线程。arg是向start_routine传递的参数。当成功创建一个新的线程时,系统会自动为新线程分配一个线程ID号,并通过pthread返回给调用者。

void pthread_exit(void*value_ptr);调用该函数可以退出线程,参数value_ptr是一个指向返回状态值的指针。

3.2 线程的管理

pthread_self(void);为了区分线程,在线程创建时系统为其分配一个唯一的ID号,由pthread_create()返回给调用者,也可以通过pthread_self()获取自己的线程ID。

int pthread_join(pthread-t thread,void**status);这个函数的作用是等待一个线程的结束。调用pthread_join()的线程将被挂起直到线程ID为参数thread指定的线程终止。

int pthread_detach(pthread_t pthread);参数pthread代表的线程一旦终止,立即释放掉该线程占有的所有资源。

3.3 线程的互斥

互斥量和临界区类似,只有拥有互斥量的线程才具有访问资源的权限,由于互斥对象只有一个,这就决定了任何情况下共享资源(代码或变量)都不会被多个线程同时访问。使用互斥不仅能够在同一应用程序的不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。

pthread_mutex_init(pthread_mutex_t*mutex,const pthread_mutexattr_t*attr);初使化一个互斥体变量mutex,参数attr表示按照attr属性创建互斥体变量mutex,如果参数attr为NULL,则以默认的方式创建。

pthread_mutex_lock(pthread_mutex_t*mutex);给一个互斥体变量上锁,如果mutex指定的互斥体已经被锁住,则调用线程将被阻塞直到拥有mutex的线程对mutex解锁为止。

pthread_mutex_unlock(pthread_mutex_t*mutex);对参数mutex指定的互斥体变量解锁。

3.4 线程的同步

同步就是线程等待某一个事件的发生,当等待的事件发生时,被等待的线程和事件一起继续执行。如果等待的事件未到达则挂起。在linux操作系统中是通过条件变量来实现同步的。

pthread_cond_init(pthread_cond_t*cond,const pthread_cond_t*attr);这个函数按参数attr指定的属性初使化一个条件变量cond。

pthread_cond_wait(pthread_cond_t*cond,pthread_mutex_t*mutex);等待一个事件(条件变量)的发生,发出调用的线程自动阻塞,直到相应的条件变量被置1。等待状态的线程不占用CPU时间。

pthread_cond_signal(pthread_cond_t*con d);解除一个等待参数cond指定的条件变量的线程的阻塞状态。

4 线程的调度

调度是用来管理任务执行顺序的方法,调度的方法有多种多样,常见的有如下方法:

先来先服务:首先请求服务的,首先处理(也称为FIFO测略);

最短作业优先:需要最少处理工作量的任务首先被处理;

基于优先级的调度:拥有较高优先级的任务最先执行;

轮转算法(round-robin):每个任务得到一个小的时间片,任务处于环形队列,依次被执行,直到任务结束。

类似于进程,线程也可以处于几个状态之一。图1显示了一个简化的线程状态图,一个线程可以处于运行态、就绪态、阻塞态。

一个线程可以是绑定的或非绑定的,如果线程是绑定的,它的调度策略就由内核调度算法决定,如果线程是非绑定的,则由线程库调度者决定那个线程的活动。Linux下线程的调度受三个因素的影响。首先可以通过pthread_attr_setscope库函数来设置contentionscope属性,将一个POSIX线程定义为绑定或非绑定类型,如果PTHREAD_SCOPE_SYSTEM被设置,则线程与一个LWP一一对应,如果PTHREAD_SCOPE_PROCESS被设置,则线程可通过LWP池中的LWP与同一个进程内的其他线程竞争使用系统资源。第二个因素是调度策略。POSIX为线程提供了三种调度策略:

4.1 SCHED_OTHER:

系统默认策略。常使用的是时间片划分策略,竞争的线程按其优先级得到时间片;

4.2 SCHED_FIFO:

先进先出的调度策略,具有最高优先级的等待时间最长的线程将成为下一个执行的线程;

4.3 SCHED_RR:轮转调度。

第三个影响调度的因素是优先级,处理POSIX线程时具有较高优先级的线程将在优先级低的线程之前调度。对一个已存在的线程的调度策略及其优先级,可以通过使用pthread_setshedparam库函数来设置。

5 多线程编程的应用实例

5.1 实例1

下面是通过创建两个线程来实现对一个数的递加的实例。

该实例主要是说明两个线程的互斥,在主函数中首先创建了thread1,thread1中对全局变量number++,这时number=0,之后创建了thread2,thread2同样对全局变量number++,而且两个线程在对number操作时都用了互斥锁,所以在一个线程对number操作时另外一个线程会停在pthread_mutex_lock(&mut)处,直到另外一个线程执行pthread_mutex_unlock(&mut)。需要说明的是,上面的两处sleep不光是为了演示的需要,也是为了让线程睡眠一段时间,让线程释放互斥锁,等待另一个线程使用此锁。

编译执行的结果是:

5.2 实例2

当count值为0时,decrement_count函数在pthread_cond_wait处被阻塞,并打开互斥锁count_lock.

调用到函数increment_count时,pthread_cond_signal()函数改变条件变量,告知decrement_count()停止阻塞。让两个线程分别运行这两个函数,可以实现线程间的同步。

5.3 多线程编程应注意事项

如何即时且干净地从线程中退出是多线程编程中的一个重要问题,下面我将主要就此问题谈谈见解:

5.3.1 通过调用pthread_exit()退出,这确实是一个干净利落的退出方式,打破了程序应有的单点退出的规则;并且在C++语言环境下他可能不会引发堆栈对象的顺利析构。

5.3.2 通过调用pthread_cancel来结束其它进程执行,虽然它能保证堆栈对象的顺利析构,但是由于取消点的不确定性、在临界区内退出而导致死锁(此问题可以通过调用pthread_cleanup_push和pthread_cleanup_pop解决)、未释放的系统资源(如文件或者套接字)等问题,致使它也并不是很好的问题解决之道。

5.3.3 通过return语句结束线程比较合理,推荐使用多线程程序虽然可以实现并行计算,高效利用多处理器,但这也给调试程序带来了很多麻烦,不过当前C-C++编译器都捆绑了线程调试器,例如Gun C编译器gcc自带了gdb,Solaris的cc-CC编译器自带了dbx,线程调试器会自动识别多线程代码,这些调试器可以用来在多线程程序中单步执行,并且检查互斥锁和TSD的值。为了可以调试程序,在编译时传入-g,这样可以防止编译器自动删除可执行文件中附加的符号表,例如:cc-g test.c-lpthread-o test。

当激活调试器时,需传入可执行文件的文件名。如dbx test。关于dbx的命令列表可参见dbx的帮助手册。

pthread_mutex_lock和pthread_cond_wait都明确表示不应该因为被信号中断而返回,那么他们可能永远阻塞吗?mutex不会,因为它只是将操作串行化,pthread_cond_wait可能会,这就需要我们在必要的时候用pthread_cond_broadcast唤醒所有阻塞在其上的线程。

6 结束语

Linux中基于POSIX标准的很好的支持了多线程技术,它减少了程序并发执行时的系统开销,提高了计算机的工作效率。在具体编程过程中要了解线程的间的关系,还要考虑共享数据的保护,在互斥和同步机制下保证代码的高效运行。

参考文献

[1]Linux系统分析与高级编程技术[M].北京:机械工业出版社,1999.

[2]Linux下的C编程[M].北京:人民邮电出版社,2001.

[3]Linux应用实例与技巧[M].北京:机械工业出版社,2001.

多线程编程基础知识 篇3

关键词:java;多线程;socket编程

中图分类号:TP311文献标识码:A文章编号:1009-3044(2007)15-30796-01

The Java Multi-threading Mechanism Weaves the Application in the Distance in the Socket

XU Xiao-long

(Dept.of Computer Science,Qufu Normal University,Rizhao 276826,China)

Abstract:This paper discusses the theory and usage of Java multithreading, especially how to use multithreading in socket programming, so that the server can response the requests of many clients at the same time.

Key words:java; multithreading; socket programming

随着分布式系统的兴起,并发多任务技术越来越重要。在现有的基于多线程的操作系统上开发并发多任务程序已成为程序设计的热点。Java是开发网络应用程序的热门技术之一,本文首先分析了Java多线程机制的特性,然后以Windows XP为平台,以JDK6.0为开发环境分析了Java的多线程机制在Socket编程中的应用。

1 线程的特点

以往所开发的程序多数是单线程的,即一个程序只有一条从头至尾的执行路线。然而现实世界的很多过程却需要多条途径同时运行,例如服务器需要同时处理多个客户的请求,多媒体程序需要对多种媒体并发控制等。

线程是指进程中单一顺序的控制流,又称为轻量级进程。每个进程可以启动几个线程,线程是最小的运行单位。操作系统的多任务特性使得线程之间独立运行,但是它们彼此共享存储空间,可能会同时操作同一内存地址。

2 Java对多线程的支持

2.1概述

Java是通过多线程机制来支持多任务和并行处理的,多线程编程是Java语言最重要的特征之一。多线程是指同时存在几个执行体,按几条不同的执行路线共同工作的情况。Java的多线程机制使得编程人员可以很方便地开发出具有多线程功能,能同时处理多个任务的功能强大的应用程序。

Java对多线程的支持表现为两个方面:一是它本身就是一个多线程体系。Java是基于多线程来执行任务的。例如,有若干个系统线程用于必要的“垃圾”回收,自动作存储器管理工作及其它系统维护工作。另外,Java内置了多线程控制机制,从而大大简化了多线程应用程序的开发。Thread类封装了所有有关线程的控制,负责线程的启动、运行、休眠、挂起、恢复、退出和终止等一般性的逻辑控制操作,并可检查线程的状态。除此之外,Thread类也实现了对线程行为的优先级的控制。由于CPU一次只能执行一个线程中的指令,因此对多线程体系而言,必须通过优先级决定线程之间的切换。

2.2线程的创建

Java是面向对象的程序语言,用Java进行程序设计就是设计和使用类,Java提供了Thread类来创建线程,线程就是Thread类或其子类的对象。

Thread thread1=new Thread();

thread1.start();

这样就创建了一个线程,并启动了该线程。启动线程就是启动线程的run()方法,而Thread类中的run()方法没有任何操作语句,所以要使线程实现预定功能,必须定义自己的run()方法。Java中通常有两种方式定义run()方法:

一是通过定义Thread类的子类,在该子类中重写run()方法。Thread子类的实例就是一个线程,启动线程就启动了子类中重写的run()方法。

二是通过实现Runnable接口,在该接口中定义run()方法。

线程被创建后处于待命状态,启动线程就是启动线程的run()方法,这是通过调用线程的start()方法来实现的。

2.3线程的优先级

对于多线程程序,每个线程的重要程度不尽相同,如多个线程在等待处理机时,往往需要优先级高的线程优先抢占到CPU得以执行;又如多个线程交替执行时,希望优先级高的线程得到CPU的时间长一些;这样,高优先级的线程处理的任务效率就高一些。

Java中线程的优先级从低到高以整数1~10表示,共分为10级,设置优先级是通过调用线程对象的setPriority()方法。

2.4线程的同步

线程的异步执行是指线程抢占CPU,不关心其它线程的状态或行为。但是在访问一些共享资源时,这种无序访问会导致无法得到正确的结果。因此当两个或多个线程需要访问同一资源时,它们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用,这种方式称为同步。

Java在同步机制中提供了语言级的支持,可以通过对关键代码段使用synchronized关键字修饰来实现针对该代码段的同步操作。

2.5线程间的相互联系

可以利用wait()、notify()及notifyAll()方法发送消息实现线程间的相互联系,在同一时刻只能有一个线程访对象中的同步方法,其它线程被阻塞。通常可以用notify()或notifyAll()方法唤醒其它一个或所有线程。而使用wait()方法来使该线程处于阻塞状态,等待其它的线程用notify()唤醒。

3 多线程在Socket编程中的应用

3.1服务器端的多线程

网上数据交互的主要特点是发生的随机性,即消息的到达不存在任何预示或规律,协作双方无法预知信息到达的时间,所以要防止信息丢失。因此,需要在服务器程序中设置一个循环语句,反复检测输入流中有没有来自客户的数据。以一个简单的例子说

(上接第796页)

明这个问题:客户通过输出流将一个字符串传给服务器,服务器收到后将字符串的所有字母转换为大写,然后通过输出流回送给客户。程序如下:

public static void main(String[] args) throws Exception{

ServerSocket ss = new ServerSocket(PORT);

Socket skt = ss.accept();

DataInputStream dis = new DataInputStream(skt.getInputSream());

DataOutputStream dos = new DataOutputStream(skt.getOutputStream());

while (true) {

String s = dis.readUTF();

if (s!=null)dos.writeUTF(s.toUpperCase()); } }

该程序的一个明显缺陷是只能接收来自一个客户的数据,因为程序会停留在while语句处反复检测,这是服务器程序所不能容忍的。为了能接收来自多个客户的数据,必须采用多线程的方法。

每与一个客户建立连接后,就启动一个线程,让该线程检测输入流中有无数据,便可实现服务器同时响应多个客户的请求。方法如下:

class ServerThread extends Thread{

Socket skt = null;

DataInputStream dis = null;

DataOutputStream dos = null;

ServerThread(Socket skt1){

skt = skt1;

try{

dis = new DataInputStream(skt.getInputStream());

dos = new DataOutputStream(skt.getOutputStream());

}catch(Exception e){} }

public void run(){

while (true){

String s = dis.readUTF();

if (s!=null)dos.writeUTF(s.toUpperCase()); }}}

这样主方法就可以只负责接收客户请求,将每个请求的Socket对象作为参数传递给ServerThread的构造方法,然后将该ServerThread对象启动,便会有一个线程专门负责监听该客户的请求。主方法为:

public static void main(String[] args) throws Exception{

ServerSocket ss = new ServerSocket(PORT);

while (true){

Socket skt = ss.accept();

if (skt!=null)new ServerThread(skt).start();}}

3.2客户端的多线程

客户如果要监听服务器回送的信息,为了防止信息的遗漏,同样需要使用循环语句反复监测输入流:

while (true){

String s =-dis.readUTF();

if (s!=null) System.out.println(s);}

在这种情况下,主程序便会反复执行循环体,无法进行其它工作。如果让一个线程专门负责监听,那么该线程启动后,主程序便可解放出来处理其它事情。线程的写法可以象服务器端一样,写一个Thread的子类,也可以实现Runnable接口。

将多线程应用到Socket程序中后,便实现了客户与服务器的合理通信,既不至于遗漏信息,也不会因为监听而影响程序的其它功能。

4 结束语

本文讨论了Java的多线程机制及其使用,研究了将多线程应用于Socket程序设计的基本模型,在此模型的基础上可以方便的演化出更为复杂的程序,如聊天、远程计算等。本文对多线程及Socket编程的初学者有一定参考价值,也是研究多线程协作、即时响应等问题的基础。

参考文献:

[1]胡雯,赵海廷.JAVA多线程同步问题研究[J].软件导刊,2007,1.

[2]姜景根,李祥.基于Java的多线程并发服务器的设计与应用[J].电脑与信息技术,2007;15(1).

[3]Bruce Eckel.Think in Java[M].北京:机械工业出版社,2002.

多线程编程基础知识 篇4

考虑这种情况:如果一个线程遇到锁嵌套的情况该怎么办,这个嵌套是指当我一个线程在获取临界资源时,又需要再次获取,

根据这种情况,代码如下:

代码如下:

‘‘‘

Created on -9-8

@author: walfred

@module: thread.ThreadTest6

‘‘‘

import threading

import time

counter = 0

mutex = threading.Lock

class MyThread(threading.Thread):

def __init__(self):

threading.Thread.__init__(self)

def run(self):

global counter, mutex

time.sleep(1);

if mutex.acquire():

counter += 1

print “I am %s, set counter:%s” % (self.name, counter)

if mutex.acquire():

counter += 1

print “I am %s, set counter:%s” % (self.name, counter)

mutex.release()

mutex.release()

if __name__ == “__main__”:

for i in range(0, 200):

my_thread = MyThread()

my_thread.start()

这种情况的代码运行情况如下:

代码如下:

I am Thread-1, set counter:1

之后就直接挂起了,这种情况形成了最简单的死锁,

那有没有一种情况可以在某一个线程使用互斥锁访问某一个竞争资源时,可以再次获取呢?在Python中为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

代码只需将上述的:

代码如下:

mutex = threading.Lock()

替换成:

代码如下:

mutex = threading.RLock()

多线程编程基础知识 篇5

本文主要论述IOS创建锁的方法(总结):

一、使用关键字

1)@synchronized(互斥锁)

优点:使用@synchronized关键字可以很方便地创建锁对象,而且不用显式的创建锁对象。

缺点:会隐式添加一个异常处理来保护代码,该异常处理会在异常抛出的时候自动释放互斥锁。而这种隐式的异常处理会带来系统的额外开销,为优化资源,你可以使用锁对象。

二、“Object-C”语言

1)NSLock(互斥锁)

2)NSRecursiveLock(递归锁)

条件锁,递归或循环方法时使用此方法实现锁,可避免死锁等问题。

3)NSConditionLock(条件锁)

使用此方法可以指定,只有满足条件的时候才可以解锁。

4)NSDistributedLock(分布式锁)

在IOS中不需要用到,也没有这个方法,因此本文不作介绍,这里写出来只是想让大家知道有这个锁存在。

如果想要学习NSDistributedLock的话,你可以创建MAC OS的项目自己演练,方法请自行Google,谢谢。

三、C语言

1)pthread_mutex_t(互斥锁)

2)GCD-信号量(“互斥锁”)

3)pthread_cond_t(条件锁)

线程安全 —— 锁

一、使用关键字:

1)@synchronized

// 实例类person

Person *person = [[Person alloc] init];

// 线程A

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

@synchronized(person) {

[person personA];

[NSThread sleepForTimeInterval:3]; // 线程休眠3秒

}

});

// 线程B

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

@synchronized(person) {

[person personB];

}

});

关键字@synchronized的使用,锁定的对象为锁的唯一标识,只有标识相同时,才满足互斥。如果线程B锁对象person改为self或其它标识,那么线程B将不会被阻塞。你是否看到@synchronized(self) ,也是对的。它可以锁任何对象,描述为@synchronized(anObj)。

二、Object-C语言

1)使用NSLock实现锁

// 实例类person

Person *person = [[Person alloc] init];

// 创建锁

NSLock *myLock = [[NSLock alloc] init];

// 线程A

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[myLock lock];

[person personA];

[NSThread sleepForTimeInterval:5];

[myLock unlock];

});

// 线程B

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[myLock lock];

[person personB];

[myLock unlock];

});

程序运行结果:线程B会等待线程A解锁后,才会去执行线程B。如果线程B把lock和unlock方法去掉之后,则线程B不会被阻塞,这个和synchronized的一样,需要使用同样的锁对象才会互斥。

NSLock类还提供tryLock方法,意思是尝试锁定,当锁定失败时,不会阻塞进程,而是会返回NO。你也可以使用lockBeforeDate:方法,意思是在指定时间之前尝试锁定,如果在指定时间前都不能锁定,也是会返回NO。

注意:锁定(lock)和解锁(unLock)必须配对使用

2)使用NSRecursiveLock类实现锁

// 实例类person

Person *person = [[Person alloc] init];

// 创建锁对象

NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];

// 创建递归方法

static void (^testCode)(int);

testCode = ^(int value) {

[theLock tryLock];

if (value > 0)

{

[person personA];

[NSThread sleepForTimeInterval:1];

testCode(value - 1);

}

[theLock unlock];

};

//线程A

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

testCode(5);

});

//线程B

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[theLock lock];

[person personB];

[theLock unlock];

});

如果我们把NSRecursiveLock类换成NSLock类,那么程序就会死锁。因为在此例子中,递归方法会造成锁被多次锁定(Lock),所以自己也被阻塞了。而使用NSRecursiveLock类,则可以避免这个问题。

3)使用NSConditionLock(条件锁)类实现锁:

使用此方法可以创建一个指定开锁的条件,只有满足条件,才能开锁。

// 实例类person

Person *person = [[Person alloc] init];

// 创建条件锁

NSConditionLock *conditionLock = [[NSConditionLock alloc] init];

// 线程A

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[conditionLock lock];

[person personA];

[NSThread sleepForTimeInterval:5];

[conditionLock unlockWithCondition:10];

});

// 线程B

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[conditionLock lockWhenCondition:10];

[person personB];

[conditionLock unlock];

});

线程A使用的是lock方法,因此会直接进行锁定,并且指定了只有满足10的情况下,才能成功解锁,

unlockWithCondition:方法,创建条件锁,参数传入“整型”。lockWhenCondition:方法,则为解锁,也是传入一个“整型”的参数。

三、C语言

1)使用pthread_mutex_t实现锁

注意:必须在头文件导入:#import

 

// 实例类person

Person *person = [[Person alloc] init];

// 创建锁对象

__block pthread_mutex_t mutex;

pthread_mutex_init(&mutex, NULL);

// 线程A

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

pthread_mutex_lock(&mutex);

[person personA];

[NSThread sleepForTimeInterval:5];

pthread_mutex_unlock(&mutex);

});

// 线程B

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

pthread_mutex_lock(&mutex);

[person personB];

pthread_mutex_unlock(&mutex);

});

实现效果和上例的相一致

2)使用GCD实现“锁”(信号量)

GCD提供一种信号的机制,使用它我们可以创建“锁”(信号量和锁是有区别的,具体请自行百度)。

// 实例类person

Person *person = [[Person alloc] init];

// 创建并设置信量

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

// 线程A

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

[person personA];

[NSThread sleepForTimeInterval:5];

dispatch_semaphore_signal(semaphore);

});

// 线程B

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

[person personB];

dispatch_semaphore_signal(semaphore);

});

效果也是和上例介绍的相一致。

我在这里解释一下代码。dispatch_semaphore_wait方法是把信号量加1,dispatch_semaphore_signal是把信号量减1。

我们把信号量当作是一个计数器,当计数器是一个非负整数时,所有通过它的线程都应该把这个整数减1。如果计数器大于0,那么则允许访问,并把计数器减1。如果为0,则访问被禁止,所有通过它的线程都处于等待的状态。

3)使用POSIX(条件锁)创建锁

// 实例类person

Person *person = [[Person alloc] init];

// 创建互斥锁

__block pthread_mutex_t mutex;

pthread_mutex_init(&mutex, NULL);

// 创建条件锁

__block pthread_cond_t cond;

pthread_cond_init(&cond, NULL);

// 线程A

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

pthread_mutex_lock(&mutex);

pthread_cond_wait(&cond, &mutex);

[person personA];

pthread_mutex_unlock(&mutex);

});

// 线程B

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

pthread_mutex_lock(&mutex);

[person personB];

[NSThread sleepForTimeInterval:5];

pthread_cond_signal(&cond);

pthread_mutex_unlock(&mutex);

});

效果:程序会首先调用线程B,在5秒后再调用线程A。因为在线程A中创建了等待条件锁,线程B有激活锁,只有当线程B执行完后会激活线程A。

pthread_cond_wait方法为等待条件锁。

pthread_cond_signal方法为激动一个相同条件的条件锁。

简单总结:

多核多线程题目总结 篇6

-如果某个系统支持两个或多个动作(Action)同时存在,那么这个系统就是一个并发系统-如果某个系统支持两个或多个动作同时执行,那么这个系统就是一个并行系统

-并发程序可同时拥有两个或多个线程。如果程序能够并行执行,则一定是运行在多核处理器上,每个线程都将分配到一个独立的处理器核上。

-“并行”概念是“并发”概念的一个子集 3.并行计算技术的主要目的:

加速求解问题的速度

例如,给定某应用,在单处理器上,串行执行需要2 周,这个速度对一般的应用而言,是无法忍受的。于是,可以借助并行计算,使用100 台处理器,加速50 倍,将执行时间缩短为6.72 个小时。

提高求解问题的规模

例如,在单处理器上,受内存资源2GB的限制,只能计算10 万个网格,但是,当前数值模拟要求计算千万个网格。于是,也可以借助并行计算,使用100 个处理器,将问题求解规模线性地扩大100 倍。并行计算的主要目标:

在并行机上,解决具有重大挑战性计算任务的科学、工程及商业计算问题,满足不断增长的应用问题对速度和内存资源的需求。

4.并行计算的主要研究内容大致可分为四个方面:

并行机的高性能特征抽取

充分理解和抽取当前并行机体系结构的高性能特征,提出实用的并行计算模型和并行性能评价方法,指导并行算法的设计和并行程序的实现。

并行算法设计与分析

设计高效率的并行算法,将应用问题分解为可并行计算的多个子任务,并具体分析这些算法的可行性和效果。

并行实现技术

主要包含并行程序设计和并行性能优化。

并行应用

这是并行计算研究的最终目的。通过验证和确认并行程序的正确性和效率,进一步将程序发展为并行应用软件,应用于求解实际问题。同时,结合实际应用出现的各种问题,不断地改进并行算法和并行程序。5.并行程序执行时间

对各个进程,墙上时间可进一步分解为计算CPU时间、通信CPU时间、同步开销时间、同步导致的进程空闲时间

计算CPU时间:进程指令执行所花费的CPU时间,包括程序本身的指令执行占用的时间(用户时间)和系统指令花费的时间;

通信CPU时间:进程通信花费的CPU时间; 同步开销时间:进程同步花费的时间;

进程空闲时间:进程空闲时间是指并行程序执行过程中,进程所有空闲时间总和(如进程阻塞式等待其他进程的消息时。此时CPU通常是空闲的,或者处于等待状态)6.并行程序性能优化

最主要的是选择好的并行算法和通信模式

减少通信量、提高通信粒度

提高通信粒度的有效方法就是减少通信次数,尽可能将可以一次传递的数据合并起来一起传递

全局通信尽量利用高效集合通信算法

对于标准的集合通信,如广播、规约、数据散发与收集等,尽量调用MPI标准库函数 挖掘算法的并行度,减少CPU空闲等待

具有数据相关性的计算过程会导致并行运行的部分进程空闲等待.在这种情况下,可以考虑改变算法来消除数据相关性

7.顺序程序的特性

顺序性:处理机严格按照指令次序依次执行,即仅当一条指令执行完后才开始执行下一条指令;

封闭性:程序在执行过程中独占系统中的全部资源,该程序的运行环境只与其自身动作有关,不受其它程序及外界因素影响;

可再现性:程序的执行结果与执行速度无关,而只与初始条件有关,给定相同的初始条件,程序的任意多次执行一定得到相同的执行结果.8.并发程序特性

交叉性:程序并发执行对应某一种交叉,不同的交叉可能导致不同的计算结果,操作系统应当保证只产生导致正确结果的交叉,去除那些可能导致不正确结果的交叉;

非封闭性:一个进程的运行环境可能被其它进程所改变,从而相互影响;

不可再现性:由于交叉的随机性,并发程序的多次执行可能对应不同的交叉,因而不能期望重新运行的程序能够再现上次运行的结果。

10.Win32线程同步的实现

11.数据作用域对数据值的影响

12.分析程序的结果

13.并行区域编程与parallel for语句的区别

-并行区域采用了复制执行方式,将代码在所有的线程内各执行一次;

-循环并行化则是采用工作分配执行方式,将循环需做的所有工作量,按一定的方式分配给各个执行线程,全部线程执行工作的总合等于原先串行执行所完成的工作量。14.OpenMP提供三种不同的互斥锁机制: 临界区(critical)原子操作(atomic)由库函数来提供同步操作 int counter=0;#pragma omp parallel {

for(int i=0;i<10000;i++)

#pragma omp atomic //atomic operation

}

printf(“counter = %dn”,counter);

counter=20000 25.影响性能的主要因素

并行化代码在应用程序中的比率-OpenMP 本身的开销

-OpenMP 获得应用程序多线程并行化的能力,需要一定的程序库支持。在这些库程序对程序并行加速的同时也需要运行库本身-负载均衡

-局部性

在程序运行过程中,高速缓存将缓存最近刚刚访问过的数据及其相邻的数据。因此,在编写程序的时候,需要考虑到高速缓存的作用,有意地运用这种局部性带来的高速缓存的效率提高。-线程同步带来的开销

多个线程在进行同步的时候必然带来一定的同步开销,在使用多线程进行开发时需要考虑同步的必要性,消除不必要的同步,或者调整同步的顺序,就有可能带来性能上的提升。26.什么是MPI 消息传递接口(Message Passing Interface,简称MPI)是一种编程接口标准,而不是一种具体的编程语言。基本含义:MPI标准定义了一组具有可移植性的编程接口。

特征:

1、典型的实现包括开源的MPICH、LAM MPI以及不开源的INTEL MPI。

2、程序员设计好应用程序并行算法,调用这些接口,链接相应平台上的MPI库,即可实现基于消息传递的并行计算

27.MPICH的安装和配置 counter++;

28.MPI程序的四个基本函数-MPI_Init和MPI_Finalize MPI_Init初始化MPI执行环境,建立多个MPI进程之间的联系,为后续通信做准备。而MPI_Finalize则是结束MPI执行环境。这两个函数用来定义MPI程序的并行区-MPI_Comm_rank

用来标识各个MPI进程,两个函数参数:

MPI_Comm类型的通信域,表示参与计算的MPI进程组 &rank,返回调用进程在comm中的标识号-MPI_Comm_size 用来标识相应进程组中有多少个进程,有两个参数: MPI_Comm类型的通信域,标尺参与计算的MPI进程组 整型指针,返回相应进程组中的进程数

29.MPI的点对点通信

两个最重要的MPI函数MPI_Send和MPI_Recv。

-int MPI_SEND(buf, count, datatype, dest, tag, comm)这个函数的含义是向通信域comm中的dest进程发送数据。消息数据存放在buf中,类型是datatype,个数是count个。这个消息的标志是tag,用以和本进程向同一目的进程发送的其他消息区别开来。

-int MPI_RECV(buf,count,datatype,source,tag,comm,status)MPI_Recv绝大多数的参数和MPI_Send相对应,有相同的意义。唯一的区别就是MPI_Recv里面多了一个参数status。status主要显示接收函数的各种错误状态。30.消息管理7要素 发送或者接收缓冲区buf; 数据数量count; 数据类型datatype;

目标进程或者源进程destination/source; 消息标签tag; 通信域comm;.消息状态status,只在接收的函数中出现。31.MPI群集通信

群集通信是包含了一对多、多对一和多对多的进程通信模式。其最大特点是多个进程参与通信。常用的MPI群集通信函数: 同步

多线程编程基础知识 篇7

Linux经过了20多年的发展,已经成为一个功能强大而稳定的操作系统,它支持很多硬件平台,目前已经发展到2.6的内核。而Linux下的网络编程应用也是极为广泛,其中套接字的编程最为流行。Linux2.6内核采用了新的线程库Nation POSIX Thread Library(NPTL),该线程库符合POSIX的规范,提供企业级的线程支持。下文将结合Linux下线程的特点来讨论它在网络编程中的应用。

1 套接字编程的基本原理

1.1 socket的类型

在Linux中的网络编程是通过socket接口来进行的。socket也叫做套接字,是一种网络API。它定义了许多函数,我们可以用它来开发网络应用程序。socket接口提供了一种进程间通信的方法,使得在相同主机或不同主机上的进程能以相同的规范进行双向信息通信。进程通过调用socket接口API来实现相互之间的通信,socket接口又利用下层的网络通信协议功能和系统调用实现实际的通信工作。它们之间的关系如图1所示。

常见的socket有3种类型:

(1)流式socket(SOCK_STREAM)。流式套接字提供可靠的、面向连接的通信流,它使用TCP协议。

(2)数据报socket(SOCK_DGRAM)。数据报套接字提供了一种无连接的服务,它使用数据报协议UDP。

(3)原始socket。

1.2 TCP套接字的工作流程

在这里,使用TCP套接字来讨论Linux网络编程。TCP套接字分为服务器端和客户端两个部分。TCP套接字的通信流程如图2所示:

在建立TCP服务器和TCP客户的时候,有两个非常重要的函数:socket()和bind()。

socket()函数用于建立一个socket连接,函数原型为:

int socket(int family,int type,int protocol),其中参数family为协议族,type为套接字类型,protocol为此socket请求所使用的协议。在建立了socket连接之后,就对sockaddr_in进行初始化,以保存所建立的socket信息。

其中sockaddr_in为保存地址结构的结构体,它的结构定义如下:

bind()函数是将将套接字和地址信息相关联,建立地址与套接字的对应关系。函数原型为:int bind (int sockfd,struct sockaddr*my_addr,int addrlen),其中参数socktd为函数socket()返回的套接字描述符,my_addr为本地地址,addrlen为地址长度。

在服务器与客户端都建立起socket连接并与本地地址绑定后,服务器端就利用listen()函数进行监听操作,使客户端连接到该服务器,客户端就可以使用connect()函数来配置套接字来与服务器建立一个TCP连接。服务器端利用accept()函数让服务器收到客户端的连接请求。在建立好输入队列后,服务器就调用accept()函数,然后睡眠并等待客户的连接请求。当accept()函数监视的套接字收到连接请求时,系统核将建立一个新的套接字,系统核将这个新套接字和请求连接进程的地址联系起来,收到服务请求的初始套接字仍可以继续在以前的套接字上监听,同时可以在新的套接字描述符上进行数据传输操作。建立连接完成后,服务器和客户就可以进行双向的数据传输,套接字提供recv()和send()函数来传输数据。当完成数据传输之后,服务器和客户用close()函数来终止连接以释放资源。

2 Linux多线程在网络编程中的应用

2.1 并发服务器

当服务器完成基本配置后,它每次只能处理一个客户的请求,处理完当前客户后,才能处理下一个客户请求。在实际应用中,服务器要处理大量的客户请求,所有客户都访问绑定在某一特定套接字地址的服务器上,因此,服务器需求满足并发的请求。

2.2 多线程的应用

在Linux服务器中,进程与线程都能支持并发,但由于线程占用更少的系统资源,并且线程之间的切换速度更快,所以在这里使用多线程来讨论并发服务器。

Linux2.6内核采用Native POSIX Thread Library (NPTL)线程库,它是与POSIX兼容的。在Linux服务器中实现并发的思路是:主线程循环调用accept()函数,当与客户端连接后,产生子线程来处理客户请求,它的处理流程如图3所示:

在Linux系统中,产生子线程需要调用pthread_create()函数,其函数原型为:

int pthread_create ((pthread_t*thread,pthread_attr_t*attr,void*(*start_routine)(void*),void*arg)),其中参数thread为线程标识符,attr为线程属性设置,start_routine为线程函数的起始地址,arg为传递给start_routine的参数。

服务器运行后,程序首先创建TCP套接字,然后绑定套接字并监听,主线程就循环调用accept()函数,来接受一个客户的连接请求,相关代码如下:

主线程完成与客户的连接后,会返回一个新的套接字描述符connectfd,这时程序再生成一个新的线程,来完成与连接上的客户的通信工作,而主线程仍然在继续监听,以接受新的客户的连接请求。

新的线程生成后,程序会将新的套接字描述符connectfd以及客户的地址信息参数&client传递给新的线程,因为pthread_create()函数每次只能传递新线程一个参数,所以需将上述两个参数封装在一个结构中,结构定义如下:

将结构体信息传递给新线程后,新的线程就可以与客户通信。但这又带来了新的问题,因为变置arg是所有线程共用的,如果主线程接收了新的客户的连接,它就会修改arg的内容,而新线程的arg信息也会被修改。因此,解决上述问题的一种方法是为每个新的线程分配存储arg的空间,在将arg传递给新的线程,新的线程处理完客户信息后再释放arg空间。相关代码如下:

至此,新线程的初始化工作就已完成,新线程将调用recv()和send()函数来实现与客户的通信工作。

3 结束语

讲述了套接字编程的原理,并讨论了Linux多线程在并发服务器中的应用,希望本文对实际的Linux网络编程有所帮助。

参考文献

[1]孙琼.嵌入式Linux应用编程[M].北京:人民邮电出版社,2006.

上一篇:语文要素包括下一篇:上学期四年级数学教研组工作总结