VC++讲义 第11章 线程间的同步

VC++讲义 第11章  线程间的同步
VC++讲义 第11章  线程间的同步

第11章线程间的同步

在 DOS时代,由于DOS并不是一个多任务的环境,所以要想实现多任务显得很勉强。随后有了Windows 3.X,虽然此操作系统有了多任务的支持但是严格的说,对多进程的支持并不够,这主要表现在进程间通信方面提供的支持非常少。一些传统的IPC方式都没有提供。后来在WinNT上完全实现了多进程/多线程支持,当然现在的Windows9X/2K都完全提供了这方面的支持。本章将介绍进程和线程的概念,及线程间的同步、多线程编程。

11.1 进程和线程的概念

使用32位Windows操作系统时,它能够同时运行几个程序,这种能力称为多任务处理,处理支持多任务,Win32操作系统还支持进程中的多线程处理。

在Win32操作系统中,采用的是抢先式多任务,这意味程序对CPU的占用时间是由系统决定的,系统为每个程序分配一定的CPU时间片,当程序的运行时间超过分配的时间片的时间后,系统就会中断该程序并把CPU控制权转交给别的程序。术语多任务其实就可以理解为系统可以同时运行对个进程。

进程(Process)就是一个运行的程序,它有独立的虚拟内存、代码、文件句柄和其他系统资源(如进程创建的文件、管道、同步对象等)组成。当启动一个进程时,操作系统会为此进程建立一个4GB的地址空间,进程是操作系统分配内存地址空间的单位。

线程(Thread),是操作系统分配处理器时间的最基本单元。所以一个进程必须包含一个线程,我们称之为主线程。如果需要,进程可以产生更多的线程,让CPU在同一时间执行不同段落的代码。进程中的线程是并行执行的,每个线程占用CPU的时间由系统来划分,系统不停地在各个线程之间切换。一个进程的所有线程共享它的虚拟地址空间、全局变量和操作系统资源。

简单的说,进程就是程序的一次执行,线程可以理解为进程中的执行的一段程序片段。在一个多任务环境中,下面的概念可以帮助我们理解两者间的差别:

●进程间是独立的,这表现在内存空间,上下文环境;线程运行在进程空间内。

●一般来讲(不使用特殊技术)进程是无法突破进程边界存取其他进程内的存储空间;

而线程由于处于进程空间内,所以同一进程所产生的线程共享同一内存空间。

●同一进程中的两段代码不能够同时执行,除非引入线程。

●线程是属于进程的,当进程退出时,该进程下的所有线程都会被强制退出并清除。

●线程占用的资源要少于进程所占用的资源。

●进程和线程都可以有优先级。

●在线程系统中进程也是一个线程。可以将进程理解为一个程序的主线程。

对于一个进程来说,当应用程序有几个任务要执行时,建立多个线程是很有用的,之所以有线程这个概念,就是因为以线程为调度对象比以进程为调度对象的执行效率会更高,原因有二:

1.由于创建新进程必须加载代码,而线程要执行的代码已经被映射到进程的地址空间,

所以创建、执行线程的速度比进程更快。

2.一个进程的所有线程共享进程的地址空间和全局变量,所以简化了线程之间的通讯。

虽然在进程中进行费时的工作不会导致系统的挂起,但会导致进程本身的挂起。所以,如果进程既要进行长期的工作,又要响应用户的输入,那么它可以启动一个线程来专门负责费时的工作,而进程(即主线程)仍然可以与用户进行交互。

11.2 Win32的线程

实际上,进程只是个外壳,真正运行的是它里面的线程,每个进程都有个主线程,就是以WinMain函数或main函数开始的,WinMain函数和main函数就是主线程的入口函数。每个线程都有个入口函数,当主线程返回时进程便退出。

11.2.1 线程的创建

可以使用CreateThread函数来创建线程,CreateThread的原型如下:

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes,

DWORD dwStackSize,

LPTHREAD_START_ROUTINE lpStartAddress,

LPVOID lpParameter,

DWORD dwCreationFlags, // creation flags

LPDWORD lpThreadId

);

第一个参数lpThreadAttributes,表示创建线程的安全属性,该参数为指向SECURITY_ATTRIBUTES结构的指针,该参数可以忽略,通常为NULL。

第二个参数dwStackSize,指定线程栈的尺寸,该参数可以忽略,通常为0,若设置该参

数为0表示默认的尺寸与进程主线程栈的尺寸相同。

第三个参数lpStartAddress,指定线程开始运行的地址,该参数通常指的就是创建的

线程的入口函数。该参数通过函数DWORD WINAPI ThreadProc(LPVOID lpParameter);来定

义,其中,这个函数中的参数lpParameter得到的数据就是由调用CreateThread函数中的

第四个参数lpParameter传递过来的。

第四个参数lpParameter,表示传递给线程入口函数的32位的参数。

第五个参数dwCreationFlags,用来控制创建的线程的状态,该参数有两个取值:

●CREATE_SUSPEND 表示此线程创建后会呈挂起状态,直到调用ResumeThread

函数来唤醒,继续执行该线程。

●0 表示此线程创建后立即运行。

第六个参数lpThreadId,用来存放返回的线程ID。如果将该参数设为NULL,则表示不

返回线程ID。

11.2.2 线程的终止

终止一个线程的方法不止一种,有以下几种:

●调用了ExitThread函数;

●线程函数返回:主线程返回导致ExitProcess被调用,其他线程返回导致

ExitThread被调用;

●调用ExitProcess导致进程的所有线程终止;

●调用T erminateThread终止一个线程;

●调用T erminateProcess终止一个进程时,导致其所有线程的终止。

11.2.3 实例:通过创建多线程来编写网络聊天程序

这里,我们通过一个例子来熟悉线程的创建,同时也可以复习一下上一章讲过的套接字

的调用。本小节讲的实例,读者大概不会陌生,很类似于OICQ聊天一样的东西。完整例程

请参见光盘中的例子代码EX11_00,具体操作步骤如下:

●步骤1:新建一个MFC对话框应用程序(在MFC Appwizard step1对话框中选中Dialog

based单选按钮),工程名为EX11_00或用户自定义。

●步骤2:编辑对话框资源。

将对话框上的原有控件全部删去,将该对话框ID 改为IDD_DLG_CHAT,名称改为“聊天

程序”。

在对话框上添加两个编辑框,一个负责接收数据,另一个负责发送数据,将它们的ID

分别设为IDC_EDIT_RECV和IDC_EDIT_SEND,负责接收数据的编辑框设为Multiline属性;

再添加一个IP地址控件负责给出发送的地址,使用默认的ID号IDC_IPADDRESS1;

最后再添加一个按钮用来发送数据,ID号设为IDC_BTN_SEND,在按钮属性对话框的Styles页面上选中Default button复选框,使它成为默认的按钮,其用意是,当用户按下回车键时实际就相当于发送数据,而不必每次都单击发送按钮。对话框编辑结果如图11-00所示。

图11-00 编辑好的对话框资源

步骤3:在应用程序类CEX11_00App的InitInstance函数中初始化套接字库。

在上一章编写Win32控制台程序时讲过,要想在Windows环境中使用套接字,必须首先初始化套接字库来加载网络环境,函数InitInstance中代码如下:

BOOL CEX11_00App::InitInstance()

{

AfxEnableControlContainer();

//初始化套接字库

if (!AfxSocketInit())

{

AfxMessageBox("Init socket error");

return FALSE;

}

……

}

其中,函数AfxSocketInit用来初始化套接字库,注意,在MFC的应用程序中要想使用套接字必须调用AfxSocketInit函数来初始化套接字库,并且该函数的调用通常放在应用程序类的InitInstance函数中。相应的,必须在工程EX11_00的StdAfx.h文件中包含头文件#include ,打开工作台的FileView页面,展开Header Files文件夹就可以找到StdAfx.h文件,在该文件的尾部包含afxsock.h头文件。

头文件afxsock.h中包含CAsyncSocket和CSocket类的定义,这两个类是MFC的套接字类,其中CSocket类由CAsyncSocket类继承来的,如果应用程序中使用了这两个类或任何由这两个类继承来的子类就必须添加afxsock.h头文件;另外,如果在应用程序中调用了

AfxSocketInit函数,也要添加afxsock.h头文件。

步骤4:在对话框类CEX11_00Dlg中定义一个成员变量m_sock,并加入一个成员函数InitSocket(),对m_sock进行初始化。

在工作台的ClassView页面通过鼠标右键单击CEX11_00Dlg类,选择Add Member Variable菜单项在类CEX11_00Dlg的头文件中添加SOCKET类型的成员变量m_sock。

在工作台的ClassView页面通过鼠标右键单击CEX11_00Dlg类,选择Add Member Function菜单项在类CEX11_00Dlg中添加返回值为BOOL类型的成员函数InitSocket()。类CEX11_00Dlg的头文件中的定义如下:

class CEX11_00Dlg : public CDialog

{

// Construction

public:

BOOL InitSocket();

SOCKET m_sock;

CEX11_00Dlg(CWnd* pParent = NULL);

……

};

在添加的成员函数InitSocket中对变量m_sock进行初始化,即建立一个套接字,将该套接字与本地地址相连。在函数InitSocket中编辑如下代码:

BOOL CEX11_00Dlg::InitSocket()

{

//创建套接字

m_sock=socket(AF_INET,SOCK_DGRAM,0);

//判断套接字是否创建成功

if (m_sock==INVALID_SOCKET)

{

MessageBox("create socket error");

return FALSE;

}

//获取本机地址

SOCKADDR_IN addrSock;

addrSock.sin_addr.S_un.S_addr=htonl(INADDR_ANY);

addrSock.sin_family=AF_INET;

addrSock.sin_port=htons(4500);

//将本地地址和套接字相连并判断绑定是否成功

if (SOCKET_ERROR==bind(m_sock,(SOCKADDR*)&addrSock,

sizeof(addrSock)))

{

MessageBox("bind socket error");

return FALSE;

}

return TRUE;

}

该段代码首先调用socket函数创建一个面向非连接服务的套接字,该函数如创建成功

返回套接字的句柄,若失败,则返回INVALID_SOCKET,因此它下面的if语句用来判断创建套接字是否成功。然后通过INADDR_ANY由计算机自动获取本地地址,与创建的套接字进行绑定,当然也通过if语句来判断绑定是否成功。

步骤5:编写主线程函数,负责发送数据。

我们把发送按钮的消息响应函数作为主线程函数,打开对话框,双击上面的发送按钮,会弹出如图11-01所示的对话框,单击ok,将该按钮的单击消息响应函数OnBtnSend添加到类CEX11_00Dlg中。

图11-01 添加命令按钮消息响应函数

在OnBtnSend函数中获取用户想要发送数据的对方的地址,然后将用户输入的文本发送出去,函数代码如下:

void CEX11_00Dlg::OnBtnSend()

{

//定义字符型变量strSend用来保存用户将要发送的数据

CString strSend;

GetDlgItemText(IDC_EDIT_SEND,strSend);

SOCKADDR_IN addrSend;

//定义DWORD型的变量dwIP用来保存用户在IP地址控件中输入的对方的IP

DWORD dwIP;

((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);

//取得对方IP地址之后发送数据

addrSend.sin_addr.S_un.S_addr=htonl(dwIP);

addrSend.sin_family=AF_INET;

addrSend.sin_port=htons(4500);

sendto(m_sock,strSend,strlen(strSend)+1,0,

(SOCKADDR*)&addrSend,sizeof(addrSend));

//数据发送之后,将发送数据的编辑框清空

SetDlgItemText(IDC_EDIT_SEND,"");

}

代码中,调用函数GetDlgItemText用来获取ID号为IDC_EDIT_SEND的编辑框控件中的文本内容(即用户输入的要发送的字符串),并将该文本内容存放到变量strSend中,该函数原形如下:

int GetDlgItemText( int nID, CString& rString ) const;

该函数是CWnd类的成员函数,用来获取对话框上指定控件的标题或文本内容。第一个参数nID表示将要获取标题或文本所在的控件的ID值。第二个参数rString,为CString 类型的变量,用来存放取得的控件的标题或文本。该函数将得到的标题或文本拷贝到参数rString指定的字符串中,并返回字符串的长度。

函数GetDlgItem,读者大概不会陌生,在第七章我们讲过,根据函数中参数给出的ID 值来获取对应的控件窗口。代码中,我们用该函数取得IP地址控件窗口,为的是调用它的成员函数GetAddress来获得用户输入的对方的IP地址。GetAddress函数原形如下:int GetAddress( DWORD& dwAddress );

该函数是CIPAddressCtrl类的成员函数,用来获取指定的IP地址控件中显示的IP地址,参数dwAddress为DWORD类型的引用,调用GetAddress函数之后获取的IP地址就存放在这个参数中,因此,之前我们定义了一个DWORD类型的变量dwIP就是用来存放用户输入的IP地址。

最后,调用sendto函数将数据发送之后,把发送数据编辑框控件中的字符清空,以便用户继续输入其他字符。

●步骤6:在对话框类的头文件中定义一个结构体RecvParam,用于向创建的线程的入口

函数传递参数。

之前,在讲创建线程的函数CreateThread时,我们讲过,此函数的第四个参数是表示传递给线程入口函数的32位的参数,但是只能传递一个参数,然而,我们这里需要两个参数:一个是套接字,一个是对话框窗口句柄,因此,只好定义一个结构体,将这两个参数作为结构体的两个数据成员一起传给线程。

在类CEX11_00Dlg的头文件中定义结构体如下:

struct RecvParam

{

HWND hwnd;

SOCKET sock;

};

定义该结构体的位置在CEX11_00Dlg类声明的前面,即class CEX11_00Dlg: public CDialog代码的前边,不要放在类声明的里边。因为该结构体不属于对话框类的成员。

●步骤7:在对话框的OnInitDialog()函数中调用InitSocket函数并创建一个线程。

在对话框CEX11_00Dlg类的OnInitDialog函数中调用InitSocket函数是为了当对话框一启动时就创建套接字并将套接字与本机地址绑定。OnInitDialog函数代码如清单11-00所示:

清单11-00 CEX11_00Dlg::OnInitDialog()函数代码———————————————————————————————————————

1 BOOL CEX11_00Dlg::OnInitDialog()

2 {

3 CDialog::OnInitDialog();

4 ……

5 InitSock();

6((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->SetAddress(192,168,123,110);

7RecvParam *pRecvParam=new RecvParam;

8pRecvParam->hwnd=m_hWnd;

9pRecvParam->sock=m_sock;

10CreateThread(NULL,0,RecvProc,(LPVOID)pRecvParam,0,NULL);

11 ……

12 } ———————————————————————————————————————第6行代码调用SetAddress函数来置IP地址控件的初始地址,本例中设置的是笔者的

主机地址。

第7行代码定义了一个指向结构RecvParam的指针变量pRecvParam,8、9行代码给结构中的两个数据成员分别赋值,第10行代码将变量pRecvParam作为参数传递给创建的线程。

●步骤8:在对话框类的实现文件(.cpp)中创建线程的入口函数过程,在该函数中负责

接收数据,代码如下:

//创建线程的入口函数

DWORD WINAPI RecvProc(

LPVOID lpParameter // thread data

)

{

//lpParameter变量保存的是传来的结构体参数

HWND hwnd=((RecvParam*)lpParameter)->hwnd;

SOCKET sock=((RecvParam*)lpParameter)->sock;

//定义addrRecv用于保存发送数据方(即对方)的主机信息

SOCKADDR_IN addrRecv;

int len=sizeof(addrRecv);

//变量buff用于保存接收的数据,经格式化后存放到szchar中

char buff[1024];

char szchar[2048];

while(1)

{

if (SOCKET_ERROR==recvfrom(sock,buff,1024,0,(SOCKADDR*)&addrRecv,&len))

break;

else

{

sprintf(szchar,"%s:%s",inet_ntoa(addrRecv.sin_addr),buff);

PostMessage(hwnd,WM_RECVDATA,0,(LPARAM)szchar);

}

}

return 1;

}

其中,参数lpParameter里边保存的就是由创建线程的函数传递过来的结构体参数,这里,通过定义hwnd和sock两个变量将传递过来的值取出以备使用。

接收数据以后,就应该处理该数据将它显示出来,本例中采用的方法是通过调用PostMessage函数向对话框窗口发送一条消息,并将收到的数据作为此消息的参数,然后在该消息的消息处理函数中来显示收到的数据。

●步骤9:添加WM_RECVDATA消息的消息处理函数并编写代码。

手动添加消息处理函数的过程在前面章节中有讲过,读者应该不会陌生。首先,在类CEX11_00Dlg的头文件(.h)中,在定义结构体RecvParam的前面给消息WM_RECVDATA定义一个ID号,如下:

#define WM_RECVDATA WM_USER+1

然后,在类的头文件(.h)中,在类声明的部分,在宏DECLARE_MESSAGE_MAP之前添加消息响应函数。如下:

class CEX11_00Dlg : public CDialog

{

// Construction

……

afx_msg void OnRecvdata(WPARAM wParam,LPARAM lParam);

DECLARE_MESSAGE_MAP()

};

然后在类的实现文件中,在宏BEGIN_MESSAGE_MAP()和END_MESSAGE_MAP()之间添加消息映射。如下:

ON_MESSAGE(WM_RECVDATA,OnRecvdata)

最后,在类的实现文件中的末尾部分定义消息处理函数OnRecvdata的函数体,并在函数中编写如清单11-01所示代码:

清单11-01 CEX11_00Dlg::OnRecvdata函数代码———————————————————————————————————————

1void CEX11_00Dlg::OnRecvdata(WPARAM wParam,LPARAM lParam)

2{

3CString strRecv;

4strRecv=(char *)lParam;

5CString strTemp;

6GetDlgItemText(IDC_EDIT_RECV,strTemp);

7strRecv+="\r\n";

8strRecv+=strTemp;

9SetDlgItemText(IDC_EDIT_RECV,strRecv);

10} ———————————————————————————————————————3~4行代码,定义字符型变量strRecv,然后用来存放由参数lParam传来的收到的数据,第6行调用的GetDlgItemText函数是取出接收编辑框中原有的内容,放到strTemp变量中,然后将接收的数据strRecv在尾部添加回车换行符之后,再连上strTemp中的文本,最后第9行将内容显示出来。

图11-02 聊天程序运行结果

这样做的方式,读者大概理解了,并不是直接将用户接收的数据显示在编辑框中之前接收的数据的前面,那样实现起来比较难,我们是把接收的数据和编辑框中原有的数据连成一个字符串显示在编辑框中,这样实现起来很简单,而且给用户的错觉是,编辑框中原有的内容向下移动了,刚刚收到的数据显示在最上面,运行结果如图11-02所示。

11.3 MFC的线程处理

在Win32 API的基础之上,MFC提供了处理线程的类和函数。处理线程的类是CWinThread,函数是AfxBeginThread、AfxEndThread等。CWinThread是MFC线程类,它的成员变量m_hThread和m_hThreadID分别记录当前线程的句柄和线程ID。在MFC应用程序中,所有的线程都是CWinThread对象,用AfxBeginThread函数可以创建一个CWinThread 对象。我们以前使用过的CWinApp类就是从CWinThread类派生的。

Win32 API中并不区分线程类型,它只需要知道线程的开始地址(即线程的入口函数)以便它开始执行线程,而在MFC中支持两种类型的线程:工作者线程和用户界面线程。

工作者线程常用于完成不要求用户输入的任务,如耗时计算、后台打印之类的任务,因此,它不需要有界面。工作者线程也适用于等待一个事件的发生。例如,从一个应用程序种接收数据,而不必要求用户等待。

用户界面线程一般用于处理用户输入并对用户产生的事件和消息作出应答,用户界面线程必须产生新的用户界面,这是与工作者线程不同。同时用户界面线程内还必须有消息循环,因此,用户界面线程比工作者线程要复杂。

11.3.1 创建工作者线程

工作者线程实际上就是并行执行的一个函数。在进行某项非常耗时的工作时,如果直接调用函数,往往容易导致主线程被阻塞,应用程序不能立即响应用户输入,交互性变差,此时,就应该考虑创建一个工作者线程来处理此类工作了。

一个MFC线程,不管是工作者线程还是用户界面线程,都是调用AfxBeginThread函数来创建并初始化,只是AfxBeginThread被重载成两个版本,一个用于工作者线程,一个用于用户界面线程。

创建工作者线程比较简单,不必从CWinThread派生新的线程类,只需要提供一个控制函数,由线程启动后执行该函数,然后,使用AfxBeginThread创建MFC线程对象。

用于创建工作者线程的函数如下:

CWinThread* AFXAPI AfxBeginThread(

AFX_THREADPROC pfnThreadProc,

LPVOID pParam,

int nPriority,

UINT nStackSize,

DWORD dwCreateFlags,

LPSECURITY_ATTRIBUTES lpSecurityAttrs

);

第一个参数pfunThreadProc,表示线程的入口函数地址,函数的原形应该如同:UINT MyControllingFunction( LPVOID pParam );

第二个参数pParam,表示传递给线程的参数。

第三个参数nPriority,表明线程的优先级,默认的优先级别THREAD_PRIORITY_NORMAL,如果为0,则与创建该线程的线程相同。该参数有以下几种级别,下面是从高到低排序的:THREAD_PRIORITY_TIME_CRITICAL

THREAD_PRIORITY_HIGHEST

THREAD_PRIORITY_ABOVE_NORMAL

THREAD_PRIORITY_NORMAL

THREAD_PRIORITY_BELOW_NORMAL

THREAD_PRIORITY_LOWEST

HREAD_PRIORITY_IDLE

第四个参数nStackSize,表示线程的栈大小,如果为0表示使用系统默认值。

第五个参数dwCreateFlags,表示创建线程时的标记,若该参数为CREATE_SUSPENDED 表示线程创建后呈挂起状态;如果为0,表示该线程一建立就立即运行。

第六个参数lpSecurityAttrs,表示安全属性,该参数一般为NULL。

该函数调用成功的返回值是CWinThread类的指针,可以通过它实现对线程的控制。在线程函数返回时线程将被结束,在线程内部可以利用void AfxEndThread( UINT nExitCode );结束线程,nExitCode为退出码。

工作者线程一旦启动,就开始执行控制函数,线程结束,控制函数也就结束了。线程控制函数的原形如下:

UINT MyControllingFunction(LPVOID pParam);

其中的函数名并不是固定的那个函数名,而是用户自定义的函数名,可以为任何合法的命名,如下面我们自定义名为MyThread。以下是一个控制函数的例子:

UINT MyThread( LPVOID pParam )

{

//接收一个窗口类指针,然后设置窗口标题

CWnd *pIndex=(CWnd*)pParam;

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

{

char TMsz[100];

sprintf(TMsz,"工作者线程 : %d",i);

pIndex ->SetWindowText(TMsz);

Sleep(10);

}

return 0; // 返回并退出线程

//或者调用void AfxEndThread( UINT nExitCode );来退出

}

然后在其他地方调用AfxBeginThread(MyThread,& m_Index,THREAD_PRIORITY_NORMAL,0,0,NULL);其中,m_Index传递窗口类指针。

11.3.2 创建用户界面线程

用户界面线程通常用于处理用户的输入,响应用户产生的时间和消息。一旦使用AppWizard创建一个MFC应用程序,就已经创建了一个用户界面线程——主线程(由CWinApp 派生的类提供)。

由于用户界面线程需要有自己的窗口界面和消息循环,因此,它的创建要比一个工作者线程的创建复杂的多,不是一个函数就可以解决的。用户界面线程的创建过程一般遵循如下步骤:

1.从CWinThread中派生新类

2.重载CWinThread的InitInstance函数

3.使用AfxBeginThread函数创建并启动线程对象

下面我们将通过一个实例来讲述如何创建一个用户界面线程。完整利程请参见光盘中例

子代码EX11_01,以下是具体操作步骤:

●步骤1:新建一个MFC单文档应用程序,工程名为EX11_01或用户自定义。

●步骤2:添加新的菜单项“用户界面线程”用于创建和启动线程。

打开工作台的ResourceView页面,修改MFC应用程序向导给我们自动生成的菜单资源,在原菜单基础上再添加一个弹出式菜单“线程”,在它下面添加菜单项“用户界面线程”,设置其ID为ID_THREAD_GUI。

●步骤3:从CWinThread中派生新类

派生新类的方法很简单,在以前章节中也有介绍过,可以使用 ClassWizard工具选择Add Class|New,也可以通过打开工作台ClassView页面,鼠标右键单击最顶层类集,在弹出的快捷方式菜单中选择New Class菜单项来启动New Class对话框,在基类列表框中选择CWindThread类,然后在Name编辑框中输入派生类名,如MyThread。

打开我们新产生的MyThread类的头文件(.h),有如下类的定义:

////////////////////////////////////////////////////////////////////////// // MyThread thread

class MyThread : public CWinThread

{

DECLARE_DYNCREATE(MyThread)

protected:

MyThread(); // protected constructor used by dynamic creation

……

DECLARE_MESSAGE_MAP()

};

代码中使用了DECLARE_DYNCREATE宏,使用该宏表明MyThread类具有动态创建的能力;使用DECLARE_MESSAGE_MAP宏表明具有消息映射,可以处理命令消息。再打开MyThread类的实现文件(.cpp),下面列出其中一部分代码;

/////////////////////////////////////////////////////////////////////////// // MyThread

IMPLEMENT_DYNCREATE(MyThread, CWinThread)

MyThread::MyThread()

{

}

……

BEGIN_MESSAGE_MAP(MyThread, CWinThread)

//{{AFX_MSG_MAP(MyThread)

// NOTE - the ClassWizard will add and remove mapping macros here.

//}}AFX_MSG_MAP

END_MESSAGE_MAP()

代码中使用了IMPLEMENT_DYNCREATE宏是和头文件中DECLARE_DYNCREATE宏相对应的,保证新建的类具有动态创建的能力;而BEGIN_MESSAGE_MAP和END_MESSAGE_MAP宏是和头文件中DECLARE_MESSAGE_MAP宏相对应的,使类具有消息映射机制。

●步骤4:重载CWinThread的虚函数

父类CWinThread有一些可以重载的虚函数,其中InitInstance函数必须要重载,该函数用于初始化实例;ExitInstance函数一般情况下也应该重载,该函数用于清除实例;而

Run函数,如果没有特殊需要,一般不去重载,该函数负责将消息分发出去。

重载InitInstance函数举例如清单11-02所示:

清单11-02 MyThread::InitInstance函数代码———————————————————————————————————————

1 BOOL MyThread::InitInstance()

2 {

3 // TODO: perform and per-thread initialization here

4CFrameWnd* pFrameWnd= new CFrameWnd();

5pFrameWnd->CreateEx(0,AfxRegisterWndClass( CS_HREDRAW|CS_VREDRAW) , 6"用户界面线程示例",

7WS_OVERLAPPEDWINDOW|WS_VISIBLE,

8CRect(100,100,400,300),

9NULL,

100);

11m_pMainWnd=pFrameWnd;

12pFrameWnd->ShowWindow(SW_SHOW);

13pFrameWnd->UpdateWindow();

14 return TRUE;

15 } ———————————————————————————————————————代码中,第4行首先构造了一个框架窗口对象,然后在第5~10行调用其成员函数CreateEx来产生窗口,最后将窗口显示出来。如果用户界面线程正常执行时,就会产生一个标题为"用户界面线程示例"的窗口

步骤5:使用AfxBeginThread函数创建并启动线程对象

在CEX11_01View类中,添加新建菜单项的COMMAND命令消息处理函数OnThreadGui,在该消息处理函数中通过调用AfxBeginThread函数来启动用户界面线程,代码如下:void CEX11_01View::OnThreadGui()

{

// TODO: Add your command handler code here

AfxBeginThread(RUNTIME_CLASS(MyThread));

}

代码中又用到了AfxBeginThread函数,但是创建工作者线程和创建用户界面线程的AfxBeginThread函数的形式是有一定区别的,在这段代码里,用的是创建用户界面线程的AfxBeginThread函数,原形如下:

CWinThread* AFXAPI AfxBeginThread(

CRuntimeClass* pThreadClass,

int nPriority,

UINT nStackSize,

DWORD dwCreateFlags,

LPSECURITY_ATTRIBUTES lpSecurityAttrs

);

第一个参数pThreadClass,表示从CWinThread派生的RUNTIME_CLASS类。

第二个参数nPriority,表示线程的优先级,默认的优先级别THREAD_PRIORITY_NORMAL,如果为0,则与创建该线程的线程相同。参数有以下几种级别,下面是从高到低排序的:

THREAD_PRIORITY_TIME_CRITICAL

THREAD_PRIORITY_HIGHEST

THREAD_PRIORITY_ABOVE_NORMAL

THREAD_PRIORITY_NORMAL

THREAD_PRIORITY_BELOW_NORMAL

THREAD_PRIORITY_LOWEST

HREAD_PRIORITY_IDLE

第三个参数nStackSize,表示线程的栈大小,如果为0表示使用系统默认值。

第四个参数dwCreateFlags,表示创建线程时的标记,若该参数为CREATE_SUSPENDED 表示线程创建后呈挂起状态;如果为0,表示该线程一建立就立即运行。

第五个参数lpSecurityAttrs,表示安全属性,该参数一般为NULL。

因为我们在视图类CEX11_01View中用到了新建的线程类MyThread,因此,我们在最后不要忘了在CEX11_01View类中包含MyThread类的头文件,如下:

#include "MyThread.h"

运行程序结果如图11-03所示。

图11-03 用户界面线程示例运行结果

新建的用户界面线程窗口显示在屏幕上,但需要读者了解的是,这个线程窗口不属于CEX11_01程序的主框架窗口,而是和主框架窗口(也可以说是主线程窗口)并列的,二者的父窗口都是系统的桌面。查看桌面底部的Windows系统任务栏就会发现除了CEX11_01应用程序窗口,也有单独的用户界面线程窗口。

此时,这两个窗口可以并行工作,互不影响,用户可以单独关闭用户界面线程窗口,就相当于正常退出了用户界面线程,不会对主线程造成影响。反过来,如果用户关闭主线程窗口,则用户界面线程窗口也会被迫关闭,这种情况属于用户界面线程非正常退出,会造成内存泄漏。

11.4 线程同步

在有若干个线程并行运行的环境里,不同线程之间的同步是至关重要的。“同步”这个词很容易让人误解,因为我们平时说两件事情同步,其意义是将两件事情同时来做;而这里的线程“同步”却恰恰相反,它的目的是避免多个线程同时进行某些操作,使多个线程之间

协调工作。

同步可以保证在一个时间内只有一个线程对某个资源(如操作系统资源等共享资源)有控制权。共享资源包括全局变量、公共数据成员或者句柄等。同步还可以使得有关联交互作用的代码按一定的顺序执行。本节将介绍为什么需要线程同步,以及如何使用同步对象来实现线程同步。

11.4.1 为什么要同步

我们知道,同一进程中的所有线程共享进程的虚拟地址空间,因此,很可能会发生多个线程同时访问同一个对象(包括全局变量、共享资源、API函数和MFC对象等),这种情况下容易导致程序的错误。例如,如果一个线程正在对一个大尺寸全局变量进行读操作,在未读完时,另一个线程又对该全局变量进行写操作,那么,第一个线程读取的变量值有可能是一种修改过程中的不稳定值。

举个例子,有下面这样一段代码:

int iIndex=0; //变量iIndex为全局变量

DOWRD threadA(void* pD)

{

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

{

int iCopy=iIndex;

//Sleep(1000);

iCopy++;

//Sleep(1000);

iIndex=iCopy;

}

}

现在假设有两个线程threadA1和threadA2在同时运行,那么运行结束后iIndex的值会是多少,是200吗?不是的,如果我们将Sleep(1000)前的注释去掉后我们会很容易明白这个问题,因为在iIndex的值被正确修改前它可能已经被其他的线程修改了。这个例子是一个将机器代码操作放大的例子,因为在CPU内部也会经历数据读/写的过程,而在线程执行的过程中线程可能被中断而让其他线程执行。变量iIndex在被第一个线程修改后,写回内存前如果它又被第二个线程读取,然后才被第一个线程写回,那么第二个线程读取的其实是错误的数据,这种情况就称为脏读(dirty read)。这个例子同样可以推广到对文件、资源的使用上。

那么要如何才能避免这一问题呢,假设我们在使用iCounter前向其他线程询问一下:有谁在用吗?如果没被使用则可以立即对该变量进行操作,否则等其他线程使用完后再使用,而且在自己得到该变量的控制权后,也要告知其他线程此时不能使用这一变量,直到自己使用完并释放为止。

属于不同进程的线程在同时访问同一内存区域或共享资源时,也会存在同样的问题。因此,在多线程应用程序中,常常需要采取一些措施来同步线程的执行。需要同步的情况包括以下几个方面:

1.当两个或多个线程需要访问每次只能被一个线程访问的共享资源时。例如,当一个线程在写文件时,要求阻止另一个线程读该文件

2.当多个线程的执行有先后顺序,它们之间需要协调运行时。例如,如果B线程需要等待A线程完成到某一程度时才能运行,那么B线程就应该暂时挂起以减少对 CPU的占用时间,让A线程处于运行状态,一旦当A线程运行到那一时刻时,才发信号给B线程,让B

线程转到运行状态。

3.在Windows 95环境下编写多线程应用程序还需要考虑重入问题。我们知道,Windows NT是真正的32位操作系统,它解决了系统重入问题。而Windows 95由于继承了Windows 3.x 的部分16位代码,没能够解决重入问题。这意味着在Windows 95中两个线程不能同时执行某个系统功能,否则有可能造成程序错误,甚至会造成系统崩溃。应用程序应该尽量避免发生两个以上的线程同时调用同一个Windows API函数的情况

那么,如何实现同步呢?Windows给我们提供了多种同步对象供我们使用,并且可以替我们管理同步对象的加锁和解锁。我们需要做的就是对每个需要同步使用的资源产生一个同步对象,在使用该资源前申请加锁,在使用完成后解锁。

下面就会介绍几种同步对象的用法及一组重要的等待函数。

11.4.2 等待函数

Win32 API提供了一组能使线程阻塞其自身执行的等待函数。这些函数只有在作为其参数的一个或多个同步对象(见下小节)产生信号时才会返回。在超过规定的等待时间后,不管有无信号,函数也都会返回。在等待函数未返回时,线程处于等待状态,此时线程只消耗很少的CPU时间。等待函数分三类:

1.等待单个对象

这类函数包括:SignalObjectAndWait、WaitForSingleObject、WaitForSingleObjectEx,其中最常用的是WaitForSingleObject,该函数原形如下:

DWORD WaitForSingleObject(

HANDLE hHandle,

DWORD dwMilliseconds

);

第一个参数hHandle,表示等待的同步对象的句柄。

第二个参数dwMilliseconds,表示等待的时间,以ms为单位。如果该参数为0,那么函数就测试同步对象的状态并立即返回;如果为INFINITE表示无限期的等待。

该函数调用之后的返回值有如下几种:

●WAIT_ABANDONED 在等待的对象为互斥对象时,表明因互斥对象被中断而变为有信

号状态,但互斥对象未释放。

●WAIT_OBJECT_0 指定的同步对象得到使用权,处于有信号的状态。

●WAIT_TIMEOUT 超过(dwMilliseconds)规定时间返回,并且同步对象无信号。

●WAIT_FAILED 函数调用失败

在线程调用WaitForSingleObject后,如果一直无法得到控制权线程将被挂起,直到超过时间或是获得控制权。

在以下情况下等待函数将返回:

同步对象获得信号时返回;等待时间达到了返回:如果等待时间不限制(Infinite),则只有同步对象获得信号才返回;如果等待时间为0,则在测试了同步对象的状态之后马上返回。

讲到这里我们必须更深入的讲一下WaitForSingleObject函数中的对象(Object)的含义,这里的对象是一个具有信号状态的对象,对象有两种状态:有信号/无信号。而等待的含义就在于等待对象变为有信号的状态,对于互斥对象来讲如果正在被使用则为无信号状态,被释放后变为有信号状态。当等待成功后WaitForSingleObject函数会将互斥对象置为无信号状态,这样其他的线程就不能获得使用权而需要继续等待。WaitForSingleObject函数还有进行排队功能,保证先提出等待请求的线程先获得对象的使用权。

2.等待多个对象

这类函数包括:WaitForMultipleObjects、WaitForMultipleObjectsEx、MsgWaitForMultipleObjects、MsgWaitForMultipleObjectsEx四种,其中最常用的函数是WaitForMultipleObjects,原形如下:

DWORD WaitForMultipleObjects(

DWORD nCount,

CONST HANDLE *lpHandles,

BOOL fWaitAll,

DWORD dwMilliseconds // 超时设置,以ms为单位,如果为INFINITE表示无限期的等待

);

第一个参数nCount,表示等待的对象数量。

第二个参数lpHandles,代表一个对象句柄数组指针。

第三个参数bWaitAll,说明了等待类型,若该参数为TRUE,那么函数在所有对象都有信号后才返回;如果为FALSE,则只要有一个对象变成有信号状态,函数就返回。

第四个参数dwMilliseconds,表示等待的时间,以ms为单位。如果该参数为0,那么函数就测试同步对象的状态并立即返回;如果为INFINITE表示无限期的等待。

该函数调用之后的返回值有如下几种:

●WAIT_OBJECT_0 到(WAIT_OBJECT_0 + nCount – 1):当fWaitAll为TRUE时表示

所有对象变为有信号状态;当fWaitAll为FALSE时,返回值减去WAIT_OBJECT_0

得到的就是变为有信号状态的对象在数组中的下标。

●WAIT_ABANDONED_0 到 (WAIT_ABANDONED_0 + nCount – 1):当fWaitAll为TRUE

时表示所有对象变为有信号状态;当fWaitAll为FALSE时,表示对象中有一个对

象为互斥对象,该互斥对象因为被中断而成为有信号状态,使用返回值减去

WAIT_OBJECT_0得到的就是变为有信号状态的对象在数组中的下标。

●WAIT_TIMEOUT:表示超过规定时间返回。

在以下情况下等待函数返回:

一个或全部同步对象获得信号时返回(在参数中指定是等待一个或多个同步对象);等待时间达到了返回:如果等待时间不限制(Infinite),则只有同步对象获得信号才返回;如果等待时间为0,则在测试了同步对象的状态之后马上返回。

3.可以发出提示的函数

这类函数包括:MsgWaitForMultipleObjectsEx、SignalObjectAndWait、WaitForMultipleObjectsEx、WaitForSingleObjectEx,这些函数主要用于重叠(Overlapped)的I/O(异步I/O)。

11.4.3 同步对象

同步对象用来协调多线程的执行,它可以被多个线程共享。前面讲过的线程的等待函数就是用同步对象的句柄作为参数,同步对象应该是所有要使用的线程都能访问到的。同步对象的状态要么是有信号的,要么是无信号的。同步对象主要有四种:关键代码段(Critical_section),互斥对象(Mutex),事件对象(Event),信标对象(Semaphores)。

在Win32中,同步对象是用来协调多线程执行的一种机制,而MFC则将它们封装成几个同步类。表11-00列出了用于线程同步的同步对象,Win32为其提供的主要函数,以及在MFC 中封装的类。

11.4.3.1 关键代码段

关键代码段也叫临界区,关键代码段是一种最简单的同步对象,它只可以被单个进程内的线程使用。它的作用是保证只有一个线程可以申请到该对象。因此,可以让所有的线程都共享同一个关键代码段对象,哪个线程拥有这个关键代码段对象,它就可以访问受保护的共享资源,其他线程只有等到第一个线程释放了关键代码段对象,才有机会获得该对象,以便访问同一数据。

关键代码段类似于商场中的一个试衣间,同一时间只能有一个人进去,而另一个人若想进去,只有等到上一个人出来,将试衣间空出来才可以。关键代码段是一种保证在某一时刻只有一个线程能够访问数据的简便方法。与其他三种同步对象相比,关键代码段为互相排斥同步提供更快、更有效的机制。

下面将介绍关键代码段对象的几个相关的函数:

1.VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

该函数用于产生关键代码段。参数lpCriticalSection为指针变量,指向一个已声明的关键代码段的一个实例。

2.VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection );

该函数用于进入关键代码段,相当于申请加锁,如果该关键代码段正被其他线程使用则该函数会等待到其他线程释放。参数lpCriticalSection意义同上。

3.BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection );

该函数用于进入关键代码段,相当于申请加锁,和EnterCriticalSection不同的是,如果该关键代码段正被其他线程使用,则该函数会立即返回FALSE,而不会等待。参数lpCriticalSection意义同上。

4.VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection );

该函数用于退出关键代码段,相当于申请解锁。参数lpCriticalSection意义同上。该函数和EnterCriticalSection函数必须成对出现,如果线程在没有获得关键代码段的使用权的情况下调用LeaveCriticalSection函数,会导致其他线程调用EnterCriticalSection 时无限等待。

5.VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection );

该函数用于删除关键代码段。参数lpCriticalSection意义同上。

关键代码段的使用一般遵循如下过程:首先,用关键代码段数据类型CRITICAL_SECTION 定义一个关键代码段对象;然后,调用InitializeCriticalSection函数创建及初始化该对象,初始化时把对象设置为NOT_SINGALED,表示允许线程使用资源;如果一段程序代码需要对某个资源进行同步保护,则这是一段关键段代码。在进入该关键段代码前调用EnterCriticalSection函数,这样,其他线程都不能执行该段代码,若它们试图执行就会被阻塞;完成关键段代码的执行之后,调用LeaveCriticalSection函数,其他的线程就可以继续执行该段代码。如果该函数不被调用,则其他线程将无限期的等待。

下面以一个实例来说明关键代码段的使用方法,该实例存放于光盘的例子代码EX11_02中,是一个Win32控制台程序,鉴于读者对创建Win32控制台程序及给工程添加C++源文件的过程已经熟悉,因此,只给出源文件(.cpp)中的代码,该代码列于清单11-03中。

清单11-03 工程EX11_02源文件(关键代码段示例)———————————————————————————————————————

1 #include

2 #include

3 //线程1的入口函数声明

4 DWORD WINAPI FunProc1(

5 LPVOID lpParameter

6 );

7 //线程2的入口函数声明

8 DWORD WINAPI FunProc2(

9 LPVOID lpParameter

10 );

11 //定义一个关键代码段对象

12 CRITICAL_SECTION cs;

13 //全局变量i

14 int i;

15 //主线程

16 void main()

17 {

18 //创建两个线程

19 CreateThread(NULL,0,FunProc1,NULL,0,NULL);

20 CreateThread(NULL,0,FunProc2,NULL,0,NULL);

21 //创建及初始化关键代码段对象

22 InitializeCriticalSection(&cs);

23 i=3;

24 cout<<"main thread is running"<

25 Sleep(200);

26 //删除关键代码段对象

27 DeleteCriticalSection(&cs);

28 }

29 //线程1的入口函数

30 DWORD WINAPI FunProc1(

31 LPVOID lpParameter // thread data

32 )

33 {

34 //线程1进入关键代码段

35 EnterCriticalSection(&cs);

36 i++;

37 int c=i+2;

38 cout<

39 cout<<"thread1 is running"<

40 //线程1离开关键代码段

41 LeaveCriticalSection(&cs);

42 return 1;

43 }

44 //线程2的入口函数

45 DWORD WINAPI FunProc2(

46 LPVOID lpParameter // thread data

47 )

48 {

49 //线程2进入关键代码段

50 EnterCriticalSection(&cs);

51 i++;

52 int c=i+2;

53 cout<

54 cout<<"thread2 is running"<

55 return 1;

56 }

该段代码在19、20行创建了两个线程,首先线程1进入关键代码段,运行33~43行之间的代码,将全局变量i的值加1(i结果为4),再将局部变量c的值赋为i+2(c结果为6)进行输出,然后离开关键代码段;之后,线程2进入关键代码段,运行48~56行之间的代码,同样对变量i和c进行赋值和输出。由于线程1先离开了关键代码段,所以线程2得以进入,二者在不同的时间访问了全局变量i,运行结果如图11-04所示。

图11-04 关键代码段实例运行结果

现在我们把清单11-03中,线程1的入口函数中的第41行代码,LeaveCriticalSection(&cs);代码注释掉,也就是说让线程1不离开关键代码段,那么线程2就会被阻塞,永远等待下去,只要线程1不离开,线程2就进不去,也无法访问全局变量

i,运行程序结果如图11-05所示。

图11-05 线程1不离开关键代码段运行结果

11.4.3.2 互斥对象

互斥对象也称互斥量,分为匿名互斥和命名互斥。它不仅可以在同一个进程的线程之间实现资源的安全共享,而且可以在不同进程之间实现安全的资源共享。互斥对象与关键代码段的作用非常相似,由于互斥对象是可以命名的,也就是说它可以跨越进程使用,因此,创建互斥对象需要的资源更多,所以如果只为了在进程内部实现同步的话,使用临关键代码段会带来速度上的优势并能够减少资源占用量。

互斥对象有两种状态,当它不被任何线程拥有时是有信号状态,而当它被拥有时则是无信号状态。互斥对象很适合用来协调不同进程种多个线程对共享资源的互斥访问。当互斥对象处于有信号状态时,第一个等待该互斥对象的线程将被唤醒,并重新将互斥对象设置为无信号状态,以免多个线程被同时唤醒;一旦拥有该互斥对象的线程执行完毕,它必须释放该互斥对象,这时,该互斥对象重新处于有信号状态,其他线程可以拥有它。

下面将介绍互斥对象的几个相关的函数:

1.HANDLE CreateMutex(

LPSECURITY_ATTRIBUTES lpMutexAttributes,

BOOL bInitialOwner,

LPCTSTR lpName

);

该函数用于创建一个互斥对象。调用该函数若创建成功,则返回互斥对象句柄;若要创建的命名互斥对象已经存在,则返回已存在的互斥对象的句柄;若调用该函数返回NULL,则表示创建失败。

第一个参数lpMutexAttributes,表示安全信息,可以忽略,通常为NULL。

第二个参数bInitialOwner,表示最初状态。如果该参数设置为TRUE,则表示创建它的线程直接拥有了该互斥对象,而不需要再申请;否则,表示创建它的线程没有获得该互斥对象的控制权。

第三个参数lpName,表示创建的互斥对象的名字。如果创建的互斥对象和已存在的互斥对象名字相同(即已经存在),那么第二个参数将被忽略。LpName也可以为NULL,表示创建的互斥对象没有名字,但是这样一来就不能被其他线程/进程打开。

2.HANDLE OpenMutex(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

LPCTSTR lpName

);

该函数用于打开一个存在的互斥对象。该函数调用成功返回打开的互斥对象的句柄;如果调用失败,则返回NULL。

第一个参数dwDesiredAccess,表示对互斥对象的存取方式。

操作系统OS报告读者与写者问题(进程同步问题)

目录 一、课程设计目的及要求 (1) 二、相关知识 (1) 三、题目分析 (2) 四、概要设计 (4) 五、代码及流程 (5) 六、运行结果 (11) 七、设计心得 (12) 八、参考文献 (12)

一、课程设计目的及要求 读者与写者问题(进程同步问题) 用n 个线程来表示n个读者或写者。每个线程按相应测试数据文件的要求,进行读写操作。请用信号量机制分别实现读者优先和写者优先的读者-写者问题。 读者-写者问题的读写操作限制: 1)写-写互斥; 2)读-写互斥; 3)读-读允许; 写者优先的附加限制:如果一个读者申请进行读操作时已有另一写者在等待访问共享资源,则该读者必须等到没有写者处于等待状态后才能开始读操作。 二、相关知识 Windows API: 在本实验中涉及的API 有: 1线程控制: CreateThread 完成线程创建,在调用进程的地址空间上创建一个线程,以执行指定的函数;它的返回值为所创建线程的句柄。 HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD DWORD dwStackSize, // initial stack size LPTHREAD_START_ROUTINE lpStartAddress, // thread function LPVOID lpParameter, // thread argument DWORD dwCreationFlags, // creation option LPDWORD lpThreadId // thread identifier ); 2 ExitThread 用于结束当前线程。 VOID ExitThread( DWORD dwExitCode // exit code for this thread ); 3Sleep 可在指定的时间内挂起当前线程。 VOID Sleep( DWORD dwMilliseconds // sleep time ); 4信号量控制: WaitForSingleObject可在指定的时间内等待指定对象为可用状态; DWORD WaitForSingleObject( HANDLE hHandle, // handle to object DWORD dwMilliseconds // time-out interval );

解决多线程中11个常见问题

并发危险 解决多线程代码中的11 个常见的问题 Joe Duffy 本文将介绍以下内容:?基本并发概念 ?并发问题和抑制措施 ?实现安全性的模式?横切概念本文使用了以下技术: 多线程、.NET Framework 目录 数据争用 忘记同步 粒度错误 读写撕裂 无锁定重新排序 重新进入 死锁 锁保护 戳记 两步舞曲 优先级反转 实现安全性的模式 不变性 纯度 隔离 并发现象无处不在。服务器端程序长久以来都必须负责处理基本并发编程模型,而随着多核处理器的日益普及,客户端程序也将需要执行一些任务。随着并发操作的不断增加,有关确保安全的问题也浮现出来。也就是说,在面对大量逻辑并发操作和不断变化的物理硬件并行性程度时,程序必须继续保持同样级别的稳定性和可靠性。 与对应的顺序代码相比,正确设计的并发代码还必须遵循一些额外的规则。对内存的读写以及对共享资源的访问必须使用同步机制进行管制,以防发生冲突。另外,通常有必要对线程进行协调以协同完成某项工作。 这些附加要求所产生的直接结果是,可以从根本上确保线程始终保持一致并且保证其顺利向前推进。同步和协调对时间的依赖性很强,这就导致了它们具有不确定性,难于进行预测和测试。 这些属性之所以让人觉得有些困难,只是因为人们的思路还未转变过来。没有可供学习的专门API,也没有可进行复制和粘贴的代码段。实际上的确有一组基础概念需要您学习和适应。很可能随着时间的推移某些语言和库会隐藏一些概念,但如果您现在就开始执行并发操作,则不会遇到这种情况。本

文将介绍需要注意的一些较为常见的挑战,并针对您在软件中如何运用它们给出一些建议。 首先我将讨论在并发程序中经常会出错的一类问题。我把它们称为“安全隐患”,因为它们很容易发现并且后果通常比较严重。这些危险会导致您的程序因崩溃或内存问题而中断。 当从多个线程并发访问数据时会发生数据争用(或竞争条件)。特别是,在一个或多个线程写入一段数据的同时,如果有一个或多个线程也在读取这段数据,则会发生这种情况。之所以会出现这种问题,是因为Windows 程序(如C++ 和Microsoft .NET Framework 之类的程序)基本上都基于共享内存概念,进程中的所有线程均可访问驻留在同一虚拟地址空间中的数据。静态变量和堆分配可用于共享。请考虑下面这个典型的例子: static class Counter { internal static int s_curr = 0; internal static int GetNext() { return s_curr++; } } Counter 的目标可能是想为GetNext 的每个调用分发一个新的唯一数字。但是,如果程序中的两个线程同时调用GetNext,则这两个线程可能被赋予相同的数字。原因是s_curr++ 编译包括三个独立的步骤: 1.将当前值从共享的s_curr 变量读入处理器寄存器。 2.递增该寄存器。 3.将寄存器值重新写入共享s_curr 变量。 按照这种顺序执行的两个线程可能会在本地从s_curr 读取了相同的值(比如42)并将其递增到某个值(比如43),然后发布相同的结果值。这样一来,GetNext 将为这两个线程返回相同的数字,导致算法中断。虽然简单语句s_curr++ 看似不可分割,但实际却并非如此。 忘记同步 这是最简单的一种数据争用情况:同步被完全遗忘。这种争用很少有良性的情况,也就是说虽然它们是正确的,但大部分都是因为这种正确性的根基存在问题。 这种问题通常不是很明显。例如,某个对象可能是某个大型复杂对象图表的一部分,而该图表恰好可使用静态变量访问,或在创建新线程或将工作排入线程池时通过将某个对象作为闭包的一部分进行传递可变为共享图表。 当对象(图表)从私有变为共享时,一定要多加注意。这称为发布,在后面的隔离上下文中会对此加以讨论。反之称为私有化,即对象(图表)再次从共享变为私有。 对这种问题的解决方案是添加正确的同步。在计数器示例中,我可以使用简单的联锁: static class Counter { internal static volatile int s_curr = 0; internal static int GetNext() { return Interlocked.Increment(ref s_curr);

实验2-2windows2000 线程同步

实验2 并发与调度 2.2 Windows 2000线程同步 (实验估计时间:120分钟) 背景知识 实验目的 工具/准备工作 实验内容与步骤 背景知识 Windows 2000提供的常用对象可分成三类:核心应用服务、线程同步和线程间通讯。其中,开发人员可以使用线程同步对象来协调线程和进程的工作,以使其共享信息并执行任务。此类对象包括互锁数据、临界段、事件、互斥体和信号等。 多线程编程中关键的一步是保护所有的共享资源,工具主要有互锁函数、临界段和互斥体等;另一个实质性部分是协调线程使其完成应用程序的任务,为此,可利用内核中的事件对象和信号。 在进程内或进程间实现线程同步的最方便的方法是使用事件对象,这一组内核对象允许一个线程对其受信状态进行直接控制 (见表4-1) 。 而互斥体则是另一个可命名且安全的内核对象,其主要目的是引导对共享资源的访问。拥有单一访问资源的线程创建互斥体,所有想要访问该资源的线程应该在实际执行操作之前获得互斥体,而在访问结束时立即释放互斥体,以允许下一个等待线程获得互斥体,然后接着进行下去。 与事件对象类似,互斥体容易创建、打开、使用并清除。利用CreateMutex() API 可创建互斥体,创建时还可以指定一个初始的拥有权标志,通过使用这个标志,只有当线程完成了资源的所有的初始化工作时,才允许创建线程释放互斥体。

为了获得互斥体,首先,想要访问调用的线程可使用OpenMutex() API来获得指向对象的句柄;然后,线程将这个句柄提供给一个等待函数。当内核将互斥体对象发送给等待线程时,就表明该线程获得了互斥体的拥有权。当线程获得拥有权时,线程控制了对共享资源的访问——必须设法尽快地放弃互斥体。放弃共享资源时需要在该对象上调用ReleaseMute() API。然后系统负责将互斥体拥有权传递给下一个等待着的线程(由到达时间决定顺序) 。 实验目的 在本实验中,通过对事件和互斥体对象的了解,来加深对Windows 2000线程同步的理解。 1) 回顾系统进程、线程的有关概念,加深对Windows 2000线程的理解。 2) 了解事件和互斥体对象。 3) 通过分析实验程序,了解管理事件对象的API。 4) 了解在进程中如何使用事件对象。 5) 了解在进程中如何使用互斥体对象。 6) 了解父进程创建子进程的程序设计方法。 工具/准备工作 在开始本实验之前,请回顾教科书的相关内容。 您需要做以下准备: 1) 一台运行Windows 2000 Professional操作系统的计算机。 2) 计算机中需安装Visual C++ 6.0专业版或企业版。 实验内容与步骤 1. 事件对象 2. 互斥体对象 1. 事件对象 清单2-1程序展示了如何在进程间使用事件。父进程启动时,利用CreateEvent() API创建一个命名的、可共享的事件和子进程,然后等待子进程向事件发出信号并终止父进程。在创建时,子进程通过OpenEvent() API打开事件对象,调用SetEvent() API使其转化为已接受信号状态。两个进程在发出信号之后几乎立即终止。 步骤1:登录进入Windows 2000 Professional。 步骤2:在“开始”菜单中单击“程序”-“Microsoft Visual Studio 6.0”–“Microsoft Visual C++ 6.0”命令,进入Visual C++窗口。

操作系统 实验 五 线程间的互斥与同步

实验五线程间的互斥与同步 实验学时:2学时 实验类型:验证、设计型 一、实验目的 理解POSIX线程(Pthread)互斥锁和POSIX信号量机制,学习它们的使用方法;编写程序,实现多个POSIX线程的同步控制。 二,实验内容 创建4个POSIX线程。其中2个线程(A和B)分别从2个数据文件(data1.txt和data2.txt)读取10个整数. 线程A和B把从文件中读取的逐一整数放入一个缓冲池. 缓冲池由n个缓冲区构成(n=5,并可以方便地调整为其他值),每个缓冲区可以存放一个整数。另外2个线程,C和D,各从缓冲池读取10数据。线程C、D每读出2个数据,分别求出它们的和或乘积,并打印输出。 提示:在创建4个线程当中,A和B是生产者,负责从文件读取数据到公共的缓冲区,C和D是消费者,从缓冲区读取数据然后作不同的计算(加和乘运算)。使用互斥锁和信号量控制这些线程的同步。不限制线程C和D从缓冲区得到的数据来自哪个文件。 在开始设计和实现之前,务必认真阅读课本6.8.4节和第6章后面的编程项目——生产者-消费者问题。

三,实验要求 按照要求编写程序,放在相应的目录中,编译成功后执行,并按照要求分析执行结果,并写出实验报告。 四,实验设计 1,功能设计 根据实验要求,主程序需要创建四个线程,两个线程负责从文件读取数据到缓冲区,两个线程负责将缓冲区的数据做数学运算。由于同一个进程中的各个线程共享资源,可以用一个二维数组的全局变量作为公共缓冲区,同时还需要一个整形全局变量size用来做数组的索引。读线程的运行函数打开不同的文件并从中读取数据到二维数组中,每次写入数组后size加一。运算线程从二维数组中读数并做运算,每次读数之前size减一。本题的关键在于如何使用信号量保证进程的同步与互斥。在运算线程从缓冲区读取之前缓冲区里必须有数,即任意时刻运算操作的执行次数必须小于等于读取操作的执行次数。同时应该保证两个读线程和两个运算线程两两互斥。由于以上分析,使用了四个信号量sem1,sem2,sem3和sem4。sem1保证线程1和线程2互斥,sem2保证线程3和线程4互斥,sem3保证线程3和线程4互斥,sem4保证线程4和线程1互斥。即这四个信号量使四个线程循环进行,从而保证了运行结果的正确性。 源代码及注释: #include #include #include #define NUM 200

OS中的进程线程同步机制

OS中的进程/线程同步机制 1 常用并发机制 1.1 信号量(Semaphore) 用于进程间传递信号的一个整数值,在信号上只可以进行三种操作,即初始化、递减和递增,这三种操作都是原子操作。递减操作用于阻塞一个进程,递增操作用于解除一个进程的阻塞。信号量也称为计数信号量或一般信号量 1.2 二元信号量(Binary Semaphore) 只取0值和1值的信号量。 1.3 互斥量(Mutex) 类似于二元信号量。关键在于为其加锁(设定值为0)的进程和为其解锁(设定值为1)的进程必须为同一个进程。 1.4 条件变量(Cond) 一种数据类型,用于阻塞进程或线程,直到特定的条件为真。 1.5 管程(Monitor) 一种编程语言结构,它在一个抽象数据类型中封装了变量、访问过程和初始化代码。管程的变量只能由管程自身的访问过程访问,每次只能有一个进程在其中执行,访问过程即临界区。管程可以有一个等待进程队列。 1.6 事件标志(Event Sign) 用作同步机制的一个内存字。应用程序代码可为标志中的每个位关联不同的事件。通过测试相关的一个或多个位,线程可以等待一个或多个事件。在全部所需位都被设定(AND)或至少一个位被设定(OR)之前,线程会一直被阻塞。 1.7 信箱/消息(Mailbox) 两个进程间交换信息的一种方法,也可用于同步。 1.8 自旋锁(Spin Lock) 一种互斥机制,进程在一个无条件循环中执行,等待锁变量的值可用。

2 常用进程/线程同步机制介绍 2.1 Windows OS中常用进程/线程同步机制 2.1.1 临界区(Critical Section) 可用于进程和线程同步。 保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。 临界区包含两个操作原语: EnterCriticalSection()进入临界区 LeaveCriticalSection()离开临界区 EnterCriticalSection()语句执行后代码将进入临界区以后无论发生什么,必须确保与之匹配的LeaveCriticalSection()都能够被执行到。否则临界区保护的共享资源将永远不会被释放。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。 MFC提供了很多功能完备的类,我用MFC实现了临界区。MFC为临界区提供有一个CCriticalSection类,使用该类进行线程同步处理是非常简单的。只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护代码片段即可。Lock()后代码用到的资源自动被视为临界区内的资源被保护。UnLock后别的线程才能访问这些资源。 2.1.2 互斥量(Mutex) 进程和线程都可用的一种同步机制。互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。 互斥量包含的几个操作原语: CreateMutex()创建一个互斥量 OpenMutex()打开一个互斥量 ReleaseMutex()释放互斥量 WaitForMultipleObjects()等待互斥量对象 2.1.3 信号量(Semaphore) 进程和线程都可用的同步机制。 信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一

实验2 线程同步机制

实验2 线程同步机制 一、实验目的: 通过观察共享数据资源但不受控制的两个线程的并发运行输出结果,体会同步机制的必要性和重要性。然后利用现有操作系统提供的同步机制编程实现关于该两个线程的有序控制,同时要求根据同步机制的Peterson软件解决方案尝试自己编程实现同步机制和用于同一问题的解决,并基于程序运行时间长短比较两种同步机制。 二、实验设计 I基于给定银行账户间转账操作模拟代码作为线程执行代码,在主线程中创建两个并发线程,编程实现并观察程序运行结果和予以解释说明。 II利用Windows互斥信号量操作函数解决上述线程并发问题,并分析、尝试和讨论线程执行体中有关信号量操作函数调用的正确位置。 III根据同步机制的Peterson软件解决方案尝试自己编程实现线程同步机制和用于上述线程并发问题的解决,并基于程序运行时间长短

将其与基于Windows互斥信号量的线程同步机制的效率展开比较。其间,可规定线程主体代码循环执行1000000次 三、源程序清单和说明 1未利用互斥信号量 #include #include #include int nAccount1 = 0, nAccount2 = 0; int nLoop = 0; int nTemp1, nTemp2, nRandom; DWORD WINAPI ThreadFunc(HANDLE Thread) { do { nTemp1 = nAccount1; nTemp2 = nAccount2; nRandom = rand(); nAccount1 = nTemp1 + nRandom; nAccount2 = nTemp2 - nRandom; nLoop++; } while ((nAccount1 + nAccount2) == 0); printf("循环次数为%d\n", nLoop); return 0; } int main() { HANDLE Thread[2]; Thread[0] = CreateThread(NULL,0,ThreadFunc,NULL,0,NULL); Thread[1] = CreateThread(NULL,0,ThreadFunc,NULL,0,NULL); WaitForMultipleObjects(2,Thread,TRUE,INFINITE); CloseHandle(Thread); return 0; }

操作系统课程设计用多进程同步方法解决生产者-消费者问题

操作系统课程设计 用多进程同步方法解决生产者-消费者问题 系别:计科系 专业: 计算机科学与技术 班级:04 级 4 班 学号:0410******* 姓名:苏德洪 时间:2006-7-7—2006-7-14

目录 一、题目: (3) 二、设计目的: (3) 三、总体设计思想概述: (3) 四、说明: (3) 五、设计要求: (3) 六、设计方案: (3) 七、流程图: (5) 八、运行结果 (7) 九、源程序 (11) 十、总结 (18) 十一、参考文献 (20)

一、题目: 用多进程同步方法解决生产者-消费者问题。 二、设计目的: 通过研究Linux 的进程机制和信号量实现生产者消费者问题的并发控制。 三、总体设计思想概述: 1、生产者—消费者问题是一种同步问题的抽象描述。 2、计算机系统中的每个进程都可以消费或生产某类资源。当系统中某一进程使用某一 资源时,可以看作是消耗,且该进程称为消费者。 3、而当某个进程释放资源时,则它就相当一个生产者。 四、说明: 有界缓冲区内设有20个存储单元,放入/取出的数据项设定为1-20这20个整型数。 五、设计要求: 1、每个生产者和消费者对有界缓冲区进行操作后,即时显示有界缓冲区的全部内容,当前 指针位置和生产者/消费者线程的标识符。 2、生产者和消费者各有两个以上。 3、多个生产者或多个消费者之间须有共享对缓冲区进行操作的函数代码。 六、设计方案: 通过一个有界缓冲区(用数组来实现,类似循环队列)把生产者和消费者联系起来。假定生产者和消费者的优先级是相同的,只要缓冲区未满,生产者就可以生产产品并将产品送入缓冲区。类似地,只要缓冲区未空,消费者就可以从缓冲区中去走产品并消费它。 应该禁止生产者向满的缓冲区送入产品,同时也应该禁止消费者从空的缓冲区中取出产品,这一机制有生产者线程和消费者线程之间的互斥关系来实现。 为解决生产者/消费者问题,应该设置两个资源信号量,其中一个表示空缓冲区的数目,用g_hFullSemaphore表示,其初始值为有界缓冲区的大小SIZE_OF_BUFFER;另一个表示缓冲区中产品的数目,用g_hEmptySemaphore表示,其初始值为0。另外,由于有界缓冲区是一个临界资源,必须互斥使用,所以还需要再设置一个互斥信号量g_hMutex,起初值为1。

4:一个经典的多线程同步问题汇总

一个经典的多线程同步问题 程序描述: 主线程启动10个子线程并将表示子线程序号的变量地址作为参数传递给子线程。子线程接收参数 -> sleep(50) -> 全局变量++ -> sleep(0) -> 输出参数和全局变量。 要求: 1.子线程输出的线程序号不能重复。 2.全局变量的输出必须递增。 下面画了个简单的示意图: 分析下这个问题的考察点,主要考察点有二个: 1.主线程创建子线程并传入一个指向变量地址的指针作参数,由于线程启动须要花费一定的时间,所以在子线程根据这个指针访问并保存数据前,主线程应等待子线程保存完毕后才能改动该参数并启动下一个线程。这涉及到主线程与子线程之间的同步。 2.子线程之间会互斥的改动和输出全局变量。要求全局变量的输出必须递增。这涉及到各子线程间的互斥。 下面列出这个程序的基本框架,可以在此代码基础上进行修改和验证。 //经典线程同步互斥问题 #include #include #include long g_nNum; //全局资源 unsigned int__stdcall Fun(void *pPM); //线程函数 const int THREAD_NUM = 10; //子线程个数 int main() { g_nNum = 0;

HANDLE handle[THREAD_NUM]; int i = 0; while (i < THREAD_NUM) { handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL); i++;//等子线程接收到参数时主线程可能改变了这个i的值} //保证子线程已全部运行结束 WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); return 0; } unsigned int__stdcall Fun(void *pPM) { //由于创建线程是要一定的开销的,所以新线程并不能第一时间执行到这来int nThreadNum = *(int *)pPM; //子线程获取参数 Sleep(50);//some work should to do g_nNum++; //处理全局资源 Sleep(0);//some work should to do printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum); return 0; } 运行结果:

Java多线程同步机制在售票系统的实现

Java多线程同步机制在售票系统的实现 论文导读:多线程技术的思想已经使用了很长的一段时间。但其不支持相同优先级的时间片轮换。多个用户线程在并发运行过程中可能同时访问临界区的内容。在Java中定义了线程同步的概念。关键词:多线程技术,多线程优先级,时间片,同步,临界区引言:多线程技术的思想已经使用了很长的一段时间,它允许CPU处理器时间共享,即很多用户可以共享处理器,每个用户的任务都分配到一段处理器时间。多线程是现代操作系统有别于传统操作系统的重要标志之一,它有别于传统的多进程的概念。所谓线程就是程序中的一个执行流,多线程程序是指一个程序中包含有多个执行流,多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发性的一个基本单位。1.基本概念:1.1线程与进程的主要区别:①同样作为基本的执行单元,线程的划分比进程小。②多进程每个占有独立的内存空间,而多线程共享同一内存空间,通过共享的内存空间来交换信息,切换效率远远高于多进程。③Java线程调度器支持不同优先级线程的抢占方式,但其不支持相同优先级的时间片轮换。④Java运行时系统所在的操作系统(例如:Windows XP)支持时间片的轮换,则线程调度器就支持相同优先级线程的时间片轮换。免费论文参考网。1.2Java 多线程的特点:1.2.1多线程的继承由于Java引入了包的概念,从而使类的继承更加简便,线程的创建就是一个最好的例子。Java多线程的实现有两种办法①通过Thread继承,在下面的研究中,我主要用继承自Thread类来实现Java的多线程技术。②通过Runnable接口。

1.2.2Java多线程的同步技术Java应用程序的多个线程共享同一进程的数据资源,多个用户线程在并发运行过程中可能同时访问临界区的内容,为了程序的正常运行,在Java中定义了线程同步的概念,实现对临界区共享资源的一致性的维护。1.3.3Java多线程的流程控制Java流程控制的方法有Sleep().Interrupt().Wait().Notif().Join()等。1.3.4临界区在一个多线程的程序当中,单独的并发的线程访问代码段中的同一对象,则这个代码段叫做临界区,我们需要用同步的机制对代码段进行保护,避免程序出现不确定的因素。1.3.5同步机制Java中支持线程的同步机制,它由synchronized方法实现,分为同步块和同步方法,在下面的讨论中用synchronized的同步块来解决问题。2.多线程同步机制在车票系统的实现2.1下面就以售票系统中所涉及的问题来讨论Java的多线程同步机制问题,在售票系统中由于很大一部分时间可能有多人在购买车票,所以必须开辟多个线程同时为他们服务,在这里我设有四个售票窗口,则开辟四个线程来为四个窗口服务模拟图如下:窗口 1 窗口2窗口 3 窗口4Thread1Thread2 Thread3Thread4售票窗口模拟图 2.2出错的程序代码如下:class TicketsSystem{public staticvoid main(String[] args){SellThread kt=new SellThread();new Thread(kt).start();new Thread(kt).start();new Thread(kt).start();new Thread(kt).start();}}class SellThreadextends Thread{inttickets=60;public voidrun(){while(true){if(tickets>0){System.out.println(Thread.currentThr ead().getName()+'sellticket '+tickets);tickets--;}}}}在上面的程序中为了

南昌大学操作系统线程进程同步实验报告

南昌大学实验报告 ---(1)进程/线程同步 学生姓名:学号:专业班级:网络工程131班 实验类型:■验证□综合□设计□创新实验日期:实验成绩: 一、实验目的 本实验讨论临界区问题及其解决方案。首先创建两个共享数据资源的并发线程。在没有同步控制机制的情况下,我们将看到某些异常现象。针对观察到的现象,本实验采用Windows 的信号量机制解决临界区互斥访问。 二、实验内容 2.1 进程/线程并发执行 Windows操作系统支持抢先式调度,这意味着一线程运行一段时间后,操作系统会暂停其运行并启动另一线程。也就是说,进程内的所有线程会以不可预知的步调并发执行。为了制造混乱,我们首先创建两个线程t1和t2。父线程(主线程)定义两个全局变量,比如accnt1和accnt2。每个变量表示一个银行账户,其值表示该账户的存款余额,初始值为0。线程模拟在两个账户之间进行转账的交易。也即,每个线程首先读取两个账户的余额,然后产生一个随机数r,在其中一个账户上减去该数,在另一个账户上加上该数。线程操作的代码框架如下: counter=0; do { tmp1 = accnt1 ; tmp2 = accnt2 ; r = rand ( ) ; accnt1 = tmp1 + r ; accnt2 = tmp2 ? r ; counter++; } while ( accnt1 + accnt2 == 0 ) ; print ( counter ) ; 两个线程执行相同的代码。只要它们的执行过程不相互交叉,那么两个账户的余额之和将永远是0。但如果发生了交叉,那么某线程就有可能读到新的accnt1值和老的accnt2值,从而导致账户余额数据发生混乱。线程一旦检测到混乱的发生,便终止循环并打印交易的次数(counter)。 请编写出完整的程序代码并运行,然后观察产生混乱需要的时间长短。因为这是我们编写的第一个程序,因此这里我给出了完整的代码,请参考。有能力的同学在参考下面的代码之前,请先自己尝试一下。 #include "stdafx.h" #include

多线程同步操作多个窗口

多线程同步操作多个窗口 RunApp "notepad.exe" RunApp "notepad.exe" RunApp "notepad.exe" Delay 2000 Dimenv temp_Hwnd temp_Hwnd = 0 Dim str, arr, i str = Plugin.Window.Search("无标题- 记事本") arr = Split(str, "|") For i = 0 To UBound(arr) - 1 temp_Hwnd = Plugin.Window.FindEx(arr(i), 0, "Edit", 0) BeginThread WriteString While temp_Hwnd <> 0'判断多线程已经启动完毕,继续循环下一个。 Delay 500 Wend Next EndScript Function WriteString() Dim str, Hwnd Hwnd = temp_Hwnd temp_Hwnd = 0 Do str = WaitKey If Hwnd <> Plugin.Window.GetKeyFocusWnd Then Call Plugin.Bkgnd.KeyPress(Hwnd, str) End If Loop End Function 多线程多开窗口同步执行与子线程间的数值如何传递: 1.Dimenv IsThread, i 2.Dim arr_Thread() 3.For i = 0 To 2 4. IsThread = False'未启动线程 5. Redim Preserve arr_Thread(i) 6. arr_Thread(i) = BeginThread(EnterThread) 7. While IsThread = False'未启动成功,等待中 8. Delay 500 9. Wend 10. '跳出循环说明 IsThread = True,已经执行到了,循环继续启动下一个 11.Next

Windows下多线程同步机制

多线程同步机制 Critical section(临界区)用来实现“排他性占有”。适用范围是单一进程的各线程之间。它是: ·一个局部性对象,不是一个核心对象。 ·快速而有效率。 ·不能够同时有一个以上的critical section被等待。 ·无法侦测是否已被某个线程放弃。 Mutex Mutex是一个核心对象,可以在不同的线程之间实现“排他性占有”,甚至几十那些现成分属不同进程。它是: ·一个核心对象。 ·如果拥有mutex的那个线程结束,则会产生一个“abandoned”错误信息。 ·可以使用Wait…()等待一个mutex。 ·可以具名,因此可以被其他进程开启。 ·只能被拥有它的那个线程释放(released)。 Semaphore Semaphore被用来追踪有限的资源。它是: ·一个核心对象。 ·没有拥有者。 ·可以具名,因此可以被其他进程开启。 ·可以被任何一个线程释放(released)。 Ev ent Object Ev ent object通常使用于overlapped I/O,或用来设计某些自定义的同步对象。它是: ·一个核心对象。 ·完全在程序掌控之下。 ·适用于设计新的同步对象。 · “要求苏醒”的请求并不会被储存起来,可能会遗失掉。 ·可以具名,因此可以被其他进程开启。 Interlocked Variable 如果Interlocked…()函数被使用于所谓的spin-lock,那么他们只是一种同步机制。所谓spin-lock是一种busy loop,被预期在极短时间内执行,所以有最小的额外负担(overhead)。系统核心偶尔会使用他们。除此之外,interlocked variables主要用于引用技术。他们:·允许对4字节的数值有些基本的同步操作,不需动用到critical section或mutex之类。 ·在SMP(Symmetric Multi-Processors)操作系统中亦可有效运作。 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

用多线程同步方法解决生产者-消费者问题(操作系统课设)

. 题目用多线程同步方法解决生产者-消费 者问题(Producer-Consumer Problem) 学院计算机科学与技术学院 专业软件工程 班级 姓名 指导教师 年月日

目录 目录 (1) 课程设计任务书 (2) 正文 (2) 1.设计目的与要求 (2) 1.1设计目的 (2) 1.2设计要求 (2) 2.设计思想及系统平台 (2) 2.1设计思想 (2) 2.2系统平台及使用语言 (2) 3.详细算法描述 (3) 4.源程序清单 (5) 5.运行结果与运行情况 (10) 6.调试过程 (15) 7.总结 (15) 本科生课程设计成绩评定表 (16)

课程设计任务书 学生姓名:专业班级: 指导教师:工作单位:计算机科学与技术学院 题目: 用多线程同步方法解决生产者-消费者问题 (Producer-Consumer Problem) 初始条件: 1.操作系统:Linux 2.程序设计语言:C语言 3.有界缓冲区内设有20个存储单元,其初值为0。放入/取出的数据项按增序设定为1-20这20个整型数。 要求完成的主要任务:(包括课程设计工作量及其技术要求,以及说明书撰写等具体要 求) 1.技术要求: 1)为每个生产者/消费者产生一个线程,设计正确的同步算法 2)每个生产者和消费者对有界缓冲区进行操作后,即时显示有界缓冲区的当前全部内容、当前指针位置和生产者/消费者线程的自定义标识符。 3)生产者和消费者各有两个以上。 4)多个生产者或多个消费者之间须共享对缓冲区进行操作的函数代码。 2.设计说明书内容要求: 1)设计题目与要求 2)总的设计思想及系统平台、语言、工具等。 3)数据结构与模块说明(功能与流程图) 4)给出用户名、源程序名、目标程序名和源程序及其运行结果。(要注明存储各个程序及其运行结果的主机IP地址和目录。) 5)运行结果与运行情况 (提示: (1)有界缓冲区可用数组实现。 (2)编译命令可用:cc -lpthread -o 目标文件名源文件名 (3)多线程编程方法参见附件。) 3. 调试报告: 1)调试记录 2)自我评析和总结 上机时间安排: 18周一~ 五 08:0 - 12:00 指导教师签名:年月日

四种进程或线程同步互斥的控制方法

四种进程或线程同步互斥的控制方法 1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。 2、互斥量:为协调共同对一个共享资源的单独访问而设计的。 3、信号量:为控制一个具有有限数量用户资源而设计。 4、事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。 一临界区 临界区的使用在线程同步中应该算是比较简单,说它简单还是说它同后面讲到的其它方法相比更容易理解。举个简单的例子:比如说有一个全局变量(公共资源)两个线程都会对它进行写操作和读操作,如果我们在这里不加以控制,会产生意想不到的结果。假设线程A 正在把全局变量加1然后打印在屏幕上,但是这时切换到线程B,线程B又把全局变量加1然后又切换到线程A,这时候线程A打印的结果就不是程序想要的结果,也就产生了错误。解决的办法就是设置一个区域,让线程A在操纵全局变量的时候进行加锁,线程B如果想操纵这个全局变量就要等待线程A释放这个锁,这个也就是临界区的概念。 二互斥体 windows api中提供了一个互斥体,功能上要比临界区强大。也许你要问,这个东东和临界区有什么区别,为什么强大?它们有以下几点不一致: 1.critical section是局部对象,而mutex是核心对象。因此像waitforsingleobject是不可以等待临界区的。 2.critical section是快速高效的,而mutex同其相比要慢很多 3.critical section使用围是单一进程中的各个线程,而mutex由于可以有一个名字,因此它是可以应用于不同的进程,当然也可以应用于同一个进程中的不同线程。 4.critical section 无法检测到是否被某一个线程释放,而mutex在某一个线程结束之后会产生一个abandoned的信息。同时mutex只能被拥有它的线程释放。下面举两个应用mutex 的例子,一个是程序只能运行一个实例,也就是说同一个程序如果已经运行了,就不能再运行了;另一个是关于非常经典的哲学家吃饭问题的例子。 三事件 事件对象的特点是它可以应用在重叠I/O(overlapped I/0)上,比如说socket编程中有两种模型,一种是重叠I/0,一种是完成端口都是可以使用事件同步。它也是核心对象,因此可以被waitforsingleobje这些函数等待;事件可以有名字,因此可以被其他进程开启。 四信号量 semaphore的概念理解起来可能要比mutex还难,我先简单说一下创建信号量的函数,因为我在开始使用的时候没有很快弄清楚,可能现在还有理解不对的地方,如果有错误还是请大侠多多指教。 CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // SD LONG lInitialCount, // initial count LONG lMaximumCount, // maximum count LPCTSTR lpName // object name )

操作系统第二章进程和线程复习题

第二章练习题 一、单项选择题 1.某进程在运行过程中需要等待从磁盘上读入数据,此时该进程的状态将( C )。 A. 从就绪变为运行; B.从运行变为就绪; C.从运行变为阻塞; D.从阻塞变为就绪2.进程控制块是描述进程状态和特性的数据结构,一个进程( D )。 A.可以有多个进程控制块; B.可以和其他进程共用一个进程控制块; C.可以没有进程控制块; D.只能有惟一的进程控制块。 3.临界区是指并发进程中访问共享变量的(D)段。 A、管理信息 B、信息存储 C、数据 D、 程序 4. 当__ B__时,进程从执行状态转变为就绪状态。 A. 进程被调度程序选中 B. 时间片到 C. 等待某一事件 D. 等待的事件发生 5. 信箱通信是一种( B )通信方式。 A. 直接通信 B. 高级通信

C. 低级通信 D. 信号量 6. 原语是(B)。 A、一条机器指令 B、若干条机器指令组成 C、一条特定指令 D、中途能打断的指令 7. 进程和程序的一个本质区别是(A)。 A.前者为动态的,后者为静态的; B.前者存储在内存,后者存储在外存; C.前者在一个文件中,后者在多个文件中; D.前者分时使用CPU,后者独占CPU。 8. 任何两个并发进程之间存在着(D)的关系。 A.各自完全独立B.拥有共享变量 C.必须互斥D.可能相互制约 9. 进程从运行态变为等待态可能由于(B )。 A.执行了V操作 B.执行了P 操作 C.时间片用完 D.有高优先级进程就绪 10. 用PV操作管理互斥使用的资源时,信号量的初值应定义为(B)。 A.任意整数 B.1 C.0 D.-1

操作系统实验线程同步

实验2:线程同步 一、实验目的 (1)掌握Windows2000环境下,线程同步。 (2)熟悉Windows2000提供的线程同步与互斥API。 (3)用Windows2000提供的线程同步与互斥API解决实际问题 (producer-consumer)。 二、实验内容 生产者与消费者问题的实现。在Windows 2000环境下,创建一组“生产者”线程和一组“消费者”线程,并建立一个长度为N的全局数组作为共享缓冲区。“生产者”向缓冲区输入数据,“消费者”从缓冲区读出数据。当缓冲区满时,“生产者”必须阻塞,等待“消费者”取走缓冲区数据后将其唤醒。当缓冲区空时,“消费者”阻塞,等待“生产者”生产了产品后将其唤醒。试用信号量实现“生产者”与“消费者”线程之间的同步。 三、实验环境 (1)使用的操作系统及版本。 Windows xp professional (2)使用的编译系统及版本。 Visual c++ 6.0 四、实验步骤 1.等待一个对象(相当于p操作) WaitForSingleObject用于等待一个对象。它等待的对象可以为: Change notification:变化通知。 Console input:控制台输入。 Event:事件。 Job:作业。 Mutex:互斥信号量。 Process:进程。 Semaphore:计数信号量。

Thread:线程。 Waitable timer:定时器。 返回值: 如果成功返回,其返回值说明是何种事件导致函数返回。 访问描述 WAIT_ABANDONED 等待的对象是一个互斥(mutex)对象,该互斥对 象没有被拥有它的线程释放,它被设置为不能被唤 醒。 WAIT_OBJECT_0 指定对象被唤醒。 WAIT_TIMEOUT 超时。 2.创建信号量 CreateSemaphore用于创建一个信号量。 返回值: 信号量创建成功,将返回该信号量的句柄。如果给出的信号量名是系统已经存在的信号量,将返回这个已存在信号量的句柄。如果失败,系统返回NULL,可以调用函数GetLastError查询失败的原因。 3.打开信号量 OpenSemaphore用于打开一个信号量。 返回值: 信号量打开成功,将返回该信号量的句柄。如果失败,系统返回NULL,可以调用函数GetLastError查询失败的原因。 4.增加信号量的值 ReleaseSemaphore用于增加信号量的值。 返回值: 如果成功,将返回一个非0值。如果失败,系统返回0,可以调用函数GetLastError 查询失败的原因。 方法一: 程序代码: #include #include

相关文档
最新文档