MFC 多线程程序设计

MFC  多线程程序设计
MFC  多线程程序设计

MFC 多线程程序设计

Multi-threaded Programming in MFC

线程(thread),是执行线程(thread of execution)的简单称呼。"Thread" 这个字的原意是「线」。

中文字里头的「线程」也有「线」的意思,所以我采用「线程」、「执行线程」这样的中文名

称。如果你曾经看过「多线」这个名词,其实就是本章所谓的「多线程」。

我曾经在第1章以三两个小节介绍Win32 环境下的进程与执行线程观念,并且以程序直接

调用CreateThread 的形式,示范了几个Win32 小例子。现在我要更进一步从操作系统

的层面谈谈执行线程的学理基础,然后带引各位看看MFC 对于「执行线程」支持了什么样

的类别。然后,实际写个MFC 多线程程序。

从操作系统层面看执行线程

书籍推荐:如果要从操作系统层面来了解执行线程,Matt Pietrek 的Windows 95 System

Programming SECRETS(Windows 95 系统程序设计大奥秘/侯俊杰译/旗标出版)无疑是

最佳知识来源。Matt 把操作系统核心模块(KERNEL32.DLL)中用来维护执行线程生存的

数据结构都挖掘出来,非常详尽。这是对执行线程的最基础认识,直达其灵魂深处。

你已经知道,CreateThread 可以产生一个执行线程,而「线程」的本体就是CreateThread 第

3个参数所指定的一个函数(一般我们称之为「执行线程函数」)。这个函数将与目前的

「执行事实」同时并行,成为另一个「执行事实」。执行线程函数的执行期,也就是该执

行线程的生命期。

操作系统如何造成这种多任务并行的现象?执行线程对于操作系统的意义到底是什么?系统

如何维护许多个执行线程?执行线程与其父亲大人(进程)的关系如何维持?CPU 只有一

个,执行线程却有好几个,如何摆平优先权与排程问题?这些疑问都可以在下面各节中获

得答案。

三个观念:模块、进程、执行线程

试着回答这个问题:进程(process)是什么?给你一分钟时间。

z z z z z...

你的回答可能是:『一个可执行档执行起来,就是一个进程』。唔,也不能算错。但能

不能够有更具体的答案?再问你一个问题:模块(module)是什么?可能你的回答还是:

『一个可执行档执行起来,就是一个模块』。这也不能够算错。但是你明明知道,模块

不等于进程。KERNEL32 DLL 是一个模块,但不是一个进程;Scribble EXE 是一个模块,

也是一个进程。

我们需要更具体的资料,更精准的答案。

如果我们能够知道操作系统如何看待模块和进程,就能够给出具体的答案了。一段可执

行的程序(包括EXE 和DLL),其程序代码、资料、资源被加载到内存中,由系统建

置一个数据结构来管理它,就是一个模块。这里所说的数据结构,名为Module Database

(MDB),其实就是PE 格式中的PE 表头,你可以从WINNT.H 档中找到一个

IMAGE_NT_HEADER 结构,就是它。

好,解释了模块,那么进程是什么?这就比较抽象一点了。这样说,进程就是一大堆拥

有权(ownership)的集合。进程拥有地址空间(由memory context 决定)、动态配置而

来的内存、文件、执行线程、一系列的模块。操作系统使用一个所谓的Process Database

(PDB)数据结构,来记录(管理)它所拥有的一切。

执行线程呢?执行线程是什么?进程主要表达「拥有权」的观念,执行线程则主要表达模块中

的程序代码的「执行事实」。系统也是以一个特定的数据结构(Thread Database,TDB)记

录执行线程的所有相关资料,包括执行线程区域储存空间(Thread Local Storage,TLS)、讯

息队列、handle 表格、地址空间(Memory Context)等等等。

最初,进程是以一个执行线程(称为主执行线程,primary thread)做为开始。如果需要,行程可以产生更多的执行线程(利用CreateThread),让CPU 在同一时间执行不同段落的码。当然,我们都知道,在只有一颗CPU 的情况下,不可能真正有多任务的情况发生,「多个执行线程同时工作」的幻觉主要是靠排程器来完成-- 它以一个硬件定时器和一组复杂的游戏规则,在不同的执行线程之间做快速切换动作。以Windows 95 和Windows NT 而言,在非特殊的情况下,每个执行线程被CPU 照顾的时间(所谓的timeslice)是20 个milliseconds。

如果你有一部多CPU 计算机,又使用一套支持多CPU 的操作系统(如Windows NT),

那么一个CPU 就可以分配到一个执行线程,真正做到实实在在的多任务。这种操作系统特性

称为symmetric multiprocessing(SMP)。Windows 95 没有SMP 性质,所以即使在多CPU 计算机上跑,也无法发挥其应有的高效能。

图14-1 表现出一个进程(PDB)如何透过「MODREF 串行」连接到其所使用的所有模组。图14-2 表现出一个模块数据结构(MDB)的细部内容,最后的DataDirectory[16] 记录着16 个特定节区(sections)的地址,这些sections 包括程序代码、资料、资源。图

14-3 表现出一个执行线程数据结构(PDB)的细部内容。

当Windows 加载器将程序加载内存中,KERNEL32 挖出一些内存,构造出一个

PDB、一个TDB、一个以上的MDBs(视此程序使用到多少DLL 而定)。针对TDB,

操作系统又要产生出memory context(就是在操作系统书籍中提到的那些所谓page tables)、消息队列、handle 表格、环境数据结构(EDB)...。当这些系统内部数据结构

都构造完毕,指令指位器(Instruction Pointer)移到程序的进入点,才开始程序的执行。图14-1 进程(PDB )透过「MODREF 串行」连接到其所使用的所有模块

执行线程优先权(Priority)

我想我们现在已经能够用很具体的形象去看所谓的进程、模块、执行线程了。「执行事实」

发生在执行线程身上,而不在进程身上。也就是说,CPU 排程单位是执行线程而非进程。排

程器据以排序的,是每个执行线程的优先权。

优先权的设定分为两个阶段。我已经在第1章介绍过。执行线程的「父亲大人」(进程)

拥有所谓的优先权等级(priority class,图1-7),可以在CreateProcess 的参数中设定。

执行线程基本上继承自其「父亲大人」的优先权等级,然后再加上CreateThread 参数中的微调差额(-2~+2)。获得的结果(图1-8)便是执行线程的所谓base priority,范围从0~31 数值愈高优先权愈高。::SetThreadPriority 是调整优先权的工具,它所指定的也是微调差

额(-2~+2)。

// 指向各个sections,

// 包括程序代码,资料,资源

图14-2 模块数据结构MDB 的细部内容(资料整理自Windows 95 System Programming SECRETS, Matt Pietrek, IDG Books)

执行线程排程(Scheduling)

排程器挑选「下一个获得CPU 时间的执行线程」的唯一依据就是:执行线程优先权。如果所有等待被执行的执行线程中,有一个是优先权16,其它所有执行线程都是优先权15(或更低),那么优先权16 者便是下一个夺标者。如果执行线程A和B同为优先权16,排程器会挑选等待比较久的那个(假设为执行线程A)。当A的时间切片(timeslice)终了,如果B以外的其它执行线程的优先权仍维持在15(以下),执行线程B就会获得执行权。「如果B以外的其它执行线程的优先权仍维持在15(以下)...」,唔,这听起来彷佛优先

权会变动似的。的确是。为了避免朱门酒肉臭、路有冻死骨的不公平情况发生,排程器

会弹性调整执行线程优先权,以强化系统的反应能力,并且避免任何一个执行线程一直未能接受CPU 的润泽。一般的执行线程优先权是7,如果它被切换到前景,排程系统可能暂

时地把它调升到8 或9 或更高。对于那些有着输入消息等待被处理的执行线程,排程系

统也会暂时调高其优先权。

对于那些优先权本来就高的执行线程,也并不是有永久的保障权利。别忘了Windows 毕竟是个消息驱动系统,如果某个执行线程调用::GetMessage 而其消息队列却是空的,这个执行线程便被冻结,直到再有消息进来为止。冻结的意思就是不管你的优先权有多高,暂时退出排班行列。执行线程也可能被以::SuspendThread 强制冻结住(::ResumeThread 可以解除冻结)。

会被冻结,表示这个执行线程「要去抓取消息,而执行线程所附带的消息队列中却没有消息」。如果一个执行线程完全和UI 无关呢?是否它就没有消息队列?倒不是,但它的程序代码中没有消息循环倒是事实。是的,这种执行线程称为worker thread。正因它不可能会被冻结,所以它绝对不受Win16Mutex 或其它因素而影响其强制性多任务性质,及其优先权。Thread Context

Context 一词,我不知道有没有什么好译名,姑且就用原文吧。它的直接意思是「前后关系、脉络;环境、背景」。所以我们可以说Thread Context 是构成执行线程的「背景」。

那是指什么呢?狭义来讲是指一组缓存器值(包括指令指位器IP)。因为执行线程常常会

被暂停,被要求把CPU 拥有权让出来,所以它必须将暂停之前一刻的状态统统记录下

来,以备将来还可以恢复。

你可以在WINNT.H 中找到一个CONTEXT 数据结构,它可以用来储存Thread

Context 。::GetThreadContext 和::SetThreadContext 可以取得和设定某个执行线程的

context,因而改变该执行线程的状态。这已经是非常低阶的行为了。Matt Pietrek 在其

Windows 95 System Programming SECRETS 一书第10 章,写了一个Win32 API Spy 程式,就充份运用了这两个函数。

我想我们在操作系统层面上的执行线程学理基础已经足够了,现在让我们看看比较实际一点的东西。

从程序设计层面看执行线程

书籍推荐:如果要从程序设计层面来了解执行线程,Jim Beveridge 和Robert Wiener 合着的Multithreading Applications in Win32 (Win32 多线程程序设计/侯俊杰译/?峰出版)是很值得推荐的一份知识来源。这本书介绍执行线程的学理观念、程序方法、同步控制、资

料一致性的保持、C runtime library 的多线程版本、C++ 的多线程程序方法、MFC 中的多线程

程序方法、除错、进程通讯(IPC)、DLLs...,以及约50 页的实际应用。

书籍推荐:Jeffrey Richter 的Advanced Windows 在进程与执行线程的介绍上(第2章和第3章),也有非常好的表现。他的切入方式是详细而深入地叙述相关Win32 API 的规格

与用法。并举实例左证。

如何产生执行线程?我想各位都知道了,::CreateThread 可以办到。图14-4 是与执行线程有

关的Win32 API。

与执行线程有关的Win32 API 功能

AttachThreadInput 将某个执行线程的输入导向另一个执行线程CreateThread 产生一个执行线程

ExitThread 结束一个执行线程GetCurrentThread 取得目前执行线程的handle

GetCurrentThreadId 取得目前执行线程的ID

GetExitCodeThread 取得某一执行线程的结束代码(可用以决定执行线程是否已结束)

GetPriorityClass 取得某一进程的优先权等级

GetQueueStatus 传回某一执行线程的消息队列状态

GetThreadContext 取得某一执行线程的context

GetThreadDesktop 取得某一执行线程的desktop 对象

GetThreadPriority 取得某一执行线程的优先权

GetThreadSelectorEntry 除错器专用,传回指定之执行线程的某个selector 的

LDT 记录项

ResumeThread 将某个冻结的执行线程恢复执行

SetPriorityClass 设定优先权等级

SetThreadPriority 设定执行线程的优先权

Sleep 将某个执行线程暂时冻结。其它执行线程将获得执行权。

SuspendThread 冻结某个执行线程

TerminateThread 结束某个执行线程

TlsAlloc 配置一个TLS(Thread Local Storage)

TlsFree 释放一个TLS(Thread Local Storage)

TlsGetValue 取得某个TLS(Thread Local Storage)的内容

TlsSetValue 设定某个TLS(Thread Local Storage)的内容

WaitForInputIdle 等待,直到不再有输入消息进入某个执行线程中

图14-4 与执行线程有关的Win32 API 函数

注意,多执行线程并不能让程序执行得比较快(除非是在多CPU 机器上,并且使用支持

symmetric multiprocessing 的操作系统),只是能够让程序比较「有反应」。试想某个程

式在某个菜单项目被按下后要做一个小时的运算工作,如果这份工作在主执行线程中做,

而且没有利用PeekMessage 的技巧时时观看消息队列的内容并处理之,那么这一个小时

内这个程序的使用者接口可以说是被冻结住了,将毫无反应。但如果沉重的运算工作是

由另一个执行线程来负责,使用者接口将依然灵活,不受影响。

Worker Threads 和U I Threads

从Windows 操作系统的角度来看,执行线程就是执行线程,并未再有什么分类。但从MFC

的角度看,则把执行线程划分为和使用者接口无关的worker threads,以及和使用者接口

(UI)有关的UI threads。

基本上,当我们以::CreateThread 产生一个执行线程,并指定一个执行线程函数,它就是一

个worker thread,除非在它的生命中接触到了输入消息-- 这时候它应该有一个消息回

路,以抓取消息,于是该执行线程摇身一变而为UI thread。

注意,执行线程本来就带有消息队列,请看图14-3 的TDB 结构。而如果执行线程程序代码

中带有一个消息循环,就称为UI thread。

错误观念

我记得曾经在微软的技术文件中,也曾经在微软的范例程序中,看到他们鼓励这样的作

法:为程序中的每一个窗口产生一个执行线程,负责窗口行为。这种错误的示范尤其存在

于MDI 程序中。是的,早期我也沾沾自喜地为MDI 程序的每一个子窗口设计一个执

行线程。基本上这是错误的行为,要付出昂贵的代价。因为子窗口一切换,上述作法会导

至执行线程也切换,而这却要花费大量的系统资源。比较好的作法是把所有UI(User

Interface)动作都集中在主执行线程中,其它的「纯种运算工作」才考虑交给worker threads

去做。

正确态度

什么是使用多执行线程的好时机呢?如果你的程序有许多事要忙,但是你还要随时保持注意某些外部事件(可能来自硬件或来自使用者),这时就适合使用多执行线程来帮忙。

以通讯程序为例。你可以让主执行线程负责使用者接口,并保持中枢的地位。而以一个分离的执行线程处理通讯端口,

MFC 多线程程序设计

我已经在第1章以一个小节介绍了Win32 多线程程序的写法,并给了一个小范例MltiThrd。这一节,我要介绍MFC 多线程程序的写法。

探索CWinThread

就像CWinApp 对象代表一个程序本身一样,CWinThread 对象代表一个执行线程本身。这个MFC 类别我们曾经看过,第6章讲「MFC 程序的生死因果」时,讲到「CWinApp::Run - 程序生命的活水源头」,曾经追踪过CWinApp::Run 的源头CWinThread::Run(里面有

一个消息循环)。可见程序的「执行事实」系发生在CWinThread 对象身上,而CWinThread 对象必须要(必然会)产生一个执行线程。

我希望「CWinThread 对象必须要(必然会)产生一个执行线程」这句话不会引起你的误会,以为程序在application object(CWinApp 对象)的构造式必然有个动作最终调用到CreateThread 或_beginthreadex。不,不是这样。想想看,当你的Win32 程序执行起来,

你的程序并没有调用CreateProcess 为自己做出代表自己的那个进程,也没有调用CreateThread 为自己做出代表自己的主执行线程(primary thread)的那个执行线程。为你的程序产生第一个进程和执行线程,是系统加载器以及核心模块(KERNEL32)合作的结果。所以,再次循着第6章一步步剖析的步骤,MFC 程序的第一个动作是CWinApp::CWinApp

(比WinMain 还早),在那里没有「产生执行线程」的动作,而是已经开始在收集执行线程的相关信息了:

// in MFC 4.2 APPCORE.CPP

CWinApp::CWinApp(LPCTSTR lpszAppName)

{

...

// initialize CWinThread state

AFX_MODULE_STATE* pModuleState = _AFX_CMDTARGET_GETSTATE();

AFX_MODULE_THREAD_STATE* pThreadState = pModuleState->m_thread;

ASSERT(AfxGetThread() == NULL);

pThreadState->m_pCurrentWinThread = this;

ASSERT(AfxGetThread() == this);

m_hThread = ::GetCurrentThread();

m_nThreadID = ::GetCurrentThreadId();

...

}

虽然MFC 程序只会有一个CWinApp 对象,而CWinApp 衍生自CWinThread,但并不

是说一个MFC 程序只能有一个CWinThread 对象。每当你需要一个额外的执行线程,不应该在MFC 程序中直接调用::CreateThread 或_beginthreadex,应该先产生一个CWinThread 对象,再调用其成员函数CreateThread 或全域函数AfxBeginThread 将执行

线程产生出来。当然,现在你必然已经可以推测到,CWinThread::CreateThread 或AfxBeginThread 内部调用了::CreateThread 或_beginthreadex (事实上答案是

_beginthreadex)。

这看起来颇有值得商议之处:为什么CWinThread 构造式不帮我们调用AfxBeginThread 呢?似乎CWinThread 为德不卒。

图14-5 就是CWinThread 的相关源代码。

#0001 // in MFC 4.2 THRDCORE.CPP

#0002 CWinThread::CWinThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam) #0003 {

#0004 m_pfnThreadProc = pfnThreadProc;

#0005 m_pThreadParams = pParam;

#0006

#0007 CommonConstruct();

#0008 }

#0009

#0010 CWinThread::CWinThread()

#0011 {

#0012 m_pThreadParams = NULL;

#0013 m_pfnThreadProc = NULL;

#0014

#0015 CommonConstruct();

#0016 }

#0017

#0018 void CWinThread::CommonConstruct()

#0020 m_pMainWnd = NULL;

#0021 m_pActiveWnd = NULL;

#0022

#0023 // no HTHREAD until it is created

#0024 m_hThread = NULL;

#0025 m_nThreadID = 0;

#0026

#0027 // initialize message pump

#0028 #ifdef _DEBUG

#0029 m_nDisablePumpCount = 0;

#0030 #endif

#0031 m_msgCur.message = WM_NULL;

#0032 m_nMsgLast = WM_NULL;

#0033 ::GetCursorPos(&m_ptCursorLast);

#0034

#0035 // most threads are deleted when not needed #0036 m_bAutoDelete = TRUE;

#0037

#0038 // initialize OLE state

#0039 m_pMessageFilter = NULL;

#0040 m_lpfnOleTermOrFreeLib = NULL;

#0042

#0043 CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam,

#0044 int nPriority, UINT nStackSize, DWORD dwCreateFlags,

#0045 LPSECURITY_ATTRIBUTES lpSecurityAttrs)

#0046 {

#0047 CWinThread* pThread = DEBUG_NEW CWinThread(pfnThreadProc, pParam);

#0048

#0049 if (!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED, nStackSize,

#0050 lpSecurityAttrs))

#0051 {

#0052 pThread->Delete();

#0053 return NULL;

#0054 }

#0055 VERIFY(pThread->SetThreadPriority(nPriority));

#0056 if (!(dwCreateFlags & CREATE_SUSPENDED))

#0057 VERIFY(pThread->ResumeThread() != (DWORD)-1);

#0058

#0059 return pThread;

#0060 }

#0061

#0062 CWinThread* AFXAPI AfxBeginThread(CRuntimeClass* pThreadClass,

#0063 int nPriority, UINT nStackSize, DWORD dwCreateFlags,

#0064 LPSECURITY_ATTRIBUTES lpSecurityAttrs)

#0065 {

#0066 ASSERT(pThreadClass != NULL);

#0067 ASSERT(pThreadClass->IsDerivedFrom(RUNTIME_CLASS(CWinThread)));

#0068

#0069 CWinThread* pThread = (CWinThread*)pThreadClass->CreateObject();

#0070

#0071 pThread->m_pThreadParams = NULL;

#0072 if (!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED, nStackSize,

#0073 lpSecurityAttrs))

#0074 {

#0075 pThread->Delete();

#0076 return NULL;

#0077 }

#0078 VERIFY(pThread->SetThreadPriority(nPriority));

#0079 if (!(dwCreateFlags & CREATE_SUSPENDED))

#0080 VERIFY(pThread->ResumeThread() != (DWORD)-1);

#0081

#0082 return pThread;

#0083 }

#0084

#0085 BOOL CWinThread::CreateThread(DWORD dwCreateFlags, UINT nStackSize,

#0086 LPSECURITY_ATTRIBUTES lpSecurityAttrs)

#0087 {

#0088 // setup startup structure for thread initialization

#0089 _AFX_THREAD_STARTUP startup; memset(&startup, 0, sizeof(startup));

#0090 startup.pThreadState = AfxGetThreadState();

#0091 startup.pThread = this;

#0092 startup.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);

#0093 startup.hEvent2 = ::CreateEvent(NULL, TRUE, FALSE, NULL);

#0094 startup.dwCreateFlags = dwCreateFlags;

#0095 ...

#0096 // create the thread (it may or may not start to run)

#0097 m_hThread = (HANDLE)_beginthreadex(lpSecurityAttrs, nStackSize,

#0098 &_AfxThreadEntry, &startup, dwCreateFlags | CREATE_SUSPENDED, (UINT*)&m_nThreadID);

#0099 ...

#0100 }

图14-5 CWinThread 的相关源代码

产生执行线程,为什么不直接用::CreateThread 或_beginthreadex?为什么要透过

CWinThread 对象?我想你可以轻易从MFC 源代码中看出,因为

CWinThread::CreateThread 和AfxBeginThread 不只是::CreateThread 的一层包装,更做

了一些application framework 所需的内部数据初始化工作,并确保使用正确的C runtime

library 版本。源代码中有:

#ifndef _MT

... // 做些设定工作,不产生执行线程,回返。

#else

... // 真正产生执行线程,回返。

#endif //!_MT)

的动作,只是被我删去未列出而已。

接下来我要把worker thread 和UI thread 的产生步骤做个整理。它们都需要调用AfxBeginThread 以产生一个CWinThread 对象,但如果要产生一个UI thread,你还必须先定义一个CWinThread 衍生类别。

产生一个Worker Thread

Worker thread 不牵扯使用者接口。你应该为它准备一个执行线程函数,然后调用AfxBeginThread:

CWinThread* pThread = AfxBeginThread(ThreadFunc, &Param);

...

UINT ThreadFunc (LPVOID pParam)

{

...

}

CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc,

LPVOID pParam,

int nPriority = THREAD_PRIORITY_NORMAL,

UINT nStackSize = 0,

DWORD dwCreateFlags = 0,

LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);

AfxBeginThread 事实上一共可以接受六个参数,分别是:

参数一pfnThreadProc 表示执行线程函数。参数二pParam 表示要传给执行线程函数的参

数。参数三nPriority 表示优先权的微调值,预设为THREAD_PRIORITY_NORMAL,也

就是没有微调。参数四nStackSize 表示堆栈的大小,默认值0 则表示堆栈最大容量为

1MB。参数五dwCreateFlags 如果为默认值0,就表示执行线程产生后立刻开始执行;如

果其值为CREATE_SUSPENDED,就表示执行线程产生后先暂停执行。之后你可以使用

CWinThread::ResumeThread 重新执行它。参数六lpSecurityAttrs 代表新执行线程的安全防

护属性。默认值NULL 表示此一属性与其产生者(也是个执行线程)的属性相同。

在这里我们遭遇到一个困扰。执行线程函数是由系统调用的,也就是个callback 函数,不

容许有this 指针参数。所以任何一般的C++ 类别成员函数都不能够拿来当做执行线程函

式。它必须是个全域函数,或是个C++ 类别的static 成员函数。其原因我已经在第6

章的「Callback 函数」一节中描述过了,而采用全域函数或是C++ static 成员函数,其

间的优劣因素我也已经在该节讨论过。

执行线程函数的类型AFX_THREADPROC 定义于AFXWIN.H 之中:

// in AFXWIN.H

typedef UINT (AFX_CDECL *AFX_THREADPROC)(LPVOID);

所以你应该把本身的执行线程函数声明如下(其中的pParam 是个指针,在实用上可以指

向程序员自定的数据结构):

UINT ThreadFunc (LPVOID pParam);

否则,编译时会获得这样的错误消息:

有时候我们会让不同的执行线程使用相同的执行线程函数,这时候你就得特别注意到执行线程

函数使用全域变量或静态变量时,数据共享所引发的严重性(有好有坏)。至于放置在

堆栈中的变量或对象,都不会有问题,因为每一个执行线程自有一个堆栈。

实验七:Linux多线程编程(实验分析报告)

实验七:Linux多线程编程(实验报告)

————————————————————————————————作者:————————————————————————————————日期:

实验七:Linux多线程编程(4课时) 实验目的:掌握线程的概念;熟悉Linux下线程程序编译的过程;掌握多线程程序编写方法。 实验原理:为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。 1 多线程概念 使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间。 使用多线程的理由之二是线程间方便的通信机制。同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。2多线程编程函数 Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义: typedef unsigned long int pthread_t; 它是一个线程的标识符。 函数pthread_create用来创建一个线程,它的原型为: extern int pthread_create((pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)); 第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。 函数pthread_join用来等待一个线程的结束。函数原型为: extern int pthread_join(pthread_t th, void **thread_return); 第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。 函数pthread_exit的函数原型为: extern void pthread_exit(void *retval); 唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给thread_return。 3 修改线程的属性 线程属性结构为pthread_attr_t,它在头文件/usr/include/pthread.h中定义。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。 设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。 另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。 4 线程的数据处理

Linux多线程编程的基本的函数

Posix线程编程指南(一) 线程创建与取消 这是一个关于Posix线程编程的专栏。作者在阐明概念的基础上,将向您详细讲述Posix线程库API。本文是第一篇将向您讲述线程的创建与取消。 线程创建 1.1 线程与进程 相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。在串行程序基础上引入线程和进程是为了提高程序的并发度,从而提高程序运行效率和响应时间。 线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。 1.2 创建线程 POSIX通过pthread_create()函数创建线程,API定义如下: 与fork()调用创建一个进程的方法不同,pthread_create()创建的线程并不具备与主线程(即调用pthread_create()的线程)同样的执行序列,而是使其运行 start_routine(arg)函数。thread返回创建的线程ID,而attr是创建线程时设置的线程属性(见下)。pthread_create()的返回值表示线程创建是否成功。尽管arg是void *类型的变量,但它同样可以作为任意类型的参数传给start_routine()函数;同时,start_routine()可以返回一个void *类型的返回值,而这个返回值也可以是其他类型,并由pthread_join()获取。 1.3 线程创建属性 pthread_create()中的attr参数是一个结构指针,结构中的元素分别对应着新线程的运行属性,主要包括以下几项: __detachstate,表示新线程是否与进程中其他线程脱离同步,如果置位则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为 PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态。

Windows多线程程序设计

Windows多线程程序设计- - 1、产生一个线程,只是个框架,没有具体实现。理解::CreateThread函数用法。 #include DWORD WINAPI ThreadFunc(LPVOID); int main() { HANDLE hThread; DWORD dwThreadID; hThread = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(ThreadFunc), NULL, 0, &dwThreadID); ...; return 0; } DWORD WINAPI ThreadFunc(LPVOID lParam) { ...; return 0; } 2、一个真正运转的多线程程序,当你运行它的时候,你会发现(也可能会害怕),自己试试吧。说明了多线程程序是无法预测其行为的,每次运行都会有不同的结果。 #include #include using namespace std; DWORD WINAPI ThreadFunc(LPVOID); int main() { HANDLE hThread; DWORD dwThreadID; // 产生5个线程 for(int i=0; i<5; i++)

{ hThread = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(ThreadFunc), (LPVOID)&i, 0, &dwThreadID); if(dwThreadID) cout << "Thread launched: " << i << endl; } // 必须等待线程结束,以后我们用更好的处理方法 Sleep(5000); return 0; } DWORD WINAPI ThreadFunc(LPVOID lParam) { int n = (int)lParam; for(int i=0; i<3; i++) { cout << n <<","<< n <<","<< n << ","< } return 0; } 3、使用CloseHandle函数来结束线程,应该是“来结束核心对象的”,详细要参见windows 多线程程序设计一书。 修改上面的程序,我们只简单的修改if语句。 if(dwThreadID) { cout << "Thread launched: " << i << endl; CloseHandle(dwThreadID); } 4、GetExitCodeThread函数的用法和用途,它传回的是线程函数的返回值,所以不能用GetExitCodeThread的返回值来判断线程是否结束。 #include #include using namespace std;

实验五 多线程程序设计(汽院含答案)

实验五多线程程序设计 实验目的 1.掌握Java语言中多线程编程的基本方法 2.掌握Runnable接口实现多线程的方法 3.掌握Thread类实现多线程的用法 实验导读 1.进程和线程的概念 进程是程序一次动态执行的过程,对应从代码加载、执行到执行结束这样一个完整的过程,也是进程自身从产生、发展到消亡的过程。 线程是比进程更小的执行单元,一个进程在执行过程中,可以产生多个线程。每个线程都有自身的产生、执行和消亡的过程。 2.线程的状态与生命周期 ●新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。此时它 已经有了相应的内存空间和其他资源。 ●运行:线程创建之后就具备了运行的条件,一旦轮到它来享用CPU资源时,即JVM将CPU使用权 切换给该线程时,此线程的就可以脱离创建它的主线程独立开始自己的生命周期了(即run方法执行的过程)。 ●中断:有4种原因的中断,CPU资源从当前线程切换给其他线程、执行了sleep(int millsecond)方法、 执行了wait()方法、进入阻塞状态。 ●死亡:run方法结束。 3.线程的创建 在Java语言中,与线程支持密切相关的是https://www.360docs.net/doc/af520518.html,ng.Thread类和https://www.360docs.net/doc/af520518.html,ng.Runnable接口。Runnable接口定义很简单,只有一个run方法。任何一个类如果希望自己的实例能够以线程的形式执行,都可以来实现Runnable接口。 继承Thread类和实现Runnable接口,都可以用来创建Thread对象,效果上并没有什么不同。继承Thread 类的方法很明显的缺点就是这个类不能再继承其他的类了,而实现Runnable接口不会有这个麻烦。 另外,在继承Thread类的代码中,this其实就是指当前正在运行的线程对象,如果使用实现Runnable 接口的方式,要得到当前正在执行的线程,需要使用Thread.currentThread()方法。 线程创建后仅仅是占有了内存资源,在JVM管理的线程中还没有这个线程,此线程必须调用start()方法(从父类继承的方法)通知JVM,这样JVM就会知道又有一个新一个线程排队等候切换了。 注意:多次启动一个线程,或者启动一个已经运行的线程对象是非法的,会抛出IllegalThreadStateException异常对象。 4.线程的优先级 同一时刻在等待队列中的线程会有很多个,它们各自任务的重要性有所不同。为了加以区分,使工作安排和资源分配时间更为合理,每个线程可以被赋予不同的优先级,让任务比较急的线程拥有更高的优先级,从而更快地进入执行状态。 Java中提供了10个等级的线程优先级,最低为Thread.MIN_PRIORITY=1,最高为

linux下的多线程编程常用函数

Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所特 有的系统调用,他的使用方式类似fork. int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr, void *(*start_rtn)(void),void *restrict arg); 返回值:若是成功建立线程返回0,否则返回错误的编号 形式参数: pthread_t *restrict tidp 要创建的线程的线程id指针 const pthread_attr_t *restrict attr 创建线程时的线程属性 void* (start_rtn)(void) 返回值是void类型的指针函数 void *restrict arg start_rtn的行参 进行编译的时候要加上-lpthread 向线程传递参数。 例程2: 功能:向新的线程传递整形值 #include #include #include void *create(void *arg) { int *num; num=(int *)arg; printf("create parameter is %d \n",*num); return (void *)0; } int main(int argc ,char *argv[]) { pthread_t tidp; int error; int test=4; int *attr=&test; error=pthread_create(&tidp,NULL,create,(void *)attr); if(error) { printf("pthread_create is created is not created ... \n"); return -1; } sleep(1); printf("pthread_create is created ...\n");

Step by Step:Linux C多线程编程入门(基本API及多线程的同步与互斥)

介绍:什么是线程,线程的优点是什么 线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲,同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。 线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在Unix系统中,一个进程包含很多东西,包括可执行程序以及一大堆的诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,那么通信就需要在用户空间和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。 Hello World(线程创建、结束、等待) 创建线程 pthread_create 线程创建函数包含四个变量,分别为: 1. 一个线程变量名,被创建线程的标识 2. 线程的属性指针,缺省为NULL即可 3. 被创建线程的程序代码 4. 程序代码的参数 For example: - pthread_t thrd1? - pthread_attr_t attr? - void thread_function(void argument)? - char *some_argument? pthread_create(&thrd1, NULL, (void *)&thread_function, (void *) &some_argument); 结束线程 pthread_exit 线程结束调用实例:pthread_exit(void *retval); //retval用于存放线程结束的退出状态 线程等待 pthread_join pthread_create调用成功以后,新线程和老线程谁先执行,谁后执行用户是不知道的,这一块取决与操作系统对线程的调度,如果我们需要等待指定线程结束,需要使用pthread_join函数,这个函数实际上类似与多进程编程中的waitpid。 举个例子,以下假设 A 线程调用 pthread_join 试图去操作B线程,该函数将A线程阻塞,直到B线程退出,当B线程退出以后,A线程会收集B线程的返回码。 该函数包含两个参数:pthread_t th //th是要等待结束的线程的标识 void **thread_return //指针thread_return指向的位置存放的是终止线程的返回状态。 调用实例:pthread_join(thrd1, NULL); example1: 1 /************************************************************************* 2 > F i l e N a m e: t h r e a d_h e l l o_w o r l d.c 3 > A u t h o r: c o u l d t t(f y b y) 4 > M a i l: f u y u n b i y i@g m a i l.c o m 5 > C r e a t e d T i m e: 2013年12月14日 星期六 11时48分50秒 6 ************************************************************************/ 7 8 #i n c l u d e 9 #i n c l u d e 10 #i n c l u d e

11 12 v o i d p r i n t_m e s s a g e_f u n c t i o n (v o i d *p t r)? 13 14 i n t m a i n() 15 { 16 i n t t m p1, t m p2?

基于多线程的端口扫描程序课程设计报告

滁州学院 课程设计报告 课程名称: 设计题目:基于多线程的端口扫描程序 院部:计算机与信息工程学院 专业:网络工程 组别:第六组 起止日期: 2012 年12月31日~2013 年1月6日指导教师: 计算机与信息工程学院二○一二年制

课程设计任务书 目录 1 需求分析. 0 1..1 网络安全 0 1.2 课程背景 0 1.3 扫描器 0 1.4 多线程扫描器介绍 (1) 错误! 未定义书签。

错误! 未定义书签。 错误! 未定义书签。 错误! 未定义书签。 1.5 端口扫描 (2) 2 概要设计. (3) 2.1 整体框架设计 (3) 2.2 流程图描述 (3) 3 详细设计. (3) 3.1 端口扫描线程启动 (3) 3.2 GUI 图形界面 (5) 3.3 按钮监听及异常处理 (6) 4 调试与操作说明. (8) 4.1 运行界面 (8) 4.2 扫描结果 (8) 4.3 错误提示 (8) 5 课程设计总结与体会. (8) 6 参考文献. (9) 7 致谢. (9) 8 附录. 0 1 需求分析 1..1 网络安全二十一世纪是信息化、网络化的世纪,信息是社会发展的重要资源。信息安全保障能力是一个国家综合国力、经济竞争实力和生存能力的重要组成部分,是世界各国在奋力攀登的制高点。网络安全是指网络系统的硬件、软件及其系统中的数据受到保护,不因偶然的或者恶意的原因而遭到破坏、更改、泄露,系统连续可靠正常地运行。网络安全包括技术领域和非技术领域两大部分: 非技术领域包括一些制度、政策、管理、安全意识、实体安全

等方面的内容; 技术领域包括隐患扫描、防火墙、入侵检测、访问控制、虚拟专用网、CA 认证、操作系统等方面的内容。这些技术的目标是保证信息的可控性、可用性、保密性、完整性、和不可抵赖性。端口扫描属于安全探测技术范畴,对应于网络攻击技术中的网络信息收集技术。 1.2 课程背景 随着Internet 的不断发展,信息技术已成为促进经济发展、社会进步的巨大推动力。端口扫描技术是网络安全扫描技术一个重要的网络安全技术。与防火墙、入侵检测系统互相配合,能够有效提高网络的安全性。安全扫描是安全技术领域中重要的一类。通过扫描能自动检测远端或本地主机系统信息,包括主机的基本信息(如计算机名、域名、组名、操作系统 型等)、服务信息、用户信息以及漏洞信息,它的重要性在于能够对网络进行安全评估,及时发现安全隐患,防患于未然。 网络的安全状况取决于网络中最薄弱的环节,任何疏忽都有可能引入不安全的因素,最有效的方法是定期对网络系统进行安全分析,及时发现并修正存在的脆弱,保证系统安全。 国外安全扫描技术的历史可以追溯到20 世纪90 年代,当时因特网刚刚起步,但是在过去的十年内,扫描技术飞速发展,迄今为止,其扫描技术已经非常完善,但是在全面性,隐蔽性和智能性上还有待提高。安全扫描从最初专门为UNIX 系统而编写的一些只有简单功能的小程序发展到现在,已经出现了可以运行多个操作系统平台上的,具有复杂功能的系统程序。 国内的扫描技术是在国外的扫描器基础上发展起来的。其中有一些专门从事安全技术的公司。这些公司的扫描器以硬件为主,其特点是执行速度快,不像软件一样受到安装主机系统的限制。 然而对于更多的基于主机的端口扫描而言,简单,实用,可靠才是它们的长处。 1.3 扫描器扫描器是一种自动检测远程或本地主机安全性弱点的程序,通过使用扫描器你可以不留痕迹的发现远程服务器的各种TCP端口的分配。这就能让我们间接的或直观的了解到远程主机所存在的安全问题。为了保证网络中计算机的安全性,必须采取主动策略, 快速、及时、准确、安全的检测出网络中计算机及防火墙开放的和未开放的端口。计算机端口扫描技术就是这种主动防御策略实现的重要技术手段。 扫描器采用模拟攻击的形式对目标可能存在的已知安全漏洞进行逐项检查。目标可以是工作站、服务器、交换机、数据库应用等各种对象。然后根据扫描结果向系统管理员提供周 密可靠的安全性分析报告,为提高网络安全整体水平产生重要依据。在网络安全体系的建设中,安全扫描工具花费低、效果好、见效快、与网络的运行相对对立、安装运行简单,可以大规模减少安全管理员的手工劳动,有利于保持全网安全政策的统一和稳定。 1.4 多线程扫描器介绍 在java 中,组件放置在窗体上的方式是完全基于代码的。组件放置在窗体上的方式通常不是通过绝对坐标控制,而是由“布局管理器”根据组件加入的顺序决定其位置。每个容器都有一个属于的自己布局管理器。使用不同的布局管理器,组件大小,位置和形状将大不相同。表格型布局管理器将容器划分成为一个多行多列的表格,表格的大小全部相同,是由其中最大的组件所决定。通过add 方法可以将组件一一放在每个表格

基于ARM的多线程应用程序设计

开放性实验报告 题目: 基于ARM的多线程应用程序设计院系名称:电气工程学院 专业班级:自动1302 学生姓名:张鹏涛 学号:201323020219 指导教师:张晓东

目录 1 系统概述与设计要求 (2) 1.1 系统概述 (2) 1.2 设计要求 (2) 2 方案论证 (2) 2.1 实现方法 (2) 2.2 线程优势 (2) 3 硬件设计 (3) 3.1 树莓派接口驱动LED电路设计 (3) 4 软件设计 (4) 4.1 驱动三色LED灯 (4) 4.1.1 驱动实现方法 (4) 4.1.2 wiringPi库安装和软件编程 (5) 4.2 服务器和客户端 (5) 4.2.1 服务器设计方法 (5) 4.2.2 客户端设计方法 (6) 5 系统调试 (6) 设计心得 (8) 参考文献 (9) 附录1(LED驱动程序) (10) 附录2(服务器程序) (10) 附录3(客户端程序代码) (14)

1 系统概述与设计要求 1.1 系统概述 本系统设计是基于树莓派开发板上实现的,树莓派由注册于英国的慈善组织“Raspberry Pi 基金会”开发,Eben·Upton/埃·厄普顿为项目带头人。2012年3月,英国剑桥大学埃本·阿普顿(Eben Epton)正式发售世界上最小的台式机,又称卡片式电脑,外形只有信用卡大小,却具有电脑的所有基本功能,这就是Raspberry Pi电脑板,中文译名"树莓派"。它是一款基于ARM的微型电脑主板,以SD/MicroSD 卡为内存硬盘,卡片主板周围有1/2/4个USB接口和一个10/100 以太网接口(A型没有网口),可连接键盘、鼠标和网线,同时拥有视频模拟信号的电视输出接口和HDMI高清视频输出接口,以上部件全部整合在一张仅比信用卡稍大的主板上,具备所有PC的基本功能。而树莓派2具有900MHz内核频率,4核ARM Cortex-A7,1GB 内存,带Micro SD 卡插槽(支持通过它启动Linux 操作系统,如Fedora),40PIN接口(可以增加驱动外设)。本系统设计正式在树莓派2环境下开发实现多线程设计,设计的主要功能就是两个客户端通过服务器互相收发信息。 1.2 设计要求 要求多个客户端能够同时连接服务器,而服务器需要创建线程来管理这多个客户端,并且能够把一个客户端发来的数据进行解析,发给另一个客户端,实现两个甚至多个客户端互相收发信息。能够通过驱动三色灯来发现系统运行的状态,红色说明有错误发生,绿色说明正在正常运行,蓝色说明有用户连接,绿色说明有客户端互相收发信息。 2 方案论证 2.1 实现方法 要实现服务器同时管理两个甚至多个客户端,就必须引入进程或线程。 2.2 线程优势 一是和进程相比,它是一种非常"节俭"的多任务操作方式。

跟我学Linux编程-13-多线程编程-性能

多线程编程-性能 在上一章节中,我们介绍了线程互斥锁的使用。通过互斥锁,使得每个线程只能串行地运行临界区代码,从而有效地避免了多线程冲突。串行的代码运行方式削弱了多线程并发运行的特性,因此线程锁也潜在地降低了程序性能,如何杜绝线程冲突,又尽可能不影响程序效率,是我们每一个线程从员需要认真考虑的事情。 减少线程性能下降的方法有如下几点: 1 尽可能减小互斥锁的颗粒,使串行代码的比例减小从而提高效率,这在上一章节末已提过。 2 加锁解锁之间的代码,运行时间尽可能减少,避免有sleep或死循环一类的超时等待。 3 对不同的冲突资源使用不同的线程锁,避免不相关的线线之间因锁反而产生关联。 4 使用pthread_mutex_trylock代替pthread_lock在检测和处理锁冲突,实现单线程对多个对像的非阻塞处理。 今天我们重点介绍pthread_mutex_trylock的使用,并通过实例的来展现其用法,探究其提高程序效果的原理。 首先,我们来看pthread_lock与pthread_mutex_trylock的函数原型: int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock( pthread_mutex_t *mutex); 可以看到,这两个函数的原型非常想像,功能也比较类似,都是尝试对给定的锁对像进行加锁,如果成功,则线程获得该锁,并返回0;不同点在于当锁已被其他线程占有的情况下,pthread_mutex_lock会阻塞,直至锁被其它线程释放并且本线程获得锁;如pthread_mutex_trylock则不然,如果锁当前已被其他线程占有,则立刻返回失败(非0值),我们的程序可判读返回值并进行下一步的处理(如处理另一个任务),避免线程被阻塞,从而提高了线程并发度,提升程序性能。我们接下来看例子: #include #include #include typedef struct { int m_cnt[3]; pthread_mutex_t m_mutex; } count_t; #define COUNT_CNT 20 #define CYC_CNT 10000 count_t g_counts[COUNT_CNT]; void *thread_task1(void *arg)

多线程编程的原则及要点

2.4多线程编程的原则及要点: 随着多核CPU的出世,多核编程方面的问题将摆上了程序员的日程,有许多老的程序员以为早就有多CPU的机器,业界在多CPU机器上的编程已经积累了很多经验,多核CPU上的编程应该差不多,只要借鉴以前的多任务编程、并行编程和并行算法方面的经验就足够了。 但是,多核机器和以前的多CPU机器有很大的不同,以前的多CPU机器都是用在特定领域,比如服务器,或者一些可以进行大型并行计算的领域,这些领域很容易发挥出多CPU的优势,而现在多核机器则是应用到普通用户的各个层面,特别是客户端机器要使用多核CPU,而很多客户端软件要想发挥出多核的并行优势恐怕没有服务器和可以进行大型并行计算的特定领域简单。 多核CPU中,要很好地发挥出多个CPU的性能的话,必须保证分配到各个CPU上的任务有一个很好的负载平衡。否则一些CPU在运行,另外一些CPU处于空闲,无法发挥出多核CPU 的优势来。 要实现一个好的负载平衡通常有两种方案,一种是静态负载平衡,另外一种是动态负载平衡。 1、静态负载平衡 静态负载平衡中,需要人工将程序分割成多个可并行执行的部分,并且要保证分割成的各个部分能够均衡地分布到各个CPU上运行,也就是说工作量要在多个任务间进行均匀的分配,使得达到高的加速系数。 2、动态负载平衡 动态负载平衡是在程序的运行过程中来进行任务的分配达到负载平衡的目的。实际情况中存在许多不能由静态负载平衡解决的问题,比如一个大的循环中,循环的次数是由外部输入的,事先并不知道循环的次数,此时采用静态负载平衡划分策略就很难实现负载平衡。 动态负载平衡中对任务的调度一般是由系统来实现的,程序员通常只能选择动态平衡的调度策略,不能修改调度策略,由于实际任务中存在很多的不确定因素,调度算法无法做得很优,因此动态负载平衡有时可能达不到既定的负载平衡要求。 3、负载平衡的难题在那里? 负载平衡的难题并不在于负载平衡的程度要达到多少,因为即使在各个CPU上分配的任务执行时间存在一些差距,但是随着CPU核数的增多总能让总的执行时间下降,从而使加速系数随CPU核数的增加而增加。 负载平衡的困难之处在于程序中的可并行执行块很多要靠程序员来划分,当然CPU核数较少时,比如双核或4核,这种划分并不是很困难。但随着核数的增加,划分的粒度将变得越来越细,到了16核以上时,估计程序员要为如何划分任务而抓狂。比如一段顺序执行的代码,放到128核的CPU上运行,要手工划分成128 个任务,其划分的难度可想而知。

跟我学Linux编程-12-多线程编程-同步

多线程编程-同步 在上一章节中,我们通过程序示例,见证了单线程世界中不可能发生的事件(一个数既是奇数又是偶数)在多线程环境中是怎样分分钟发生的,我通过细分程序执行步骤,分析了奇异事件发生的过程,并探明了其原因:一个线程在对全局变量gcnt进行两次判读的过程中,另一个线刚好改变了这个变量的值。在多线程编程术语中,称这两个线程同时进入了临界区域。 所谓临界区域,是指多线程环境下两个及以上线程同时执行可能会导致冲突的一段代码。在上一章节的示例中,这几行代码就是一个临界区域: gcnt++; if (gcnt % 2) { if (!(gcnt % 2)) printf("[%d] : %d\n", id, gcnt); } 冲突之所以会发生,是因为临界区域的代码,通常需要很多个CPU指令周期才能完成,其运行过程随时可能被打断(进行了线程调试),CPU去运行另外的线程,如果这个线程刚好也进入了临界区域,则异常的程序状态极可能会发生。 如果当某个线程进入临界区域,在其退出区域之前,其他的线程无论如何也不能进入该区域,那么冲突就不会发生。Linux提供了这种保证多线程进入临界区域互斥的机制,这正是本章节所要介绍的内容:线程锁。 我们今天的示例程序还是在上一章节的示例上改进而来的,我们的任务就是使用线程锁,保证“一个数既是奇数又是偶数”的奇异事件在多线程环境下也不发生,代码如下: #include #include #include int gcnt = 0; pthread_mutex_t g_mutex; void *thread_task(void *arg) { int id = (int)arg; while (1) { pthread_mutex_lock(&g_mutex); gcnt++; if (gcnt % 2)

2嵌入式系统设计实验二(多线程)

注意实验二是在实验一的基础上完成其内容,具体环境配置见实验一 目录 实验二多线程应用程序设计 (2) 2.1实验目的 (2) 2.2、实验内容 (2) 2.3、预备知识 (2) 2.4、实验设备及工具 (2) 2.5、实验原理及代码分析 (3) 2.6、实验步骤 (11) 2.7、思考题 (13)

实验二多线程应用程序设计 2.1实验目的 ?了解多线程程序设计的基本原理。 ?学习pthread库函数的使用。 2.2、实验内容 ?读懂pthread.c的源代码,熟悉几个重要的pthread库函数的使用。掌握共享 锁和信号量的使用方法。 ?进入/root/share/exp/basic/02_pthread目录,运行make 产生pthread程序, 使用NFS方式连接开发主机进行运行实验。 2.3、预备知识 ?有C语言基础 ?掌握在Linux下常用编辑器的使用 ?掌握Makefile 的编写和使用 ?掌握Linux下的程序编译与交叉编译过程 2.4、实验设备及工具 ?硬件:UP-TECH S2410/P270 DVP嵌入式实验平台,PC机Pentium 500以上, 硬 盘40G以上,内存大于128M。 ?软件:PC机操作系统REDHAT LINUX 9.0 +MINICOM + ARM-LINUX开发环境

2.5、实验原理及代码分析 1.多线程程序的优缺点 ?多线程程序作为一种多任务、并发的工作方式,有以下的优点: ?1)提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很 长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操 作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线 程,可以避免这种尴尬的情况。 ?2)使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不 同的线程运行于不同的CPU上。 ?3)改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个 独立或半独立的运行部分,这样的程序会利于理解和修改。 ?Libc中的pthread库提供了大量的API函数,为用户编写应用程序提供支持。2.实验源代码与结构流程图 ?本实验为著名的生产者-消费者问题模型的实现,主程序中分别启动生产者线 程和消费者线程。生产者线程不断顺序地将0到1000的数字写入共享的循环 缓冲区,同时消费者线程不断地从共享的循环缓冲区读取数据。流程图如图 2.2.1所示: 图2.1生产者-消费者实验源代码结构流程图

linux多线程编程

2.终止线程 (2) 3. 等待线程终止 (2) pthread_exit和pthread_join进一步说明: (3) 4.分离线程 (7) 5.获取线程标识符 (8) 6.比较线程ID (8) 7.一次性初始化 (8) 8. 设置线程的调度策略和优先级 (9) 9. 获取线程的优先级 (11) 10.取消线程 (12) 取消线程,是否会释放线程的所有资源?例子: (14) 设置取消类型 (16) 11.初始化属性 (17) 12.设置分离状态 (18) 13.设置范围 (18) 14. 设置继承的调度策略 (18) 16. 设置调度参数 (19) 17.初始化互斥锁 (21) 18.销毁互斥锁 (21) 19.锁定互斥锁 (22) 20.解除锁定互斥锁 (23) 21. 互斥锁的类型: (23) 22. 初始化互斥锁属性对象 (23) 23. 销毁互斥锁属性对象 (23) 24.设置互斥锁类型的属性 (24) 互斥锁动态初始化和静态初始化区别: (26) 销毁互斥锁:事实上没做任何销毁操作,如下: (27) 非递归类型的互斥锁解锁和加锁操作: (27) 29.初始化条件变量 (27) 30.基于条件变量阻塞 (27) 31.解除阻塞一个线程 (28) 31.解除阻塞所有线程 (29) 33. 在指定的时间之前阻塞 (30) 32.唤醒丢失问题 (31) 33. 计数信号量概述 (31) 34. 初始化信号 (31) 35. 增加信号 (31) 36. 基于信号计数进行阻塞 (32) 37.多线程链表添加删除例子(使用条件变量实现互斥): (32) 38.为线程特定数据创建键 (34) 39. 删除线程特定数据键 (35) 40.设置线程特定数据 (35) 41. 获取线程特定数据 (35)

POSIX线程程序设计(中文版)

POSIX 多线程程序设计 Blaise Barney, Lawrence Livermore National Laboratory 目录表 1.摘要 2.Pthreads 概述 1.什么是线程? 2.什么是Pthreads? 3.为什么使用Pthreads? 4.使用线程设计程序 3.Pthreads API编译多线程程序 4.线程管理 1.创建和终止线程 2.向线程传递参数 3.连接(Joining)和分离(Detaching)线程 4.栈管理 5.其它函数 5.互斥量(Mutex Variables) 1.互斥量概述 2.创建和销毁互斥量 3.锁定(Locking)和解锁(Unlocking)互斥量 6.条件变量(Condition Variable) 1.条件变量概述 2.创建和销毁条件变量 3.等待(Waiting)和发送信号(Signaling) 7.没有覆盖的主题 8.Pthread 库API参考 9.参考资料 在多处理器共享内存的架构中(如:对称多处理系统SMP),线程可以用于实现程序的并行性。历史上硬件销售商实现了各种私有版本的多线程库,使得软件开发者不得不关心它的移植性。对于UNIX系统,IEEE POSIX 1003.1标准定义了一个C语言多线程编程接口。依附于该标准的实现被称为POSIX theads 或Pthreads。 该教程介绍了Pthreads的概念、动机和设计思想。内容包含了Pthreads API主要的三大类函数:线程管理(Thread Managment)、互斥量(Mutex Variables)和

条件变量(Condition Variables)。向刚开始学习Pthreads的程序员提供了演示例程。 适于:刚开始学习使用线程实现并行程序设计;对于C并行程序设计有基本了解。不熟悉并行程序设计的可以参考EC3500: Introduction To Parallel Computing。 什么是线程? ?技术上,线程可以定义为:可以被操作系统调度的独立的指令流。但是这是什么意思呢? ?对于软件开发者,在主程序中运行的“函数过程”可以很好的描述线程的概念。 ?进一步,想象下主程序(a.out)包含了许多函数,操作系统可以调度这些函数,使之同时或者(和)独立的执行。这就描述了“多线程”程序。 ?怎样完成的呢? ?在理解线程之前,应先对UNIX进程(process)有所了解。进程被操作系统创建,需要相当多的“额外开销”。进程包含了程序的资源和执行状态信息。如下: o进程ID,进程group ID,用户ID和group ID o环境 o工作目录 o程序指令 o寄存器 o栈 o堆 o文件描述符 o信号动作(Signal actions) o共享库 o进程间通信工具(如:消息队列,管道,信号量或共享内存)

Linux多线程编程问题

Linux 多线程编程问题 1重入问题 传统的UNIX没有太多考虑线程问题,库函数里过多使用了全局和静态数据,导致严重的线程重入问题。 1.1–D_REENTRANT /-pthread和errno的重入问题。 所先UNIX的系统调用被设计为出错返回-1,把错误码放在errno中(更简单而直 接的方法应该是程序直接返回错误码,或者通过几个参数指针来返回)。由于线程 共享所有的数据区,而errno是一个全局的变量,这里产生了最糟糕的线程重入问 题。比如: do { bytes = recv(netfd, recvbuf, buflen, 0); } while (bytes != -1 && errno != EINTR); 在上面的处理recv被信号打断的程序里。如果这时连接被关闭,此时errno应该不 等于EINTR,如果别的线程正好设置errno为EINTR,这时程序就可能进入死循环。 其它的错误码处理也可能进入不可预测的分支。 在线程需求刚开始时,很多方面技术和标准(TLS)还不够成熟,所以在为了 解决这个重入问题引入了一个解决方案,把errno定义为一个宏: extern int *__errno_location (void); #define errno (*__errno_location()) 在上面的方案里,访问errno之前先调用__errno_location()函数,线程库提供这个 函数,不同线程返回各自errno的地址,从而解决这个重入问题。在编译时加 -D_REENTRANT就是启用上面的宏,避免errno重入。另外-D_REENTRANT 还影响一些stdio的函数。在较高版本的gcc里,有很多嵌入函数的优化,比如把printf(“Hello\n”); 优化为 puts(“hello\n”); 之类的,有些优化在多线程下有问题。所以gcc引入了–pthread 参数,这个 参数出了-D_REENTRANT外,还校正一些针对多线程的优化。 因为宏是编译时确定的,所以没有加-D_REENTRANT编译的程序和库都有errno 重入问题,原则上都不能在线程环境下使用。不过在一般实现上主线程是直接使用 全局errno变量的,也就是__errno_location()返回值为全局&errno,所以那些没加 -D_REENTRANT编译的库可以在主线程里使用。这里仅限于主线程,有其它且只 有一个固定子线程使用也不行,因为子线程使用的errno地址不是全局errno变量 地址。 对于一个纯算法的库,不涉及到errno和stdio等等,有时不加_REENTRANT也是 安全的,比如一个纯粹的加密/解谜函数库。比较简单的判断一个库是否有errno问 题是看看这个库是使用了errno还是__errno_location():

实验二-多线程应用程序设计

成绩 信息与通信工程学院实验报告 课程名称:嵌入式系统原理与应用 实验题目:多线程应用程序设计指导教师: 班级:学号:学生姓名: 一、实验目的和任务 1.掌握VI编译环境。 2.掌握GCC编译命令。 3.掌握多个文件共同编译方法。 4.掌握GDB调试命令。 5.了解多线程程序设计的基本原理。 6.学习 pthread 库函数的使用。 二、实验设备 7.硬件:PC机 8.软件:LINUX操作系统、虚拟机 三、实验内容及原理 1.在VI编辑器里编写两个文件(其中一个为主程序,实现显示“hello,linux world, I am 1405014XXX XXX”,,一个为子程序,实现1~n的乘法),为其书写头文件,共同 编译为可执行文件,执行,观察运行结果。学习书写MAKEFILE文件,编译,执行,观察结果。利用GCC 编译(加参数-g)为可执行文件,利用GDB调试,学习GDB调试命令。 2.编写多线程程序设计。编译并运行,观察结果。(可参照课件或实验指导书)

四、实验步骤或程序流程 1.Gcc编译实验 1)编写实验代码: 图3.1实验主程序 图3.2实验子程序 2)编写Makefile文件: 图3.3 Makefile文件 3)Make执行Makefile文件,生成可执行程序并运行:

图3.4 执行 4)Gdb调试运行: 图3.5 gdb调试显示代码

图3.6 gdb调试断点运行 图3.7 gdb调试逐步运行

2.多线程程序设计: 1)对实验代码进行gcc编译: 图3.7gcc编译生成可执行文件 2)运行结果: 图3.8程序运行结果

相关文档
最新文档