实验二:并发与调度
实验目的
在本实验中,通过对事件和互斥体对象的了解,来加深对Windows进程、线程同步和互斥的理解。
(1) 回顾系统进程、线程的有关概念,加深对Windows进程、线程的理解。
(2) 了解事件和互斥体对象。
(3) 通过分析实验程序,了解管理事件对象的API。
(4) 了解在进程中如何使用事件对象。
(5) 了解在进程中如何使用互斥体对象。
(6) 了解父进程创建子进程的程序设计方法。
实验内容
进程间的同步、线程间的互斥
实验步骤
1、进程间的同步
实验描述:
(1)父进程和子进程使用相同的程序,通过命令行中的”child”识别子进程;
(2)父进程创建一个人工复位事件对象,初始状态是非信号状态,然后创建子进程,利用WaitForSingleObject()无限等待事件;
(3)父进程在一个无限循环中等待事件,获取到事件后输出提示;
(4)子进程打开事件对象,利用SetEvent()发送事件,从而使父进程接收到事件。
Synchronization.cpp
// Synchronization.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
# include <windows.h>
# include <iostream>
// 事件名称
static LPCTSTR g_szContinueEvent ="event.Continue";
// 创建子进程,与父进程执行相同的程序。
BOOL CreateChild()
{
// 获取当前进程的可执行文件名
TCHAR szFilename[MAX_PATH];
::GetModuleFileName(NULL, szFilename, MAX_PATH) ;
// 格式化用于创建新进程的命令行,包括EXE文件名和字符串“child”。
TCHAR szCmdLine[MAX_PATH];
::sprintf(szCmdLine, "\"%s\" child" , szFilename) ;
std::cout << szCmdLine << std::endl;
// 新进程的启动信息结构
STARTUPINFO si;
::ZeroMemory(reinterpret_cast<void*>(&si), sizeof(si)) ;
si.cb = sizeof(si); // 本结构的大小
// 返回的新进程的进程信息结构
PROCESS_INFORMATION pi;
// 使用同一可执行文件和带有“child”的命令行创建新进程。
BOOL bCreateOK = ::CreateProcess(
szFilename, // 新进程的可执行文件名
szCmdLine, // 传给新进程的命令行参数
NULL, // 缺省的进程安全性
NULL, // 缺省的线程安全性
FALSE, // 不继承句柄
CREATE_NEW_CONSOLE, // 特殊的创建标志
NULL, // 新环境
NULL, // 当前目录
&si, // 启动信息结构
&pi ) ; // 返回的进程信息结构
// 释放对子进程的引用
if (bCreateOK)
{
:: CloseHandle(pi.hProcess);
:: CloseHandle(pi.hThread);
}
return(bCreateOK) ;
}
// 创建一个事件和一个子进程,然后等待子进程向事件发出信号
void WaitForChild()
{
// 创建一个事件对象
HANDLE hEventContinue = ::CreateEvent(
NULL, // 缺省的安全性,子进程将具有访问权限
TRUE, // 是否是人工复位事件
//FALSE,
FALSE, // 事件的初始值
g_szContinueEvent); // 事件名称,用于父子进程间共享。
if (hEventContinue != NULL)
{
std :: cout << "event created " << std :: endl;
// 创建子进程
if (::CreateChild())
{
std::cout << " chlid created" << std::endl;
// 等待,直到子进程发出信号
std::cout << "Parent waiting on child." << std::endl;
while(1)
{
::WaitForSingleObject(hEventContinue, INFINITE);
std::cout << "parent received the event signaled from child"
<< std::endl;
//实际应用中可在此进行事件处理。
:: Sleep(1000);
}
}
// 清除句柄
::CloseHandle(hEventContinue);
hEventContinue=INVALID_HANDLE_VALUE;
}
}
// 以下函数在子进程中被调用,其功能是向父进程发出信号
void SignalParent()
{
std::cout << "child process begining......" << std :: endl;
// 尝试打开句柄
HANDLE hEventContinue = ::OpenEvent(
EVENT_MODIFY_STATE, // 所要求的最小访问权限
FALSE, // 不是可继承的句柄
g_szContinueEvent); // 事件名称
if(hEventContinue != NULL)
{
getchar();
//子进程发送事件,通知父进程。
:: SetEvent(hEventContinue);
std :: cout << "event signaled" << std :: endl;
getchar();
//子进程将事件设置为无信号状态。
ResetEvent(hEventContinue);
std :: cout << "event reset" << std :: endl;
getchar();
}
// 清除句柄
:: CloseHandle(hEventContinue) ;
hEventContinue = INVALID_HANDLE_VALUE;
}
int main(int argc, char* argv[])
{
// 检查是父进程还是子进程。
if (argc>1 && :: strcmp(argv[1] , "child" )== 0)
{
// 为子进程,向父进程创建的事件发出信号。
:: SignalParent();
std :: cout << "Child released." << std :: endl;
}
else
{
// 为父进程,创建一个事件和一个子进程,并等待子进程发出信号。
:: WaitForChild();
std :: cout << "Parent released." << std :: endl;
}
return 0;
}
自动复位
然后在子进程中输入1检测:
事件信号
事件重置
然后:
父母等待孩子。
父级收到来自子级的事件信号
人工复位
// Synchronization.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
# include <windows.h>
# include <iostream>
// 事件名称
static LPCTSTR g_szContinueEvent ="event.Continue";
// 创建子进程,与父进程执行相同的程序。
BOOL CreateChild()
{
// 获取当前进程的可执行文件名
TCHAR szFilename[MAX_PATH];
::GetModuleFileName(NULL, szFilename, MAX_PATH) ;
// 格式化用于创建新进程的命令行,包括EXE文件名和字符串“child”。
TCHAR szCmdLine[MAX_PATH];
::sprintf(szCmdLine, "\"%s\" child" , szFilename) ;
std::cout << szCmdLine << std::endl;
// 新进程的启动信息结构
STARTUPINFO si;
::ZeroMemory(reinterpret_cast<void*>(&si), sizeof(si)) ;
si.cb = sizeof(si); // 本结构的大小
// 返回的新进程的进程信息结构
PROCESS_INFORMATION pi;
// 使用同一可执行文件和带有“child”的命令行创建新进程。
BOOL bCreateOK = ::CreateProcess(
szFilename, // 新进程的可执行文件名
szCmdLine, // 传给新进程的命令行参数
NULL, // 缺省的进程安全性
NULL, // 缺省的线程安全性
FALSE, // 不继承句柄
CREATE_NEW_CONSOLE, // 特殊的创建标志
NULL, // 新环境
NULL, // 当前目录
&si, // 启动信息结构
&pi ) ; // 返回的进程信息结构
// 释放对子进程的引用
if (bCreateOK)
{
:: CloseHandle(pi.hProcess);
:: CloseHandle(pi.hThread);
}
return(bCreateOK) ;
}
// 创建一个事件和一个子进程,然后等待子进程向事件发出信号
void WaitForChild()
{
// 创建一个事件对象
HANDLE hEventContinue = ::CreateEvent(
NULL, // 缺省的安全性,子进程将具有访问权限
TRUE, // 是否是人工复位事件
//FALSE,
FALSE, // 事件的初始值
g_szContinueEvent); // 事件名称,用于父子进程间共享。
if (hEventContinue != NULL)
{
std :: cout << "event created " << std :: endl;
// 创建子进程
if (::CreateChild())
{
std::cout << " chlid created" << std::endl;
// 等待,直到子进程发出信号
std::cout << "Parent waiting on child." << std::endl;
while(1)
{
::WaitForSingleObject(hEventContinue, INFINITE);
std::cout << "parent received the event signaled from child"
<< std::endl;
//实际应用中可在此进行事件处理。
:: Sleep(1000);
}
}
// 清除句柄
::CloseHandle(hEventContinue);
hEventContinue=INVALID_HANDLE_VALUE;
}
}
// 以下函数在子进程中被调用,其功能是向父进程发出信号
void SignalParent()
{
std::cout << "child process begining......" << std :: endl;
// 尝试打开句柄
HANDLE hEventContinue = ::OpenEvent(
EVENT_MODIFY_STATE, // 所要求的最小访问权限
FALSE, // 不是可继承的句柄
g_szContinueEvent); // 事件名称
if(hEventContinue != NULL)
{
getchar();
//子进程发送事件,通知父进程。
:: SetEvent(hEventContinue);
std :: cout << "event signaled" << std :: endl;
getchar();
//子进程将事件设置为无信号状态。
if(getchar()!=NULL)
{
ResetEvent(hEventContinue);
}
std :: cout << "event reset" << std :: endl;
getchar();
}
// 清除句柄
:: CloseHandle(hEventContinue) ;
hEventContinue = INVALID_HANDLE_VALUE;
}
int main(int argc, char* argv[])
{
// 检查是父进程还是子进程。
if (argc>1 && :: strcmp(argv[1] , "child" )== 0)
{
// 为子进程,向父进程创建的事件发出信号。
:: SignalParent();
std :: cout << "Child released." << std :: endl;
}
else
{
// 为父进程,创建一个事件和一个子进程,并等待子进程发出信号。
:: WaitForChild();
std :: cout << "Parent released." << std :: endl;
}
return 0;
}
一直执行
回车复位后停止
思考题
1.本实验中事件是如何在父子进程间共享的? // 事件名称
static LPCTSTR g_szContinueEvent ="event.Continue";
通过名称来在父子进程间共享事件,事件在父进程和子进程之间同步,子进程发送,父进程获取
2.Windows的哪个API对应事件的P操作?哪个API对应事件的V操作?
//P获取,V释放
WaitForSingleObject() P
SetEvent() V
3.本实验中是哪个进程向哪个进程发送事件的?
在不同的进程中,一个进程获取事件,一个进程发送事件。
子进程向父进程发送
//子发--->父做
4.获取和发送事件的调用位置有什么特点?
同步配对:
有WaitForSingleObject就会有SetEvent,
两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。
不同进程之间对称出现,先发送事件,后获取。
5.请通过修改程序并对比运行结果来说明Windows的自动复位事件和人工复位事件的区别。
//子进程将事件设置为无信号状态。
ResetEvent(hEventContinue);
人工复位(true)会一直响应,(一直保持这个状态)
人工复位调用WairForSingleObject后不会将事件设置为无信号状态,必须手动调用ResetEvent置事件为无信号状态,所以此代码会一直循环执行语句。
自动复位(false)只会响应一次(事件被消耗,所以只能响应一次)
自动复位调用WaitForSingleObject后会自动将事件设置为无信号状态
6.Windows旳事件是计数的吗?
不是用来计数的,它是一个二进制,来表示两种状态:有信号状态和无信号状态,只能判断事件有没有发生。
2、线程间的互斥
实验描述:
(1) 在主线程中定义线程间共享变量,创建一个互斥体,创建一个加法线程和一个减法线程;
(2) 主线程等待两个线程的结束;
(3) 加法线程和减法线程在互斥体的保护下对共享变量进行操作,操作期间有睡眠,并且循环操作。
Mutex.cpp
// mutex.cpp : Defines the entry point for the console application.
#include "stdafx.h"
# include <windows.h>
# include <iostream>
HANDLE hMutexValue;
//加法线程和减法线程操作的共享变量。
int Value[2];
DWORD WINAPI IncThreadProc(LPVOID lpParam)
{
int Count;
Count=(int)lpParam;
while (1)
{
// 等待访问数值
:: WaitForSingleObject(hMutexValue, INFINITE);
// 改变并显示该值
Value[0]+=Count;
Value[1]+=Count;
std :: cout << "thread:" << :: GetCurrentThreadId()
<<" Value[0]:"<<Value[0]<<" Value[1]:" << Value[1] << std :: endl;
:: Sleep(1000);
// 释放互斥信号量
:: ReleaseMutex(hMutexValue);
}
return(0);
}
DWORD WINAPI DecThreadProc(LPVOID lpParam)
{
int Count;
Count=(int)lpParam;
while (1)
{
// 等待访问数值
:: WaitForSingleObject(hMutexValue, INFINITE);
// 改变并显示该值
Value[0]-=Count;
Value[1]-=Count;
std :: cout << "thread:" << :: GetCurrentThreadId()
<<" Value[0]:"<<Value[0]<<" Value[1]:" << Value[1] << std :: endl;
:: Sleep(1000);
// 释放互斥信号量
:: ReleaseMutex(hMutexValue);
}
return(0);
}
int main(int argc, char* argv[])
{
HANDLE hThreadInc;
HANDLE hThreadDec;
DWORD dwThreadID[2];//用于存储线程的ID
Value[0]=100;
Value[1]=200;
// 创建互斥体用于访问数值
hMutexValue=::CreateMutex(
NULL, // 缺省的安全性
FALSE, // 初始时不拥有。
//一个Mutex在没有任何线程拥有的时候处于有信号状态。
NULL); // 匿名的,用于线程间。
//创建加法线程
hThreadInc=::CreateThread(
NULL, // 缺省的安全性
0, // 缺省堆栈
IncThreadProc, // 线程控制函数。
(LPVOID)10, //线程参数,每次加10
0, // 无特殊的标志
&dwThreadID[0]);// 忽略返回的id
//创建减法线程
hThreadDec=::CreateThread(
NULL, // 缺省的安全性
0, // 缺省堆栈
DecThreadProc, // 线程控制函数
(LPVOID)5, //线程参数,每次减5
0, // 无特殊的标志
&dwThreadID[1]);// 忽略返回的id
// 依次无限等待两个线程结束。
// 也可以调用WaitForMultiObject()同时等待多个对象。
if (hThreadInc!=INVALID_HANDLE_VALUE && hThreadDec!=INVALID_HANDLE_VALUE)
{
::WaitForSingleObject(hThreadInc, INFINITE);
::WaitForSingleObject(hThreadDec, INFINITE);
}
return 0;
}
思考题
1.如果main()中没有语句WaitForSingleObject()会出现什么现象?为什么?
主线程直接结束,子线程也随之结束。
因为主进程顺序执行到return 0直接结束,而子线程和主进程是组合关系也随着结束,WaitForSingleObject()则会让主线程挂起,等待其中线程执行完成。
2.为什么两个线程使用同一个句柄来引用互斥体?
定义了一个全局变量int Value[2]; 作为互斥体,使两个线程同步。
互斥体是一种特殊的信号量,在给定的时间内,只允许一个线程访问某个资源。在使用互斥体之前,必须使用CreatMutex()创建一个互斥体,互斥体是一个全局对象,它可能被其他进程使用。为此,当两个进程都打开了使用相同名称的互斥体时,二者引用了相同的互斥体。使用这种方法可以将两个进程同步。
如果使用不同的互斥体后,一个线程标记句柄已经进入临界区,另一个线程由于使用不同的互斥体则得不到前一个线程进入临界区的消息,所以两个线程不会互斥进入临界区。
3.Windows的哪个API对应互斥体的P操作?哪个API对应互斥体的V操作?
WaitForSingleObject(hMutexValue, INFINITE); P
ReleaseMutex(hMutexValue); V
4.获取和释放互斥体的调用位置有什么特点?
在同一个线程中, 互斥体谁申请,谁使用,谁释放。
5.请指出本实验中的共享资源和临界区。
共享资源value[2]
临界区:2个加法与减法各一个
临界区1:
Value[0]+=Count;
Value[1]+=Count;
std::cout<<"thread:"<<::GetCurrentThreadid()
<<"Value[0]:"<<Value[0]<<" Value[1]<<std::endl;
::Sleep(1000);
临界区2:
// 改变并显示该值
Value[0]-=Count;
Value[1]-=Count;
std :: cout << "thread:" << :: GetCurrentThreadId()
<<" Value[0]:"<<Value[0]<<" Value[1]:" << Value[1] << std :: endl;
:: Sleep(2000);
6.两个线程hThreadInc和hThreadDec,如果一个线程运行到:: Sleep()处,另一线程很可能运行到哪个地方?处于什么状态?
另一线程很可能运行到 WaitForSingleObject()减法线程,处于阻塞状态。