操作系统.docx

上传人:b****5 文档编号:5096800 上传时间:2022-12-13 格式:DOCX 页数:22 大小:128.99KB
下载 相关 举报
操作系统.docx_第1页
第1页 / 共22页
操作系统.docx_第2页
第2页 / 共22页
操作系统.docx_第3页
第3页 / 共22页
操作系统.docx_第4页
第4页 / 共22页
操作系统.docx_第5页
第5页 / 共22页
点击查看更多>>
下载资源
资源描述

操作系统.docx

《操作系统.docx》由会员分享,可在线阅读,更多相关《操作系统.docx(22页珍藏版)》请在冰豆网上搜索。

操作系统.docx

操作系统

线程—第一课时

计算机系统由硬件和软件两部分组成,操作系统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周期—当然,这还不包括执行被调用函数在内核

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 军事

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1