多线程引起的问题:
多线程可是提高程序的运算效率,但是对于进程中含有共享消息时必须使用线程同步,否则会发生与时间有关的错误,如乱码和竞争的现象,输出信息的杂乱。例如,多个线程同时访问同一个 全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修 改后的。
通过代码举例说明:
#include<iostream>
#include<windows.h>
using namespace std;
DWORD WINAPI ThreadProc1( LPVOID lpParameter);
DWORD WINAPI ThreadProc2( LPVOID lpParameter);
static int index = 0;
static int tickets=100;
int main(int argc, char* argv[])
{
HANDLE hTread1,hTread2;
hTread1 = CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
hTread2 = CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
CloseHandle(hTread1);
CloseHandle(hTread2);
Sleep(4000);
return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (TRUE)
{
if (tickets>0)
{
cout<<"thread1 sell ticket : "<<tickets--<<endl;
}
else
break;
}
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (TRUE)
{
if (tickets>0)
{
cout<<"thread2 sell ticket : "<<tickets--<<endl;
}
else
break;
}
return 0;
}
程序结果为:
线程同步:
为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。象这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。
线程的同步可分用户模式的线程同步和内核对象的线程同步两大类:
(1)其中用户模式中线程的同步方法主要有原子访问和临界区等方法。其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。
(2)内核对象的线程同步则主要由事件、互斥、信号量等内核对象构成。由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。
(1)其中用户模式中线程的同步方法主要有原子访问和临界区等方法。其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。
(2)内核对象的线程同步则主要由事件、互斥、信号量等内核对象构成。由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。
用户模式中的线程同步方法:
临界区
临界区是一段对共享资源的保护代码,该保护代码在任意时刻只允许一个线程对共享资源访问。如果有多个线程试图同时访问临界区,那么只有一个线程进入临界区,其他试图访问临界区的线程将被挂起,并一直持续到进入临界区的线程离开,其他线程才可以继续抢占临界区。注意:虽然临界区同步速度比较快,但是只能用来同步本进程内的线程,而不能跨进程同步。
相关的API函数:
(1)在使用临界区时必须声明一个关于CRITICAL_SECTION类型的变量来声明一个临界区,由于多个线程需要访问临界对象,所以需将它设置为全局对象。
(2)使用临界区之前需要在main函数中使用InitializeCriticalSection函数创建一个临界区对象。
void InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); lpCriticalSection是一个指向CRITICAL_SECTION类型的参数。
(3)当需要访问临界区的线程在进入临界区之前需要调用EnterCriticalSection函数,用来判断线程是否得到指定临界区对象的所有权,如果没有得到该所有权,那么该线程EnterCriticalSection函数一直等待,使该线程停止运行,直到该线程获取到临界区的所有权,访问到受保护的资源。
void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
(4)当该线程访问受保护的资源完成后,需要调用LeaveCriticalSection函数,释放指定的临近区对象的所有权。
void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
(5)当main函数退出之前,需要调用DeleteCriticalSection函数释放没有被任何线程使用的临界区对象的所有资源。
#include<iostream>
#include<windows.h>
using namespace std;
DWORD WINAPI ThreadProc1( LPVOID lpParameter);
DWORD WINAPI ThreadProc2( LPVOID lpParameter);
static int index = 0;
static int tickets=100;
CRITICAL_SECTION cs;
int main(int argc, char* argv[])
{
HANDLE hTread1,hTread2;
InitializeCriticalSection(&cs);
hTread1 = CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
hTread2 = CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
CloseHandle(hTread1);
CloseHandle(hTread2);
Sleep(4000);
DeleteCriticalSection(&cs);
return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (TRUE)
{
EnterCriticalSection(&cs);
if (tickets>0)
{
cout<<"thread1 sell ticket : "<<tickets--<<endl;
}
else
break;
LeaveCriticalSection(&cs);
}
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (TRUE)
{
EnterCriticalSection(&cs);
if (tickets>0)
{
cout<<"thread2 sell ticket : "<<tickets--<<endl;
}
else
break;
LeaveCriticalSection(&cs);
}
return 0;
}
程序结果如图:
内核模式中的线程同步方法:
互斥对象:
互斥(Mutex)是一种用途非常广泛的内核对象。能够保证多个线程对同一共享资源的互斥访问。同临界区有些类似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。与其他几种内核对象不同,互斥对象在操作系统中拥有特殊代码,并由操作系统来管理,操作系统甚至还允许其进行一些其他内核对象所不能进行的非常规操作。
互斥对象包含一个使用数量,一个线程ID和一个计数器。其中ID是用来标示系统中的哪一个线程当前拥有该互斥对象,计数器用于指明该线程拥有互斥对象的次数。
互斥对象包含一个使用数量,一个线程ID和一个计数器。其中ID是用来标示系统中的哪一个线程当前拥有该互斥对象,计数器用于指明该线程拥有互斥对象的次数。
使用过程:
(1)首先定义一个HANDLE类型的全局变量,用于保存即将创建的互斥对象句柄。
(2)在main函数中调用CreateMutex函数创建一个互斥对象。
HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性,可以给该参数传递为NULL。
BOOL bInitialOwner, //指定互斥对象初始的拥有者。如果为TRUE,则创建这个互斥对象的线程则拥有该
对象的所有权,否则,没有该互斥对象的所有权。
LPCTSTR lpName );//指定互斥对象的名称。如果为NULL,则创建一个匿名互斥对象。
如果调用成功,则该函数将返回创建的互斥对象的句柄。
(3)在线程需要保护的代码前添加WaitForSingleObject函数的调用,让该线程请求互斥对象的所有权,如果未请求到互斥对象所有权,则让线程一直等待,
除非所请求的对象处于有信号状态,该函数才会返回,线程才能继续往下执行,才能执行受保护的代码。
DWORD WaitForSingleObject( HANDLE hHandle, //所请求的互斥对象句柄
DWORD dwMilliseconds ); //指定等待的时间,以毫秒为单位。
线程通过该函数来主动请求互斥对象的使用权,如果互斥对象处于无信号状态,则该函数一直等待,使线程处于暂停状态。
该函数的返回值有三种情况:1>WAIT_OBJECT_0:所请求的对象是有信号状态。
2>WAIT_TIMEOUT:指定的时间间隔已过,但所请求的对象还是无信号状态。
3>WAIT_ABANDONED:始终为无信号状态。
(4)对所要保护的代码操作完成后,应该调用ReleaseMutex函数释放当前线程对互斥对象的所有权,这时,系统将该互斥对象的线程ID设置为0,然后将该互 斥对象是指为有信号状态,使其他线程有机会获取该互斥对象的所有权,从而获得对共享资源的方位。
BOOL ReleaseMutex( HANDLE hMutex );
(1)首先定义一个HANDLE类型的全局变量,用于保存即将创建的互斥对象句柄。
(2)在main函数中调用CreateMutex函数创建一个互斥对象。
HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性,可以给该参数传递为NULL。
BOOL bInitialOwner, //指定互斥对象初始的拥有者。如果为TRUE,则创建这个互斥对象的线程则拥有该
对象的所有权,否则,没有该互斥对象的所有权。
LPCTSTR lpName );//指定互斥对象的名称。如果为NULL,则创建一个匿名互斥对象。
如果调用成功,则该函数将返回创建的互斥对象的句柄。
(3)在线程需要保护的代码前添加WaitForSingleObject函数的调用,让该线程请求互斥对象的所有权,如果未请求到互斥对象所有权,则让线程一直等待,
除非所请求的对象处于有信号状态,该函数才会返回,线程才能继续往下执行,才能执行受保护的代码。
DWORD WaitForSingleObject( HANDLE hHandle, //所请求的互斥对象句柄
DWORD dwMilliseconds ); //指定等待的时间,以毫秒为单位。
线程通过该函数来主动请求互斥对象的使用权,如果互斥对象处于无信号状态,则该函数一直等待,使线程处于暂停状态。
该函数的返回值有三种情况:1>WAIT_OBJECT_0:所请求的对象是有信号状态。
2>WAIT_TIMEOUT:指定的时间间隔已过,但所请求的对象还是无信号状态。
3>WAIT_ABANDONED:始终为无信号状态。
(4)对所要保护的代码操作完成后,应该调用ReleaseMutex函数释放当前线程对互斥对象的所有权,这时,系统将该互斥对象的线程ID设置为0,然后将该互 斥对象是指为有信号状态,使其他线程有机会获取该互斥对象的所有权,从而获得对共享资源的方位。
BOOL ReleaseMutex( HANDLE hMutex );
include<iostream>
#include<windows.h>
using namespace std;
DWORD WINAPI ThreadProc1( LPVOID lpParameter);
DWORD WINAPI ThreadProc2( LPVOID lpParameter);
static int index = 0;
static int tickets=100;
HANDLE hMutex;//声明互斥对象变量
int main(int argc, char* argv[])
{
HANDLE hTread1,hTread2;
hMutex = CreateMutex(NULL,FALSE,NULL);//创建一个匿名的互斥对象
hTread1 = CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
hTread2 = CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
CloseHandle(hTread1);
CloseHandle(hTread2);
Sleep(4000);
CloseHandle(hMutex);
return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(hMutex,INFINITE);//请求互斥对象所有权,互斥对象为有信号状态时,继续执行。
if (tickets>0)
{
cout<<"thread1 sell ticket : "<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);//释放互斥对象所有权,让其他线程获取。
}
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (TRUE)
{
WaitForSingleObject(hMutex,INFINITE);
if (tickets>0)
{
cout<<"thread2 sell ticket : "<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
}
程序结果如上图。
事件对象:
事件对象也是属于内核对象,它的主要成员包括:1.使用计数 2.指明该事件是一个自动重置事件还是一个人工重置事件的布尔值3.指明该事件处于已通知状态还是未通知状态的布尔值。
人工重置事件和自动重置事件的区别:当人工重置事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;当一个自动重置事件对象得到通知时,等待该事件对象的线程只有一个变为可调度线程,同时操作系统会将该事件对象设置为无信号状态。这样,当对所保护的代码执行完成后,需要调用SetEvent函数将该事件对象设置为有信号状态。而人工重置事件对象在一个线程得到该事件对象之后,操作系统不会将该事件对象设置为无信号状态。除非显式地调用ResetEvent函数将其设置为无信号状态,否则该对象会已知是有信号状态。
人工重置事件和自动重置事件的区别:当人工重置事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;当一个自动重置事件对象得到通知时,等待该事件对象的线程只有一个变为可调度线程,同时操作系统会将该事件对象设置为无信号状态。这样,当对所保护的代码执行完成后,需要调用SetEvent函数将该事件对象设置为有信号状态。而人工重置事件对象在一个线程得到该事件对象之后,操作系统不会将该事件对象设置为无信号状态。除非显式地调用ResetEvent函数将其设置为无信号状态,否则该对象会已知是有信号状态。
使用过程:
(1)声明一个全局局部,用来保存即将创建的事件对象句柄。
HANDLE hEvent;
(2)在主线程main函数中调用CreateEvent函数创建一个事件内核对象。
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性,可以为NULL即使用默认的安全属性。
BOOL bManualReset, //指定是创建人工重置事件对象还是自动重置事件对象。
BOOL bInitialState, //指定事件对象的初始状态,如果为TRUE,则该对象初始是有信号状态,否则为无信号状态。
LPTSTR lpName ); //指定时间对象的名称。
如果为人工重置事件对象,当线程等待到该对象的所有权之后,需要调用ResetEvent函数手动将该事件对象设置为无信号状态;如果为自动重置事件对象,当 线程等到该对象的所有权之后,系统会自动将该对象设置为无信号状态。
(3)在main函数中调用CreateEvent函数创建事件对象后,其初始状态设置为无信号状态,这样其他线程请求这个事件对象时,因为该事件对象始终都是 无信号状 态,WaitForSingleObject函数将导致线程暂停,不能使其他线程获取到该事件对象,所以需要在创建事件对象之后,调用SetEvent函数将事件对象从无信号设置为 有信号状态。
(1)声明一个全局局部,用来保存即将创建的事件对象句柄。
HANDLE hEvent;
(2)在主线程main函数中调用CreateEvent函数创建一个事件内核对象。
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性,可以为NULL即使用默认的安全属性。
BOOL bManualReset, //指定是创建人工重置事件对象还是自动重置事件对象。
BOOL bInitialState, //指定事件对象的初始状态,如果为TRUE,则该对象初始是有信号状态,否则为无信号状态。
LPTSTR lpName ); //指定时间对象的名称。
如果为人工重置事件对象,当线程等待到该对象的所有权之后,需要调用ResetEvent函数手动将该事件对象设置为无信号状态;如果为自动重置事件对象,当 线程等到该对象的所有权之后,系统会自动将该对象设置为无信号状态。
(3)在main函数中调用CreateEvent函数创建事件对象后,其初始状态设置为无信号状态,这样其他线程请求这个事件对象时,因为该事件对象始终都是 无信号状 态,WaitForSingleObject函数将导致线程暂停,不能使其他线程获取到该事件对象,所以需要在创建事件对象之后,调用SetEvent函数将事件对象从无信号设置为 有信号状态。
(4)如果使用的是自动重置事件对象,那么某一通过WaitForSingleObject函数获取到该共享资源所有权后,将事件自动设置为无信号状态,防止其他线程访问该共享 资源。
(5)如果该线程访问该共享资源之后,需要通过SetEvent函数将事件对象从无信号状态转变为有信号状态,使其他线程能够访问该共享资源。
BOOL SetEvent( HANDLE hEvent ); //将指定的事件对象设置为有信号状态,使其他线程可以访问该事件对象的所有权。
(5)如果该线程访问该共享资源之后,需要通过SetEvent函数将事件对象从无信号状态转变为有信号状态,使其他线程能够访问该共享资源。
BOOL SetEvent( HANDLE hEvent ); //将指定的事件对象设置为有信号状态,使其他线程可以访问该事件对象的所有权。
#include <windows.h>
#include <iostream.h>
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int tickets=100;//static全局变量 共享资源
HANDLE g_hEvent;//事件对象句柄
void main()
{
HANDLE hThread1;
HANDLE hThread2;
//创建自动设置事件对象
g_hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);
SetEvent(g_hEvent);//重点就是这个设定
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(4000);
//关闭事件对象句柄
CloseHandle(g_hEvent);
}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while (true)
{
WaitForSingleObject(g_hEvent,INFINITE);
//ResetEvent(g_hEvent);
if (tickets>0)
{
Sleep(1);
cout<<"thread1 sell ticket :"<<tickets--<<endl;
SetEvent(g_hEvent);//重点就是这个设定
}
else
{
SetEvent(g_hEvent);//重点就是这个设定
break;
}
}
return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while (true)
{
//请求事件对象
WaitForSingleObject(g_hEvent,INFINITE);
//ResetEvent(g_hEvent);
if (tickets>0)
{
Sleep(1);
cout<<"thread2 sell ticket :"<<tickets--<<endl;
SetEvent(g_hEvent);//重点就是这个设定
}
else
{
SetEvent(g_hEvent);//重点就是这个设定
break;
}
}
return 0;
}
程序结果如上:
互斥对象、事件对象与临界区的比较:
1>互斥对象、事件对象都是属于内核对象,利用内核对象进行线程同步,速度较慢,但可以在多个进程中的多个线程间可以进行同步。
2>临界区属于在用户模式下,同步速度较快,但是很容易进入死锁状态,因为在等待进入临界区时无法设定超时值。
2>临界区属于在用户模式下,同步速度较快,但是很容易进入死锁状态,因为在等待进入临界区时无法设定超时值。