实验3 同步机制(2次)

更新时间:2023-11-18 13:50:01 阅读量: 教育文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

实验3 同步机制

实验内容:

学习Windows有关进程/线程同步的背景知识和API,分析2个实验程序(事件对象在进程间传送信号的应用和利用互斥体和临界区实现读者写者问题),观察程序的运行情况并分析执行结果。 实验目的:

在本实验中,通过对事件和互斥体对象的了解,来加深对Windows 线程同步的理解。 (1) 了解事件和互斥体对象。

(2) 通过分析实验程序,理解管理事件对象的API。 (3) 理解在进程中如何使用事件对象。

(4) 理解在进程中如何使用互斥体和临界区对象。 (5) 理解父进程创建子进程的程序设计方法。 实验要求:

(1) 理解Windows有关进程/线程同步的背景知识和API。

(2) 按要求运行2个程序,观察程序执行的结果,并给出要求的结果分析。

(3) 参照3-2程序,写出一个实现单个生产者—消费者问题的算法,可以使用单个缓冲区,也可以使用缓冲池,生产者随机产生任意形式的数据并放入缓冲区中,消费者则以随机的时间间隔从缓冲区中取数据,随机时间请使用随机数产生。

并发与同步的背景知识

Windows开发人员可以使用线程同步对象来协调线程和进程的工作,以使其共享信息并执行任务。此类对象包括互锁、临界区、事件、互斥体和信号等。

多线程编程中关键的一步是保护所有的共享资源,工具主要有互锁函数、临界段和互斥体等;另一个是协调线程使其完成应用程序的任务,为此,可利用内核中的事件对象和信号。

在进程内或进程间实现线程同步的最方便的方法是使用事件对象,这组内核对象允许一个线程对其受信状态进行直接控制。

而互斥体则是另一个可命名且安全的内核对象,主要目的是引导对共享资源的访问。拥有单一访问资源的线程创建互斥体,所有希望访问该资源的线程应该在实际执行操作之前获得互斥体,而在访问结束时立即释放互斥体,以允许下一个等待线程获得互斥体,然后接着进行下去。

与事件对象类似,互斥体容易创建、打开、使用并清除。利用CreateMutex() API可创建互斥体,创建时可以指定一个初始的拥有权标志,通过使用这个标志,只有当线程完成了资源的所有的初始化工作时,才允许创建线程释放互斥体。

表3-1 用于管理事件对象的API API名称 CreateEvent() OpenEvent() SetEvent() ResetEvent() PulseEvent() 描述 在内核中创建一个新的事件对象。此函数允许有安全性设置、手工还是自动重置的标志以及初始时已接受还是未接受信号状态的标志 创建对已经存在的事件对象的引用。此API函数需要名称、继承标志和所需的访问级别 将手工重置事件转化为已接受信号状态 将手工重置事件转化为非接受信号状态 将自动重置事件对象转化为已接受信号状态。当系统释放所有的等待它的线程时此种转化立即发生

为了获得互斥体,首先,想要访问调用的线程可使用OpenMutex() API来获得指向对象的句柄;然后,线程将这个句柄提供给一个等待函数。当内核将互斥体对象发送给等待线程时,就表明该线程获得了互斥体的拥有权。当线程获得拥有权时,线程控制了对共享资源的访问——必须设法尽快地放弃互斥体。放弃共享资源时需要在该对象上调用ReleaseMute() API。然后系统负责将互斥体拥有权传递给下一个等待着的线程 (由到达时间决定顺序) 。

实验内容与步骤 1. 事件对象

清单3-1程序展示如何在进程间使用事件。

父进程启动时,利用CreateEvent() API创建一个命名的、可共享的事件和子进程,然后等待子进程向事件发出信号并终止父进程。

在创建时,子进程通过OpenEvent() API打开事件对象,调用SetEvent() API使其转化为已接受信号状态。两个进程在发出信号之后几乎立即终止。

清单3-1 创建和打开事件对象在进程间传送信号 // event项目

# include # include # include

// 以下是句柄事件。实际中很可能使用共享的包含文件来进行通讯 static LPCTSTR g_szContinueEvent =\

// 本方法只是创建了一个进程的副本,以子进程模式 (由命令行指定) 工作 BOOL CreateChild() {

// 提取当前可执行文件的文件名

TCHAR szFilename[MAX_PATH] ;

GetModuleFileName(NULL, szFilename, MAX_PATH) ;

// 格式化用于子进程的命令行,指明它是一个EXE文件和子进程 TCHAR szCmdLine[MAX_PATH] ;

sprintf(szCmdLine, \

// 子进程的启动信息结构 STARTUPINFO si;

ZeroMemory(reinterpret_cast(&si), sizeof(si)) ; si.cb = sizeof(si); // 必须是本结构的大小

// 返回的子进程的进程信息结构 PROCESS_INFORMATION pi;

// 使用同一可执行文件和告诉它是一个子进程的命令行创建进程 BOOL bCreateOK = CreateProcess( szFilename, // 生成的可执行文件名 szCmdLine, // 指示其行为与子进程一样的标志 NULL, // 子进程句柄的安全性 NULL, // 子线程句柄的安全性 FALSE, // 不继承句柄 0, // 特殊的创建标志 NULL, // 新环境 NULL, // 当前目录 &si, // 启动信息结构 &pi ) ; // 返回的进程信息结构

// 释放对子进程的引用 if (bCreateOK) {

CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }

return(bCreateOK) ; }

// 下面的方法创建一个事件和一个子进程,然后等待子进程在返回前向事件发出信号 void WaitForChild() {

HANDLE hEventContinue = CreateEvent( NULL, // 缺省的安全性,子进程将具有访问权限 TRUE, // 手工重置事件 FALSE, // 初始时是非接受信号状态 g_szContinueEvent); // 事件名称 if (hEventContinue != NULL) { cout << \事件对象已经建立 …… \ endl; // 创建子进程 if ( CreateChild()) { cout << \建立了一个子进程 \ endl;

// 等待,直到子进程发出信号 cout << \

WaitForSingleObject(hEventContinue, INFINITE);

Sleep(1500); // 删除这句试试,或者改变数值 cout << \ }

// 清除句柄

CloseHandle(hEventContinue);

hEventContinue=INVALID_HANDLE_VALUE; } }

// 以下方法在子进程模式下被调用,其功能只是向父进程发出终止信号 void SignalParent() {

// 尝试打开句柄

cout << \ endl; HANDLE hEventContinue = OpenEvent( EVENT_MODIFY_STATE, // 所要求的最小访问权限 FALSE, // 不是可继承的句柄 g_szContinueEvent); // 事件名称 if(hEventContinue != NULL) {

SetEvent(hEventContinue); cout << \ }

// 清除句柄

CloseHandle(hEventContinue) ;

hEventContinue = INVALID_HANDLE_VALUE; }

int main(int argc, char* argv[] ) {

// 检查父进程或是子进程是否启动

if (argc>1 && strcmp(argv[1] , \ {

// 向父进程创建的事件发出信号 SignalParent() ; } else { // 创建一个事件并等待子进程发出信号 WaitForChild(); Sleep(1500);

cout << \ }

return 0; }

步骤1:编译并执行3-1.exe程序。

程序运行结果是 (分行书写) :

① __________________________________________________________________ ② __________________________________________________________________ ③ __________________________________________________________________ ④ __________________________________________________________________ ⑤ __________________________________________________________________ ⑥ __________________________________________________________________

阅读和分析程序3-1,请回答:

(1) 程序中,创建一个事件使用了哪一个系统函数?创建时设置的初始信号状态是什么?

a. __________________________________________________________________ b. __________________________________________________________________ (2) 创建一个进程 (子进程) 使用了哪一个系统函数?

____________________________________________________________________

(3) 从步骤1的输出结果,对照分析3-1程序,能够看出程序运行的流程吗?请简单描述:

________________________________________________________________________ ________________________________________________________________________ ________________________________________________________________________ ________________________________________________________________________ ________________________________________________________________________ ________________________________________________________________________

步骤2:编译程序生成执行文件3-1.exe,在命令行状态下执行程序,分别使用格式:

(1) 3-1 child

(2) 3-1 或3-1 ***

运行程序,记录执行的结果,并分行说明产生不同结果的原因。 2. 互斥体对象

清单3-2的程序中是读者写者问题的一个实现,满足读者优先原则,使用同步机制的互斥体和临界区,对每个读者和写者分别用一个线程来表示。

测试数据文件的数据格式说明:

测试数据文件包括n行测试数据,每行描述创建的是用于产生读者还是写者的数据。每行测试数据包括4个字段,各字段间用空格分隔。

? ? ? ?

第一字段为线程序号。

第二字段表示相应线程角色,W表示写者,R表示读者。 第三字段为线程延迟。

第四字段为线程读写操作持续时间。

清单3-2 利用互斥体和临界区实现读者写者问题 #include \#include #include #include #include #include #include

#define READER 'R' // 读者 #define WRITER 'W' // 写者

#define INTE_PER_SEC 1000 // 每秒时钟中断数目 #define MAX_THREAD_NUM 64 // 最大线程数目 #define MAX_FILE_NUM 32 // 最大数据文件数目 #define MAX_STR_LEN 32 // 字符串长度 int readcount = 0; // 读者数目 CRITICAL_SECTION RP_Write; // 定义临界区

struct ThreadInfo // 定义线程数据结构 {

int serial; // 线程序号

char entity; // 线程类别(判断是读者线程还是写者线程) double delay; // 线程延迟

double persist; // 线程读写操作持续时间 };

//////////////////////////////////////////////////////////////////////// /// 读者优先——读者线程 /// p:读者线程信息

void RP_ReaderThread(void *p) {

// 互斥变量

HANDLE h_Mutex;

h_Mutex = OpenMutex(MUTEX_ALL_ACCESS,FALSE,\DWORD wait_for_mutex; // 等待互斥变量所有权 DWORD m_delay; // 延迟时间

DWORD m_persist; // 读文件持续时间 int m_serial; // 线程序号

//从参数中获得信息

m_serial = ((ThreadInfo *)(p))->serial;

m_delay = (DWORD)(((ThreadInfo *)(p))->delay*INTE_PER_SEC); m_persist = (DWORD)(((ThreadInfo*)(p))->persist*INTE_PER_SEC); Sleep (m_delay) ; //延迟等待

printf(\

//等待互斥信号,保证对readcount的访问、修改互斥 wait_for_mutex = WaitForSingleObject(h_Mutex,-1); // 读者数目增加 readcount++; if(readcount ==1) {

//第一个读者,等待资源

EnterCriticalSection(&RP_Write); }

ReleaseMutex(h_Mutex) ; //释放互斥信号

// 读文件

printf(\Sleep(m_persist) ;

// 退出线程

printf(\

//等待互斥信号,保证对readcount的访问、修改互斥 wait_for_mutex = WaitForSingleObject(h_Mutex,-1) ; //读者数目减少 readcount-- ;

if(readcount == 0) {

//如果所有读者读完,唤醒写者 LeaveCriticalSection(&RP_Write) ; }

ReleaseMutex(h_Mutex) ; //释放互斥信号 }

//////////////////////////////////////////////////////////////////////// // 读者优先——写者线程 // p:写者线程信息

void RP_WriterThread(void* p) {

DWORD m_delay; //延迟时间

DWORD m_persist; //写文件持续时间 int m_serial; //线程序号

//从参数中获得信息

m_serial = ((ThreadInfo *)(p))->serial;

m_delay = (DWORD)(((ThreadInfo *)(p))->delay*INTE_PER_SEC) ; m_persist = (DWORD)(((ThreadInfo *)(p))->persist*INTE_PER_SEC) ; Sleep (m_delay); //延迟等待

printf(\// 等待资源

EnterCriticalSection (&RP_Write) ;

// 写文件

printf(\Sleep(m_persist) ;

// 退出线程

printf(\//释放资源

LeaveCriticalSection(&RP_Write); }

/////////////////////////////////////////////////////////// // 读者优先处理函数 // file:文件名

void ReaderPriority(char *file) {

DWORD n_thread = 0; // 线程数目 DWORD thread_ID; // 线程ID

DWORD wait_for_all; // 等待所有线程结束

// 互斥对象

HANDLE h_Mutex;

h_Mutex = CreateMutex(NULL,FALSE,\

// 线程对象的数组

HANDLE h_Thread[MAX_THREAD_NUM] ; ThreadInfo thread_info[MAX_THREAD_NUM] ;

readcount = 0 ; // 初始化readcountt InitializeCriticalSection(&RP_Write) ; // 初始化临界区 ifstream inFile; // 打开文件 inFile.open(file) ;

printf(\while(inFile) {

//读人每一个读者、写者的信息 inFile>>thread_info[n_thread].serial ; inFile>>thread_info[n_thread].entity; inFile>>thread_info[n_thread].delay; inFile>>thread_info[n_thread++].persist; inFile.get(); }

for(int i = 0; i < (int)(n_thread); i++) {

if(thread_info[i].entity == READER || thread_info[i].entity == 'r') { // 创建读者线程 h_Thread[i] = CreateThread(NULL,0, (LPTHREAD_START_ROUTINE)(RP_ReaderThread), &thread_info[i], 0,&thread_ID) ; } else { // 创建写者线程 h_Thread[i] = CreateThread(NULL,0, (LPTHREAD_START_ROUTINE)(RP_WriterThread), &thread_info[i], 0 ,&thread_ID) ; } }

//等待所有线程结束

wait_for_all = WaitForMultipleObjects(n_thread,h_Thread,TRUE,-1) ; printf(\ }

/////////////////////////////////////////////////////////////////// //主函数

int main(int argc, char* argv[]) {

char ch;

while ( true ) { printf(\ printf(\ 1: Reader Priority\\n\ printf(\ 2: Exit to Windows\\n\ printf(\ printf( \ //如果输入信息不正确,继续输入 do{ ch = (char)_getch() ; } while(ch != '1' && ch != '2');

system(\

//选择2,返回 if(ch == '2') return 0;

//选择l,读者优先 else

ReaderPriority(\ //结束

printf(\ _getch() ;

system(\}

return 0; }

步骤3:编译并建立3-2.exe可执行文件。在工具栏单击“Execute Program”按钮,执行3-2.exe程序。

(1) 对运行的结果逐行给出分析描述。

____________________________________________________________________ ________________________________________________________________________ ________________________________________________________________________ ________________________________________________________________________ ________________________________________________________________________

(2) 根据运行输出结果,对照分析3-2程序,画出程序运行的流程图

(3) 自己定义一组实验数据并保存到文件后再执行程序(文件名字为thread.dat, 必须放在相同目录下,使用记事本按数据格式要求编辑),分析执行结果,并观察结果是否与设想的一致。

下面是一个测试数据文件的例子: 1 R 3 5

2 W 4 5 3 R 5 2 4 R 6 5 5 W 5.1 3 3. 编程实现生产者—消费者问题

参照3-2程序,写出实现单个生产者—消费者问题的算法,可以使用单个缓冲区,也可以使用缓冲池,生产者随机产生任意形式的数据并放入缓冲区中,消费者则以随机的时间间隔从缓冲区中取数据,随机时间请使用随机数产生。

首先创建一个生产者线程和一个消费者线程,然后生产者与消费者都以各自的速度生产和消费,而缓冲区则是两者的临界资源。

几个API函数说明

1.CreateThread

函数功能:该函数创建一个在调用进程的地址空间中执行的线程。

函数原型:HANDLE CreateThread (LPSECURITY_ATTRIBUTES lpThreadAttributes,

DWORD dwStacksize, LPTHREAD_START_ROUTINE lpStartAddress,

LPVOID lpParameter, DWORD dwCreatiOnFlags, LPDWORD lpThreadId);

参数说明:

? lpThreadAttributes:指向一个SECURITY_ATTRIBUTES结构,该结构决定了返回的句

柄是否可被子进程继承。若lpThreadAttributes为NULL,则句柄不能被继承。

在Windows NT中该结构的lpSecurityDescriptor成员定义了新进程的安全性描述符。若lpThreadAttributes为NULL,则线程获得一个默认的安全性描述符。

? dwStackSize:定义原始堆栈提交时的大小(按字节计)。系统将该值舍人为最近的页。

若该值为0,或小于默认时提交的大小,默认情况是使用与调用线程同样的大小。更多的信息,请看ThreadStackSize。

? lpStartAddress:指向一个LPTHREAD_START_ROUTINE类型的应用定义的函数,该线程

执行此函数。该指针还表示远程进程中线程的起始地址。该函数必须存在于远程进程中。 ? lpParameter:定义一个传递给该进程的32位值。

? dwCreationFlags:定义控制进程创建的附加标志。若定义了CREATE_SUSPENDED标志,

线程创建时处于挂起状态,并且直到ResumeThread函数调用时才能运行。若该值为0,则该线程在创建后立即执行。

? lpThreadId:指向一个32位值,它接收该线程的标识符。

返回值:若函数调用成功,返回值为新线程的句柄;若函数调用失败,返回值为NULL。 2.ExitThread

函数功能:该函数结束一个线程。

函数原型:VOID ExitThread(DWORD dwEextCode)

参数:dwExitCode定义调用线程的退出代码。使用GetExitCodeThread函数来检测一个线

程的退出代码。

返回值:无。

说明:调用ExitThread函数,是结束一个线程的较好的方法。调用该函数后(或者直接地调

用,或者从一个线程过程返回),当前线程的堆栈取消分配,线程终止。若调用该函数时,该线程为进程的最后一个线程,则该线程的进程也被终止。

线程对象的状态变为发信号状态,以释放所有正在等待该线程终止的其他线程。线程的终止状态从STILL_ACTIVATE变为dwExitCode参数的值。

线程结束时不必从操作系统中移去该线程对象。当线程的最后一个句柄关闭时,该线程对象被删除。 3.Sleep

函数功能:该函数对于指定的时间间隔挂起当前的执行线程。 函数原型:VOID Sleep(DWORD dwMilliseconds)

参数:dwMilliseconds:定义挂起执行线程的时间,以毫秒(ms)为单位。取值为0时,该线

程将余下的时间片交给处于就绪状态的同一优先级的其他线程。若没有处于就绪状态的同一优先级的其他线程,则函数立即返回,该线程继续执行。若取值为INFINITE则造成无限延迟。

返回值:该函数没有返回值。

说明:一个线程可以在调用该函数时将睡眠时间设为0ms,以将剩余的时间片交出。

4.CreateMutex

函数功能:该函数创建有名或者无名的互斥对象。

函数原型:HANDLE CreateMutex (LPSECURITY_ATTRIBUTES lpMutexAttributes,

BOOL bInitialOwner, LPCTSTR lpName);

参数:

? lpMutexAttributes:指向SECURITY_ATTRIBUTES结构的指针,该结构决定子进程是否

能继承返回句柄。如果lpMutexAttributes为NULL,那么句柄不能被继承。

在Windows NT中该结构的lpSecurityDescriptor成员指定新互斥对象的安全描述符。如果lpMutexAttributes为NULL,那么互斥对象获得默认的安全描述符。

? bInitialOwner:指定互斥对象的初始所属身份。如果该值为TRUE,并且调用者创建互

斥对象,那么调用线程获得互斥对象所属身份。否则,调用线程不能获得互斥对象所属身份。判断调用者是否创建互斥对象请参阅返回值部分。

? lpName:指向以NULL结尾的字符串,该字符串指定了互斥对象名。该名字的长度小于

MAX_ PATH且可以包含除反斜线(\)路径分隔符以外的任何字符。名字是区分大小写的。 如果lpName与已存在的有名互斥对象名相匹配,那么该函数要求用MUTEX_ALL_ACCESS权限访问已存在的对象。在这种情况下,由于参数bInitialOwner已被创建进程所设置,该参数被忽略。如果参数lpMutexAttributes不为NULL,它决定句柄是否解除继承,但是其安全描述符成员被忽略。

如果lpName为NULL,那么创建的互斥对象无名。

如果lpName与已存在的事件、信号量、可等待定时器、作业或者文件映射对象的名字相匹配,那么函数调用失败,并且GetLastError函数返回ERROR_INVALID_HANDLE,其原因是这些对象共享相同的名字空间。 返回值:

如果函数调用成功,返回值是互斥对象句柄;如果函数调用之前,有名互斥对象已存在,那么函数给已存在的对象返回一个句柄,并且函数GetLastError返回ERROR_ALREADY_EXISTS,否则,调用者创建互斥对象。

如果函数调用失败,则返回值为NULL。若想获得更多错误信息,请调用GetLastError函数。 说明:

由函数CreateMuiex返回的句柄有MUTEX_ALL_ACCESS权限可以去访问新的互斥对象,并且可用在请求互斥对象句柄的任何函数中。

调用进程中的任何线程可以在调用等待函数时指定互斥对象句柄。当指定对象的状态为信号态时,-返回单对象等待函数。当任何一个或者所有的互斥对象都为信号态时,返回多对象等待函数指令。等待函数返回后,等待的线程被释放,继续向下执行。

当一个互斥对象不被任何线程拥有时,处于信号态。创建该对象的线程可以使用bInitialOwner标志来请求立即获得对该互斥对象的所有权。否则,线程必须使用等待函数

本文来源:https://www.bwwdw.com/article/7otv.html

Top