关于多线程的概念问题,请参考http://blog.csdn.net/hust1900/article/details/8701984转载的博文。
进程和线程都是操作系统的概念。进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。通常我们使用的软件,例如office,QQ等等都是一个个的进程。
线程是进程内部的一个执行单元。一个进程可能包含多个线程(例如,我们可以同时打开多个word文档,可以同时打开多个QQ,即是是一个QQ,我们也可以同时和多个人聊天等等)系统创建好进程后,实际上就启动执行了该进程的主执行线程,主执行线程以函数地址形式,比如说main或WinMain函数,将程序的启动点提供给Windows系统。主执行线程终止了,进程也就随之终止。
每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。一个进程中的所有线程都在该进程的虚拟地址空间中,共同使用这些虚拟地址空间、全局变量和系统资源,所以线程间的通讯非常方便,多线程技术的应用也较为广泛。
操作系统为每一个运行线程安排一定得CPU时间——时间片。多线程可以实现并行处理,避免了某项任务长时间占用CPU时间。要说明的一点是,目前大多数的计算机都是单处理器(CPU)的,为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮换方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。由此可见,如果两个非常活跃的线程为了抢夺对CPU的控制权,在线程切换时会消耗很多的CPU资源,反而会降低系统的性能。这一点在多线程编程时应该注意。
多线程编程的流程和思路。
多线程一般都是从主线程入手,通过创建多个子线程来分时处理共同的资源。
思路:
第一,要在主线程中创建子线程入口,从而进入子线程中处理;
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security attributes
DWORD dwStackSize, // initial thread stack size
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function 即是我们需要定义的子线程处理函数,可以随意命名,此例中我们命名为Fun1Proc()
LPVOID lpParameter, // argument for new thread
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId // pointer to receive thread ID
);
第二,定义子线程实现程序
DWORD Fun1Proc(LPVOID lpParameter);
我们 要实现的子线程的各种函数功能都是在这个函数中定义的,一旦程序从主线程进入到子线程中,就要处理相应地子线程的函数功能。
第三,线程之间的切换以及子线程和主线程之间的切换(因为CPU处理进程中的一个线程,所以要做好线程之间关于CPU的使用权的转换问题)
因为线程中,多个子线程之间,可能因为对公共资源的访问不受限制而子线程之间又没有一个相互约束等条件,可能会导致子线程访问的随机错误,例如下例中:
本文通过一个火车站卖票的简单化的程序来认识VC编程的多线程问题。
// 多线程.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "windows.h"
#include <iostream.h>
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int index=0;
int tickets=100;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
//创建线程
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(4000);
}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(TRUE)
{
if(tickets>0)
{
cout<<"thread1 sell ticket:"<<tickets--<<endl;
}
else
break;
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(TRUE)
{
if(tickets>0)
{
cout<<"thread2 sell ticket:"<<tickets--<<endl;
}
else
break;
}
return 0;
}
程序运行结果:
上例我们想要得到的结果是线程1和线程2交替访问,得到票,可是,运行后结果我们会发现,程序的最后几行全部都是输出的线程1来访问的,线程2 没有再访问到。虽然这个错误是随机性的,再次运行可能不会出现,也可能在某些地方出现线程2访问而线程1不访问,但是对于系统来说是不能出现的。造成这样的主要原因在于,我们未对两个子线程访问主内存添加任何限制条件。当从主线程跳到子线程的时候,我们进入到了子线程的内部后,可能由于子线程1的分配的时间片到了,但是我们程序还没有完成子线程1的处理,这样就直接跳出到线程2中,从而造成了公共资源的不同步,从而导致我们的输出结果出错(因为子线程1没有输出,而直接进入子线程2输出了本该子线程1输出的票数)
这样,我们就有一个思路了,为什么不给子线程们分配足够多的时间呢?即我们可能让子线程1在处理的同时,子线程2处于休眠状态。这可以通过给子线程设置一个休眠时间来实现Sleep().这样,当子线程1执行完后,再执行Sleep(),处于休眠状态,然后其他线程就可以再这段时间内单独访问进程中的共享资源。访问完成后,子线程2也进入休眠状态,而子线程1则度过了休眠期,又开始执行访问操作。
这样,就可以保证子线程们可以有序的访问进程内的共享资源,而不会出现本该1线程访问输出的资源被子线程2得到了。这里介绍互斥对象实现线程同步:
互斥对象属于内核对象,它能够确保线程中拥有对单个资源的互斥访问权。互斥对象包含一个使用数量,一个线程ID和一个计数器。其中线程ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。
这里用到了函数:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
// pointer to security attributes
BOOL bInitialOwner, // flag for initial ownership
LPCTSTR lpName // pointer to mutex-object name
);
当线程对共享资源访问结束之后,应释放该对象的所有权,也就是让该对象处于已通知状态,这里就需要调用函数:
BOOL ReleaseMutex(
HANDLE hMutex // handle to mutex object
);
另外,线程必须主动请求共享对象的使用权才有可能获得该所有权,这可以通过
DWORD WaitForSingleObject(
HANDLE hHandle, // handle to object to wait for
DWORD dwMilliseconds // time-out interval in milliseconds
);
修改后的例子如下:
// 多线程.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "windows.h"
#include <iostream.h>
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int index=0;
int tickets=100;
HANDLE hMutex;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
//创建互斥对象
hMutex=CreateMutex(NULL,FALSE,NULL);
//创建线程
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(4000);
//设置主线程的一个休眠时间,这样,当主线程执行到此的时候,自动进入休眠期,释放对CPU的控制,而内存资源就会被其他线程访问
}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(TRUE)
{
WaitForSingleObject(hMutex,INFINITE);
if(tickets>0)
{
Sleep(1);
cout<<"thread1 sell ticket:"<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(TRUE)
{
WaitForSingleObject(hMutex,INFINITE);
if(tickets>0)
{
Sleep(1);
cout<<"thread2 sell ticket:"<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
}
运行结果:
下面介绍利用事件对象方法来实现线程同步
首先明确,事件对象也属于内核对象,它包含以下三个成员:
1,使用计数
2,用于指明该事件是一个自动重置事件还是一个人工重置事件的布尔值
3,用于指明该事件处于已通知状态还是未通知状态的布尔值。
步骤如下:
创建一个事件对象
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //通常设置为NULL
// pointer to security attributes
BOOL bManualReset, // flag for manual-reset event //指明是人工还是自动重置。若为TRUE,则为人工重置,若为FALSE则是自动重置
BOOL bInitialState, // flag for initial state //创建事件对象的初始状态,一般设为FALSE,为无信号状态。然后使用SetEvent来激活
LPCTSTR lpName // pointer to event-object name //事件对象名 一般为NULL,意为创建一个匿名事件对象
);
设置事件对象状态为有信号状态(一般紧接着CreateEvent()函数)
BOOL SetEvent(
HANDLE hEvent // handle to event object
);
重置事件对象状态 设置对象状态为无信号状态(一般在子线程执行完之后设置,从而可以让其他的线程使用事件对象来执行)
BOOL ResetEvent(
HANDLE hEvent // handle to event object
);
事件对象分为人工重置和自动重置两种,因此可以使用两种方法来编程。
使用自动重置事件对象来编程
// 5.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "windows.h"
#include <iostream.h>
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int index=0;
int tickets=100;
//HANDLE hMutex;
HANDLE g_hEvent;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
//创建互斥对象
//hMutex=CreateMutex(NULL,FALSE,NULL);
//创建自动重置事件对象,为无信号状态
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);
//设置主线程的一个休眠时间,这样,当主线程执行到此的时候,自动进入休眠期,释放对CPU的控制,而内存资源就会被其他线程访问
CloseHandle(g_hEvent);
}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
if(tickets>0)
{
Sleep(1);
cout<<"thread1 sell ticket:"<<tickets--<<endl;
SetEvent(g_hEvent);
}
else
{
SetEvent(g_hEvent);
break;
}
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
if(tickets>0)
{
Sleep(1);
cout<<"thread2 sell ticket:"<<tickets--<<endl;
SetEvent(g_hEvent);
}
else
{
SetEvent(g_hEvent);
break;
}
}
return 0;
}
运行结果:
人工重置事件对象和自动重置事件对象的区别:
当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调线程;当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程,同时操作系统会将该事件对象设置无信号状态,这样,当对所有保护的代码执行完成后,需要调用SetEvent函数将该事件对象设置为有信号状态。而人工重置的事件对象,在一个线程得到该事件对象后,操作系统并不会将该事件对象设置为无信号状态,除非显式地调用ResetEvent函数将其设置为无信号状态,否则该对象会一直有信号下去。