多任务操作系统进程和线程同步控制

来源:互联网 发布:数据库设计入门经典pdf 编辑:程序博客网 时间:2024/06/05 18:21

http://blog.163.com/tianle_han/blog/static/661782620086125122542/



在多任务操作系统环境下,进程和线程的同步控制是多线程和多进程编程的一个重点,稍作总结

 

一、临界区(criticalsection)

 

1、临界区是线程同步的一种方式,即它在同一时刻只允许一个线程进入,其他线程只能等往此临界区被释放后才能进入,否则只能等待,线程也将挂起。需要注意的是临界区在同一线程中可以重复连续的多次进入,它并不像互斥型信号量一样只能进入一次。但进入多少次在线程不需要使用临界区的时候,便需要释放多少次,即enter和leave的个数要相等。否则的话,会阻挠其他线程的进入。如果一个线进入临界区而没有leave之前就down掉了,那个其他要进入临界区的线程只能死等了,而且因为临界区不是内核对象,所以系统没有办法清除临界区。而且不会知道进入临界区的线程是生是死,这也是临界区的一个缺陷。在编程时解决此缺陷的一个方法是封装一个类,以临界区引用作为参数,在构造时进入临界区,在析构时离开临界区。这样每次在使用临界资源时,只需要构造此类的临时对象,而不用再想着在用完临界区时再离开,因为临时变量在销毁时会调用析构函数,从而离开临界区。

 

2、使用临界区也可能会引起死锁,如果一段代码需要使用两个以上的临界资源,而这段代码又在不同的线程中出现,那么就会出现引起死锁的可能,因为两个线程在进入资源时可能顺序不一样,从而出现互等现象。虽然有一些方法可以侦测死锁,但一般都过于复杂,所以最好就是在编程时找到一种方法确保死锁不会发生。

 

就像倔强的哲学家一样,他们的进餐不会谦让,所以他们需要付出代价,那就是死锁产生时,他们只能获得左手旁的一支筷子,而且又都不愿意松手,最终结果是他们都不能进餐。

 

我们解决问题就要找到根源,哲学家进餐问题的产生的根源有三个,一是他们同时需要两支筷子,也就是两个临界资源才能进餐。二是他们都很“倔强”,也就是有比较高的自由意识,从来不愿意把左手的筷子让给别人。三是有多个人想要同时进餐。所以我们针对这三个特点可以采取三种方式来解决问题。一就是让他们一次只需要一支筷子就能进餐,这种方式不可取,因为一支筷子没法进餐,放到编程中也就是资源少了一个无法解决问题。二是限制他们的自由意识,让他们愿意放弃自己已经到手的筷子。这时候就需要提前检查能不能同时获取两支筷子,如果不能那就一个都不要获取,即只能两个资源都可以同时得到时才去获取资源。这种方法是可取的,但在实际编程中,由于临界区不是内核对象,所以不能提前检查状态。即不能使用waitformultiobject。要采用这种方法就要更改同步控制的资源,不能使用临界区。第三种方法是减少使用人数,让他们在同一时刻只允许有一人进餐。这种方法也是限制他们的自由意识,对编程来说不可取,因为大家(线程)可能要同时使用。

 

虽然临界区有很多缺点,但在实际编程中仍然运用很广泛,这是因为死锁产生是需要条件的,在死锁条件不满足的情况下是不会产生死锁的。比如在解决问题时不需要多于一个的临界区,也就是哲学家只需要一支筷子就能进餐,这时候我们就可以使用临界区。还有我们在编程时也可以刻意控制在同一时刻只允许一个哲学家(线程)进餐。只要破坏产生死锁的三个条件就可以使用临界区了。

 

3、因为临界区不是核心对象,所以它只在同一进程的不同线程中起作用,即工作在用户态,速度也非常快。

 

二、互斥器(mutex)

 

1、互斥器是mutual exclusion的缩写,它是操作系统核心对象,可以工作在进程间或线程间或进程和线程间,但因为要锁定时要在核心态和用户间切换,所以效率比较低。

 

2、使用createmutex来创建互斥器,如果在同一进程中,可以创建没有名称的互斥器,使用句柄来进行操作,如果在不同的进程中使用,可以创建命名互斥器,如果创建的互斥器在操作系统中已经存在同名的互斥器,会返回此互斥器的句柄,这时候和使用openmutex打开此互斥器的效果相同(注意此句柄和创建时候的句柄不同,因为操作系统会给互斥器引用计数,每次使用都会增加引用计数,使用完毕后,使用closehandle关闭句柄,引用计数减1,引用计数为0时会撤消此互斥器),这时候如果使用GetLastError会返回已经存在的信息。

 

3、使用wait函数来锁定互斥器,使用release函数来解锁计数器。

 

4、线程或进程如果在结束之前使用的mutex没有释放,那么此mutex就成了被舍弃的互斥器,而一个等待的线程会返回WAIT_ABONDED_0,不管线程是使用ExitThread结束还是因为down掉而结束,这种情况都可能是存在的,虽然我们可以从wait的通知中获取到这种情况,但对此互斥器所保护的资源的损害,我们可能是无法修复的。所以应该尽量避免此现象的发生。

 

5、使用createmutex时可以指定创建的线程拥有它,这样便可以不再使用wait函数来拥有它,如果不是这样,就需要使用wait函数来获取,但有可能会在wait之前发生线程切换,这时候有可能其它的线程会在此线程之前使用wait来获取此互斥器。

 

三、信号量

 

1、 信号量是解决像生产者消费者这样模型的问题,它认为所有的资源都一样,信号量代表资源的数量,它可以拥有一个最大值,当数量超过最大值的时候,就像仓库满了一样,生产者线程就只有等待。而如果信号量为0时,消费者线程也只有等待。

 

2、使用CreateSemaphore创建信号量,在创建时可以指定信号量的最大值,如果创建的信号量已经存在,和CreateMutex一样,创建仍然会成功,但使用GetLastError会返回已经存在的信息。使用wait函数锁定信号量,每次锁定,信号量的值减1,直到0,锁定便会等待,使用ReleaseSemaphore来解锁,每次解锁信号量的值会增加一定的量,通常是1,直到最大值,当达到最大值时解锁便会不再起作用。

 

3、信号通常用来保护缓冲区,有写入的线程,也有读取的线程

 

四、事件机制

 

1、事件机制是高级IO操作的一种模型,采用事件通知的方式,比如socket可以和事件绑定等,等检测到事件时再去处理详细的事件,Event的状态完全可以由用户控制,是最具弹性的同步机制,只有两种状态,激发和未激发状态。它的状态不像mutex和samphore那样会因为wait函数的使用而改变,而是完全由用户控制状态。用户可以精确的告诉事件在什么时候做什么事

 

2、使用CreateEvent创建事件,在创建时可以指定是否自动设置非激发状态,第二个参数bManualReset为true时表示需要调用ResetEvent手工设置为非激发状态,如果为false,就会自动重置,即在激发状态(wait函数返回)之后,自动重置为非激发状态,其他wait函数需要等待。使用SetEvent设置事件为激发状态,因wait而等待的线程为被唤醒。使用PulseEvent改变事件的状态时,如果是需要手工设置的事件,则使事件处于激发状态,唤醒所有等待的线程,然后变为非激发状态。如果是自动设置的事件,则会把事件设置为激发状态,唤醒一个等待的线程,然后恢复为非激发状态。