操作系统.docx
《操作系统.docx》由会员分享,可在线阅读,更多相关《操作系统.docx(22页珍藏版)》请在冰豆网上搜索。
![操作系统.docx](https://file1.bdocx.com/fileroot1/2022-12/12/fc918766-85a3-418a-886d-11889e51b9cb/fc918766-85a3-418a-886d-11889e51b9cb1.gif)
操作系统
线程—第一课时
计算机系统由硬件和软件两部分组成,操作系统OS(OperatingSystem)是配置在计算机硬件上的第一层软件,是对硬件的首次扩充。
操作系统的定义:
是控制和管理计算机系统的硬件和软件资源,合理地组织计算机工作流程,以及方便用户的程序和数据的集合。
见下图:
当前windows操作系统属于实时操作系统,也是抢占式操作系统。
它有这样几个特性:
并发、共享、虚拟、异步。
操作系统的主要任务,是为多道程序的运行提供良好的运行环境,以保证多道程序有条不紊地、高效的运行,并能最大程度地提高系统中各种资源的利用率和方便用户的使用。
在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单位是进程。
如果说,在操作系统中,引入进程的目的,是为了是多个程序能并发执行,以提高资源利用率和系统吞吐量,那么,在操作系统中,引入线程,则是为了减少程序在并发执行时所付出的开销。
线程的定义:
线程(thread,台湾称执行绪)是"进程"中某个单一顺序的控制流。
也被称为轻量进程(lightweightprocesses)。
计算机科学术语,指运行中的程序的调度单位。
线程两部分组成:
一个是线程的内核对象操作系统用它管理线程。
系统还用内核对象来存放线程统计信息的地方。
一个线程栈用于维护线程执行时所需的所有函数参数和局部变量。
1)轻型实体
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源,比如,在每个线程中都应具有一个用于控制线程运行的线程控制块TCB,用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
2)独立调度和分派的基本单位。
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。
由于线程很“轻”,故线程的切换非常迅速且开销小。
3)可并发执行。
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行。
4)共享进程资源。
在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:
所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。
线程与进程的区别可以归纳为以下几点:
1)地址空间和其它资源(如打开文件):
进程间相互独立,同一进程的各线程间共享。
某进程内的线程在其它进程不可见。
2)通信:
进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:
线程上下文切换比进程上下文切换要快得多。
4)在多线程OS中,进程不是一个可执行的实体。
并行是指在同一时刻,有多条指令在多个处理器上同时执行。
并发是指在同一时刻,只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
“并行”是指无论从微观还是宏观,二者都是一起执行的,就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。
而“并发”在微观上不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行,从宏观外来看,好像是这些进程都在执行,这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。
从以上本质不难看出,“并发”执行,在多个进程存在资源冲突时,并没有从根本提高执行效率。
何时创建线程?
比如公交车,一个司机开一圈,两个司机开一圈,时间可能两个司机更慢一些。
多此一举。
接下来我们来由一个例子引入线程:
在一个按钮的事件处理中:
while(i<=100/*&&p->m_bflag*/)
{
Sleep(20);
p->m_progressCtrl.SetPos(i++);
}
当程序在走while循环时,程序死等在那里,只有while循环结束,才能继续执行其他代码。
因此引入线程.
一、首先,创建线程:
Win32提供了一系列的API函数来完成线程的创建、挂起、恢复、终结以及通信等工作。
下面将选取其中的一些重要函数进行说明。
1、HANDLECreateThread(LPSECURITY_ATTRIBUTESlpThreadAttributes,
DWORDdwStackSize,
LPTHREAD_START_ROUTINElpStartAddress,
LPVOIDlpParameter,
DWORDdwCreationFlags,
LPDWORDlpThreadId);
该函数在其调用进程的进程空间里创建一个新的线程,并返回已建线程的句柄,其中各参数说明如下:
lpThreadAttributes:
指向一个SECURITY_ATTRIBUTES结构的指针,该结构决定了线程的安全属性,一般置为NULL;
dwStackSize:
指定了线程的堆栈深度,一般都设置为0;
lpStartAddress:
表示新线程开始执行时代码所在函数的地址,即线程的起始地址。
一般情况为(LPTHREAD_START_ROUTINE)ThreadFunc,ThreadFunc是线程函数名;
lpParameter:
指定了线程执行时传送给线程的32位参数,即线程函数的参数;
dwCreationFlags:
控制线程创建的附加标志,可以取两种值。
如果该参数为0,线程在被创建后就会立即开始执行;如果该参数为CREATE_SUSPENDED,则系统产生线程后,该线程处于挂起状态,并不马上执行,直至函数ResumeThread被调用;
lpThreadId:
该参数返回所创建线程的ID;
如果创建成功则返回线程的句柄,否则返回NULL。
2、DWORDSuspendThread(HANDLEhThread);
该函数用于挂起指定的线程,如果函数执行成功,则线程的执行被终止。
3、DWORDResumeThread(HANDLEhThread);
该函数用于结束线程的挂起状态,执行线程。
4、VOIDExitThread(DWORDdwExitCode);
该函数用于线程终结自身的执行,主要在线程的执行函数中被调用。
其中参数dwExitCode用来设置线程的退出码。
5、BOOLTerminateThread(HANDLEhThread,DWORDdwExitCode);
一般情况下,线程运行结束之后,线程函数正常返回,但是应用程序可以调用TerminateThread强行终止某一线程的执行。
各参数含义如下:
hThread:
将被终结的线程的句柄;
dwExitCode:
用于指定线程的退出码。
使用TerminateThread()终止某个线程的执行是不安全的,可能会引起系统不稳定;虽然该函数立即终止线程的执行,但并不释放线程所占用的资源。
因此,一般不建议使用该函数。
6、BOOLPostThreadMessage(DWORDidThread,
UINTMsg,
WPARAMwParam,
LPARAMlParam);
该函数将一条消息放入到指定线程的消息队列中,并且不等到消息被该线程处理时便返回。
idThread:
将接收消息的线程的ID;
Msg:
指定用来发送的消息;
wParam:
同消息有关的字参数;
lParam:
同消息有关的长参数;
调用该函数时,如果即将接收消息的线程没有创建消息循环,则该函数执行失败。
二、写个例子:
类似播放器的按钮,开始、暂停和停止
1点击开始按钮—创建线程
m_hThread=CreateThread(NULL,//是否被继承
0,//线程堆栈的大小
&ThreadProc,//函数的地址
this,//线程函数参数
CREATE_SUSPENDED,//创建标志0立即执行
&m_nid//线程ID
);
if(!
m_hThread)
{
MessageBox(_T("线程创建失败"));
}
ResumeThread(m_hThread);//恢复线程
创建线程函数
DWORDWINAPICthreadDlg:
:
ThreadProc(LPVOIDlpParameter)
{
CthreadDlg*p=(CthreadDlg*)lpParameter;
while(1/*p->m_bflag*/)
{
//如果事件有信号则结束当前线程
if(WaitForSingleObject(p->m_hEvent,100)==WAIT_OBJECT_0)
{
break;
}
inti=0;
while(i<=100/*&&p->m_bflag*/)
{
Sleep(20);
p->m_progressCtrl.SetPos(i++);
}
}
//有信号
SetEvent(p->m_hCheckEvent);
return0;
}
2.点击暂停–挂起线程
SuspendThread(m_hThread);
3点击停止---结束线程
结束线程时有3种方法
(1)全局变量
(2)事件(双事件)
将一个事件置为有信号,则线程中有个等待函数,当等到信号,则跳出循环,线程结束。
(3)强制终止
Exitthread()结束线程,只能结束当前线程,将操作系统分配的资源回收
Terminatethread()结束任意线程,不会释放该线程资源,直到包含当前线程的进程结束。
用户界面线程—1课时
1.用MFC的afxBeginthread创建线程有两种
工作者线程:
后台运行,没有自己的消息队列,直到向这个线程中发送消息,系统会为此线程创建一个消息队列.
用户界面线程:
有自己的界面和消息队列。
创建用户界面线程的步骤:
1.添加一个对话框类
2.添加一个又CWinThread派生的一个线程类
在线程的初始化函数InitInstance中
Cdigdig;
m_pMainwnd=&dig;//将对话框和线程类联系在一块,作为用户界面线程的主窗口.
Dig.domodal();
问题:
1.如果先关闭自己创建出来的线程,最后关闭进程的主线程没问题。
如果直接关闭主线程,则会有内存泄露,为什么呢?
原因是由于包含自己创建的线程的进程已经结束,自己线程强制终止,造成内存泄露。
解决方法:
在主线程退出之前,将自己创建的线程一一结束。
(通过发送消息的方式)。
代码如下:
//结束自己创建出来的线程
for(inti=0;i{
m_pthread[i]->PostThreadMessage(WM_QUIT,0,0);
WaitForSingleObject(m_pthread[i]->m_hThread,INFINITE);
}
线程间通信—1课时
线程间通信有两种方法:
1.全局变量
2.发送消息
由一个例子说明:
题目:
首先点击启动线程创建一个线程A,然后去选择一个要去运算的数,点击计算的时候,将这个数由主线程----线程A,线程A计算出结果,将结果由线程A---主线程
1.启动线程
m_hThread=CreateThread(NULL,0,&ThreadProc,this,CREATE_SUSPENDED,&m_nThreadID);
if(!
m_hThread)
{
MessageBox(_T("创建线程失败"));
}
ResumeThread(m_hThread);
DWORDWINAPICthread2Dlg:
:
ThreadProc(LPVOIDlpParameter)
{
Cthread2Dlg*pthis=(Cthread2Dlg*)lpParameter;
MSGmsg;
while
(1)
{
intsum=0;
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
if(msg.message==UM_MSG)
{
for(inti=0;i<=msg.wParam/*pthis->m_nsum*/;i++)
{
sum+=i;
}
//将计算结果返回到主线程
//pthis->PostMessage(UM_MSG,sum);
//theApp.PostThreadMessage(UM_MSG,sum,0);
/*CStringstr;
str.Format(_T("%d"),sum);
pthis->GetDlgItem(IDC_EDIT1)->SetWindowText(str);*/
}
}
}
return0;
}
2.计算
//获得当前点击的单选按钮(即要计算的值)
UpdateData(TRUE);
switch(m_nradio)
{
case0:
m_nsum=100;
break;
case1:
m_nsum=1000;
break;
case2:
m_nsum=10000;
break;
}
//将要计算的结果发送给线程A
PostThreadMessage(m_nThreadID,UM_MSG,m_nsum,0);
注意:
如果是通过//theApp.PostThreadMessage(UM_MSG,sum,0);将数据由线程A发向UI线程,则应该在APP类中,添加响应线程的自定义消息代码如下:
voidonmsg(WPARAMwparam,LPARAMlparam);
ON_THREAD_MESSAGE(UM_MSG,&Cthread2App:
:
onmsg)
voidCthread2App:
:
onmsg(WPARAMwparam,LPARAMlparam)
{
CStringstr;
str.Format(_T("%d"),wparam);
//GetDlgItem(IDC_EDIT1)->SetWindowText(str);
}
线程同步—2课时
虽然多线程能给我们带来好处,但是也有不少问题需要解决。
例如,对于像磁盘驱动器这样独占性系统资源,由于线程可以执行进程的任何代码段,且线程的运行是由系统调度自动完成的,具有一定的不确定性,因此就有可能出现两个线程同时对磁盘驱动器进行操作,从而出现操作错误;又例如,对于银行系统的计算机来说,可能使用一个线程来更新其用户数据库,而用另外一个线程来读取数据库以响应储户的需要,极有可能读数据库的线程读取的是未完全更新的数据库,因为可能在读的时候只有一部分数据被更新过。
使隶属于同一进程的各线程协调一致地工作称为线程的同步。
MFC提供了多种同步对象,下面只介绍最常用的四种:
临界区(CCriticalSection)
事件(CEvent)
互斥量(CMutex)
信号量(CSemaphore)
通过这些类,可以比较容易地做到线程同步。
由一个卖火车票例子引入:
现在由xx地到xx地有1000张车票,火车站有10个窗口同时售票,直到卖完为止。
解题思路:
首先,创建10个线程,同时执行卖票操作,线程在操作系统中并行或者并发的运行,不能保证同一张票就卖给一个人,或者可能卖到负数的情况。
所以引入临界区和互斥量。
0.考虑到多个线程访问一个变量的问题。
可以采用原子访问。
原子访问:
指的是一个线程在访问某个资源的同时,能够保证没有其他线程会在同一时刻访问同一资源。
Interlocked系列。
InterlockedExchangeAdd(&g_x,1);
InterlockedIncrement(&g_x);
InterlockedDecrement(&g_x);
Interlocked系列函数非常好用。
我们当然优先考虑使用它们。
但是大多数实际的编程问题需要处理的数据结构往往比一个简单的32位值或者64位值复杂的多。
这个时候需要用到关键段即临界区。
关键段:
是一小段代码,它在执行之前需要独占对一些共享资源的访问权。
这种方式可以让多行代码以“原子方式”来对资源进行操控。
这里的原子方式,指的是代码知道除了当前线程外,没有其他任何线程会同时访问该资源。
当前系统仍然可以暂停当前线程去调度其他的线程,但是在当前线程离开关键段之前,系统是不会调度任何想要访问同一资源的其他线程。
CRITICAL_SECTIONg_cs;//比如飞机上的卫生间,马桶则是我们要保存的数据
IniticalizeCriticalsection(&g_cs);//初始化CRITICAL_SECTION中的成员变量
EnterCriticalSection(&g_cs);//线程用它来检查占用标志的函数
//有如下几种情况,
(1)如果当前没有线程在访问资源(卫生间门上显示为无人),那么它将允许调用线程进入“卫生间”。
将门上的标志置为有人.
(2)如果EnterCriticalSection发现已经有另一个线程在“卫生间”,那么调用线程在“卫生间”门外等待(系统会为第一想要访问该“卫生间”的线程创建一个事件的内核对象,将其由用户模式切换到内核模式,处于等待状态),直到另一个线程离开“卫生间”为止。
(系统会从一些想要访问“卫生间”的线程中,将其中一个线程置为可调度状态)
LeaveCriticalSection(&g_cs);//离开“卫生间”,门上重新置为无人
DeleteCriticalsection(&g_cs);//删除事件的内核对象,以及为CRITICAL_SECTION初始化的资源。
注意:
如果现在有线程正在访问临界区,其他想要访问临界区的线程走到EnterCriticalSection的时候,EnterCriticalSection会把此线程由用户模式切换到内核模式(切换的时间大约是1000个CPU周期),当访问临界区的线程离开临界区,系统会将想要访问临界区的线程切换到可调度的状态。
切换过于浪费时间,
(1)所以可以用TryEnterCriticalSection来代替EnterCriticalSection。
TryEnterCriticalSection线程在访问时,如果不能访问资源,那么它继续做其他事情,而不用等待。
(2)用旋转锁。
为了提高关键段的性能,microsoft把旋转锁合并到了关键段中.因此,当调用EnterCriticalSection的时候,它会用一个旋转锁不断的循环,尝试在一段时间内获得访问权。
只有当尝试失败的时候,线程才会切换到内核模式并进入等待状态。
为了在使用关键段的时候同时使用旋转锁,我们必须调用下面的函数来初始化关键段。
boolInitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTIONpcs,
DOWDdwSpinCount
);
SetCriticalSectionSpinCount(//改变旋转锁的次数
PCRITICAL_SECTIONpcs,
DOWDdwSpinCount
)
关键段和错误处理
一.使用CCriticalSection类
当多个线程访问一个独占性共享资源时,可以使用“临界区”对象。
任一时刻只有一个线程可以拥有临界区对象,拥有临界区的线程可以访问被保护起来的资源或代码段,其他希望进入临界区的线程将被挂起等待,直到拥有临界区的线程放弃临界区时为止,这样就保证了不会在同一时刻出现多个线程访问共享资源。
CCriticalSection类的用法非常简单,步骤如下:
1.定义CCriticalSection类的一个全局对象(以使各个线程均能访问),如CCriticalSectioncritical_section;
2.在访问需要保护的资源或代码之前,调用CCriticalSection类的成员Lock()获得临界区对象:
critical_section.Lock();
3.在线程中调用该函数来使线程获得它所请求的临界区。
如果此时没有其它线程占有临界区对象,则调用Lock()的线程获得临界区;否则,线程将被挂起,并放入到一个系统队列中等待,直到当前拥有临界区的线程释放了临界区时为止。
4.访问临界区完毕后,使用CCriticalSection的成员函数Unlock()来释放临界区:
critical_section.Unlock();
通俗讲,就是线程A执行到critical_section.Lock();语句时,如果其它线程(B)正在执行critical_section.Lock();语句后且critical_section.Unlock();语句前的语句时,线程A就会等待,直到线程B执行完critical_section.Unlock();语句,线程A才会继续执行。
二.使用互斥量CMUTEX类
互斥量内核对象用来确保一个线程独占对于一个资源的访问。
互斥量的用法相当于现在有一堆人,等待空中的气球(互斥量),一旦有人抢到了,则他独占,直到他释放互斥量。
互斥对象包含一个使用计数、线程ID以及一个递归计数。
线程ID:
用来标识当前占用这个互斥量的是系统中那个线程。
如果为false则此互斥量是触发状态,否则,当前线程有对互斥量的拥有权。
直到他释放,其他线程才可以拥有。
递归计数:
表示这个线程占用该互斥量的次数。
互斥量CMUTEX类的用法非常简单,步骤如下:
1.创建互斥量
HANDLECreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes,//安全属性
BOOLbInitialOwner,//如果为false则此互斥量是触发状态,否则,当前线程有对互斥量的拥有权。
LPCTSTRlpName//互斥量的名字
);
2.在线程函数中抢互斥量的拥有权,一旦拥有,则其他线程不可以再去抢夺
WaitForSingleObject(pthis->m_mutex,INFINITE);
直到此线程调用
ReleaseMutex(pthis->m_mutex);
其他线程可以继承,抢夺对互斥量的拥有权。
互斥对象与临界区对象很像.互斥对象与临界区对象的不同在于:
1.互斥对象可以在进程间使用,而临界区对象只能在同一进程的各线程间使用。
当然,互斥对象也可以用于同一进程的各个线程间,但是在这种情况下,使用临界区会更节省系统资源,更有效率。
2.关键段(临界区对象)是用户模式下的同步对象,所以效率会高一些。
互斥量是内核对象,内核对象的唯一缺点就是它们的性能。
调用线程必须从用户模式切换到内核模式。
这种切换是非常耗时的:
在X86平台上,一个空的系统调用大概会占用200个CPU周期—当然,这还不包括执行被调用函数在内核