C#内置事件机制在Unity3D应用
Unity3D是現在越來越流行的3D遊戲引擎,它支援JavaScript,c#和Boo語言。如果你是個Unity3D的愛好者,但只會JavaScript。這裏有一篇文章關於處理事件和消息傳遞,也許更適合你。A Useful Messaging System
你知道C#有一個內置的事件機制嗎?這個東東在Unity3D裏也非常好用。下面舉一個例子。
為了回應一個GameObject的事件分發,你通常要建立一個腳本繼承MonoBehaviour並且實現你需要的方法。比如你想對滑鼠懸停作出反應,就要創建OnMouseOver方法。通常代碼會像這個樣子: < xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" />
C#代碼
void OnMouseOver () {
renderer.material.color = Color.red;
}
這樣工作沒問題。但如果你想通知另外一個物件響應這個事件(OnMouseOver事件)怎麼辦?
第一種方式是保持另外物件的腳本引用,然後在你的OnMouseOver方法中調用它:
C#代碼public MyScript myScript;
void OnMouseOver () {
myScript.NotifyMouseOver();
}
這樣做沒問題,但是不夠好。因為你需要一直保持另外一個物件的引用,如果想通知多個物件要保持多個引用。代碼會變得很亂。
Messages 消息
另一個辦法是用SendMessage或SendMessageUpwards方法。看上去這是解決問題的最好辦法,但是這些方法存在嚴重的缺陷,以我的觀點,你應該儘量不去使用它們。
這些方法的語法並不靈活,你需要傳遞一個方法名字的字串,這樣做很容易出錯。另外這些方法只能用在同一個物件的附屬關係中。換句話說你只能在下面幾種情況中調用SendMessage或SendMessageUpwards方法,這些方法的腳本被關聯到同一個GameObject中,或者被關聯到這個GameObject的祖先關係物件中。
Events 事件
幸運的是有一個更好的解決辦法,這就是C#內置的事件機制。我不在這裏過多的描述機制是如何工作的,你如果有興趣可以學習相關的知識,訪問MSDN手冊。(C# 中的委託和事件)
現在讓我們看看如何在Unity3D中使用事件機制。
C#代碼
using UnityEngine;
public class EventDispatcher : MonoBehaviour {
public delegate void EventHandler(GameObject e);
public event EventHandler MouseOver;
void OnMouseOver () {
if (MouseOver != null)
MouseOver (this.gameObject);
}
}
如果你不知道這段代碼到底幹什麼,先不要著急。重要的是一旦你把這段代碼關聯到一個GameObject,只要在整個項目的任何一個腳本中保持這個物件,你就可以像下面這樣處理事件:
C#代碼
private GameObject s;
[...]
s.GetComponent<EventDispatcher>().MouseOver += Listener;
[...]
void Listener(GameObject g) {
// g is being hovered, do something...
}
這種方式比用消息更靈活,因為它可以被用在任何一個腳本中,而不僅僅在同一個物件附屬關係中。如果在整個應用中保持一個單例模式的物件,你就可以監聽任何從這個物件分發出來的事件。
另外一個重要特點,同一個監聽方法可以響應不同物件的事件。通過傳遞事件源對象的引用作為參數,你總會知道哪個對象分發了事件,就像我的代碼展示的那樣。(對於這句話可以這樣理解,假如遊戲中扔一顆導彈炸死了一個小兵並導致坦克減血,小兵死亡和坦克減血這兩個事件都觸發了同一個監聽方法-玩家得分,通過傳遞進來的事件源物件,就能知道小兵還是坦克觸發了玩家得分這個監聽方法。)
References, controllers and MVC
現在讓我們比較一下第一和第三種方式。在最開始的例子中(第一種方式保持另外物件的腳本引用),你需要在事件分發代碼中保持監聽者的物件引用,我說了這不是一個好主意。在用內置事件機制,改進的版本中(第三種方式),你需要在監聽者代碼中保持事件分發者的引用。你也許會問,為什麼後者更好?
首先,分發者不需要知道自己事件的監聽者是誰,不需要知道有多少監聽者。它只負責事件的發送。在最開始的例子中(第一種方式),如果要告訴分發者停止通知監聽者,你能想像這種程式判斷有多麼笨重嗎?
事件機制中,是由監聽者自己決定監聽什麼事件,什麼時候開始監聽,什麼時候停止監聽。像這樣的物件通常用於管理程式的狀態或者執行某些遊戲邏輯。這個就叫做控制器,借用MVC設計模式的概念。這樣我們的代碼會更清晰,不易出錯。(譯者認為觀察者設計模式更符合)
最後一點,我喜歡重載“+=”操作符去添加監聽方法。現在你也許能夠猜到,如果想結束監聽某個事件,可以這麼寫:
C#代碼
s.GetComponent<EventDispatcher>().MouseOver -= Listener;
當然你可以創建一個通用的EventDispatcher類,實現所有GameObject能夠分發的事件。可以參看下面的代碼。另外在實現OnGUI事件時要特別小心,如果想知道為什麼,讀讀這篇文章。
C#代碼
using UnityEngine;
using System.Collections;
public class EventDispatcher : MonoBehaviour
{
public delegate void EventHandler (GameObject e);
public delegate void CollisionHandler (GameObject e, Collision c);
public event EventHandler MouseOver;
void OnMouseOver ()
{
if (MouseOver != null)
MouseOver (this.gameObject);
}
public event EventHandler MouseDown;
void OnMouseDown ()
{
if (MouseDown != null)
MouseDown (this.gameObject);
}
public event EventHandler MouseEnter;
void OnMouseEnter ()
{
if (MouseEnter != null)
MouseEnter (this.gameObject);
}
public event EventHandler MouseExit;
void OnMouseExit ()
{
if (MouseExit != null)
MouseExit (this.gameObject);
}
public event EventHandler BecameVisible;
void OnBecameVisible ()
{
if (BecameVisible != null)
BecameVisible (this.gameObject);
}
public event EventHandler BecameInvisible;
void OnBecameInvisible ()
{
if (BecameInvisible != null)
BecameInvisible (this.gameObject);
}
public event CollisionHandler CollisionEnter;
void OnCollisionEnter (Collision c)
{
if (CollisionEnter != null)
CollisionEnter (this.gameObject, c);
}
public event CollisionHandler CollisionExit;
void OnCollisionExit (Collision c)
{
if (CollisionExit != null)
CollisionExit (this.gameObject, c);
}
}
保存工程的信息:比如游戏进程中的位置信息,对抗双方的个人信息等:
方法1:使用xml文件:
xml文件要以UTF-8的格式存储;
但是这样做会使得programmer 可以从脚本中控制xml文件中的所有的字符,包括xml文件中的语法命令字符,因此会带来不安全隐患;
附上两段代码:
A 这一段是我自己写的,将一个xml文件按照字符串读入;
虽然unity3d中的string类型说明中讲到保存的是unicode characters,但是实际上当xml文件比较大的时候,如果保存成unicode,就读不出来,如果保存成UTF-8就不存在这个问题;
using UnityEngine;
using System.Collections;
public class ReadXML: MonoBehaviour {
//store the read in file
WWW statusFile;
//decide wether the reading of xml has been finished
bool isReadIn = false;
// Use this for initialization
IEnumeratorStart () {//不能用void,否则没有办法使用yield
isReadIn = false;
yield return StartCoroutine(ReadIn());
isReadIn = true;
}
IEnumerator ReadIn()
{
yield return statusFile = new WWW("file:///D:/unity/rotationAndcolor/Assets/information/testxml.xml");//注意路径的写法
}
// Update is called once per frame
void Update () {
if(isReadIn)
{
string statusData = statusFile.data;
print(statusData.Length);
}
}
//get the parameters in the xml file
void getPatameters(string _xmlString)
{
//_xmlString.[0]
}
void postParameters()
{
}
}
B 这一段代码来自http://www.unifycommunity.com/wiki/index.php?title=Save_and_Load_from_XML
usingUnityEngine;
usingSystem.Collections;
usingSystem.Xml;
usingSystem.Xml.Serialization;
usingSystem.IO;
usingSystem.Text;
publicclass_GameSaveLoad:MonoBehaviour{
// An example where the encoding can be found is at
// http://www.eggheadcafe.com/articles/system.xml.xmlserialization.asp
// We will just use the KISS method and cheat a little and use
// the examples from the web page since they are fully described
// This is our local private members
Rect_Save, _Load, _SaveMSG, _LoadMSG;
bool_ShouldSave, _ShouldLoad,_SwitchSave,_SwitchLoad;
string_FileLocation,_FileName;
publicGameObject_Player;
UserData myData;
string_PlayerName;
string_data;
Vector3VPosition;
// When the EGO is instansiated the Start will trigger
// so we setup our initial values for our local members
voidStart(){
// We setup our rectangles for our messages
_Save=newRect(10,80,100,20);
_Load=newRect(10,100,100,20);
_SaveMSG=newRect(10,120,400,40);
_LoadMSG=newRect(10,140,400,40);
// Where we want to save and load to and from
_FileLocation=Application.dataPath;
_FileName="SaveData.xml";
// for now, lets just set the name to Joe Schmoe
_PlayerName ="Joe Schmoe";
// we need soemthing to store the information into
myData=newUserData();
}
voidUpdate(){}
voidOnGUI()
{
/
stringUTF8ByteArrayToString(byte[]characters)
{
UTF8Encoding encoding =newUTF8Encoding();
stringconstructedString = encoding.GetString(characters);
return(constructedString);
}
byte[]StringToUTF8ByteArray(stringpXmlString)
{
UTF8Encoding encoding =newUTF8Encoding();
byte[]byteArray = encoding.GetBytes(pXmlString);
returnbyteArray;
}
// Here we serialize our UserData object of myData
stringSerializeObject(objectpObject)
{
stringXmlizedString =null;
MemoryStream memoryStream =newMemoryStream();
XmlSerializer xs =newXmlSerializer(typeof(UserData));
XmlTextWriter xmlTextWriter =newXmlTextWriter(memoryStream, Encoding.UTF8);
xs.Serialize(xmlTextWriter, pObject);
memoryStream =(MemoryStream)xmlTextWriter.BaseStream;
XmlizedString = UTF8ByteArrayToString(memoryStream.ToArray());
returnXmlizedString;
}
// Here we deserialize it back into its original form
objectDeserializeObject(stringpXmlizedString)
{
XmlSerializer xs =newXmlSerializer(typeof(UserData));
MemoryStream memoryStream =newMemoryStream(StringToUTF8ByteArray(pXmlizedString));
XmlTextWriter xmlTextWriter =newXmlTextWriter(memoryStream, Encoding.UTF8);
returnxs.Deserialize(memoryStream);
}
// Finally our save and load methods for the file itself
voidCreateXML()
{
StreamWriter writer;
FileInfo t =newFileInfo(_FileLocation+"//"+ _FileName);
if(!t.Exists)
{
writer = t.CreateText();
}
else
{
t.Delete();
writer = t.CreateText();
}
writer.Write(_data);
writer.Close();
Debug.Log("File written.");
}
voidLoadXML()
{
StreamReader r = File.OpenText(_FileLocation+"//"+ _FileName);
string_info = r.ReadToEnd();
r.Close();
_data=_info;
Debug.Log("File Read");
}
}
// UserData is our custom class that holds our defined objects we want to store in XML format
publicclassUserData
{
// We have to define a default instance of the structure
publicDemoData _iUser;
// Default constructor doesn't really do anything at the moment
publicUserData(){}
// Anything we want to store in the XML file, we define it here
publicstructDemoData
{
publicfloatx;
publicfloaty;
publicfloatz;
publicstringname;
}
}
以下是javascript版本
importSystem;
importSystem.Collections;
importSystem.Xml;
importSystem.Xml.Serialization;
importSystem.IO;
importSystem.Text;
// Anything we want to store in the XML file, we define it here
classDemoData
{
varx : float;
vary : float;
varz : float;
varname: String;
}
// UserData is our custom class that holds our defined objects we want to store in XML format
classUserData
{
// We have to define a default instance of the structure
publicvar_iUser : DemoData =newDemoData();
// Default constructor doesn't really do anything at the moment
functionUserData(){}
}
//public class GameSaveLoad: MonoBehaviour {
// An example where the encoding can be found is at
// http://www.eggheadcafe.com/articles/system.xml.xmlserialization.asp
// We will just use the KISS method and cheat a little and use
// the examples from the web page since they are fully described
// This is our local private members
privatevar_Save : Rect;
privatevar_Load : Rect;
privatevar_SaveMSG : Rect;
privatevar_LoadMSG : Rect;
//var _ShouldSave : boolean;
//var _ShouldLoad : boolean;
//var _SwitchSave : boolean;
//var _SwitchLoad : boolean;
privatevar_FileLocation : String;
privatevar_FileName : String ="SaveData.xml";
//public GameObject _Player;
var_Player : GameObject;
var_PlayerName : String ="Joe Schmoe";
privatevarmyData : UserData;
privatevar_data : String;
privatevarVPosition : Vector3;
// When the EGO is instansiated the Start will trigger
// so we setup our initial values for our local members
//function Start () {
functionAwake(){
// We setup our rectangles for our messages
_Save=newRect(10,80,100,20);
_Load=newRect(10,100,100,20);
_SaveMSG=newRect(10,120,200,40);
_LoadMSG=newRect(10,140,200,40);
// Where we want to save and load to and from
_FileLocation=Application.dataPath;
// we need soemthing to store the information into
myData=newUserData();
}
functionUpdate(){}
functionOnGUI()
{
// ***************************************************
// Loading The Player...
// **************************************************
if(GUI.Button(_Load,"Load")){
GUI.Label(_LoadMSG,"Loading from: "+_FileLocation);
// Load our UserData into myData
LoadXML();
if(_data.ToString()!="")
{
// notice how I use a reference to type (UserData) here, you need this
// so that the returned object is converted into the correct type
//myData = (UserData)DeserializeObject(_data);
myData = DeserializeObject(_data);
// set the players position to the data we loaded
VPosition=newVector3(myData._iUser.x,myData._iUser.y,myData._iUser.z);
_Player.transform.position=VPosition;
// just a way to show that we loaded in ok
Debug.Log(myData._iUser.name);
}
}
// ***************************************************
// Saving The Player...
// **************************************************
if(GUI.Button(_Save,"Save")){
GUI.Label(_SaveMSG,"Saving to: "+_FileLocation);
//Debug.Log("SaveLoadXML: sanity check:"+ _Player.transform.position.x);
myData._iUser.x= _Player.transform.position.x;
myData._iUser.y= _Player.transform.position.y;
myData._iUser.z= _Player.transform.position.z;
myData._iUser.name= _PlayerName;
// Time to creat our XML!
_data = SerializeObject(myData);
// This is the final resulting XML from the serialization process
CreateXML();
Debug.Log(_data);
}
}
//string UTF8ByteArrayToString(byte[] characters)
functionUTF8ByteArrayToString(characters : byte[])
{
varencoding : UTF8Encoding =newUTF8Encoding();
varconstructedString : String = encoding.GetString(characters);
return(constructedString);
}
//byte[] StringToUTF8ByteArray(string pXmlString)
functionStringToUTF8ByteArray(pXmlString : String)
{
varencoding : UTF8Encoding =newUTF8Encoding();
varbyteArray : byte[]= encoding.GetBytes(pXmlString);
returnbyteArray;
}
// Here we serialize our UserData object of myData
//string SerializeObject(object pObject)
functionSerializeObject(pObject : Object)
{
varXmlizedString : String =null;
varmemoryStream : MemoryStream =newMemoryStream();
varxs : XmlSerializer =newXmlSerializer(typeof(UserData));
varxmlTextWriter : XmlTextWriter =newXmlTextWriter(memoryStream, Encoding.UTF8);
xs.Serialize(xmlTextWriter, pObject);
memoryStream = xmlTextWriter.BaseStream;// (MemoryStream)
XmlizedString = UTF8ByteArrayToString(memoryStream.ToArray());
returnXmlizedString;
}
// Here we deserialize it back into its original form
//object DeserializeObject(string pXmlizedString)
functionDeserializeObject(pXmlizedString : String)
{
varxs : XmlSerializer =newXmlSerializer(typeof(UserData));
varmemoryStream : MemoryStream =newMemoryStream(StringToUTF8ByteArray(pXmlizedString));
varxmlTextWriter : XmlTextWriter =newXmlTextWriter(memoryStream, Encoding.UTF8);
returnxs.Deserialize(memoryStream);
}
// Finally our save and load methods for the file itself
functionCreateXML()
{
varwriter : StreamWriter;
//FileInfo t = new FileInfo(_FileLocation+"//"+ _FileName);
vart : FileInfo =newFileInfo(_FileLocation+"/"+ _FileName);
if(!t.Exists)
{
writer = t.CreateText();
}
else
{
t.Delete();
writer = t.CreateText();
}
writer.Write(_data);
writer.Close();
Debug.Log("File written.");
}
functionLoadXML()
{
//StreamReader r = File.OpenText(_FileLocation+"//"+ _FileName);
varr : StreamReader = File.OpenText(_FileLocation+"/"+ _FileName);
var_info : String = r.ReadToEnd();
r.Close();
_data=_info;
Debug.Log("File Read");
}
//}
方法2:使用unity 3d 的ISerializable类
它的好处是,可以将文件存成自己定义的后缀形式,并且2进制化存储,在u3d的帮助文档中有相关介绍。
以下是必须的。!不然很多时候上传东西会报错。摘自官网
Unity offers an Asset Server add-on product for easy integrated versioning of your projects. If you for some reason are not able use the Unity Asset Server, it is possible to store your project in any other version control system, such as Subversion, Perforce or Bazaar, although this requires some manual initial setup of your project and moving and renaming of assets has to be performed using your version control client and not inside Unity.
External Version Control is a Unity Pro feature.
Before checking your project in, you have to tell Unity to modify the project structure slightly to make it compatible with storing assets in an external version control system. This is done by selecting Edit->Project Settings->Editor in the application menu and enabling External Version Control support by clicking theEnable button. This will create a text file for every asset in the Assets
directory containing the necessary bookkeeping information required by Unity. The files will have a .meta
file extension with the first part being the full file name of the asset it is associated with. When moving or renaming assets in the version control system, make sure you also move or rename the .meta
file accordingly.
When checking the project into a version control system, you should at least add the Assets
directory to the system. In case you want to track project and build settings as well you also need to add the Library folder and these files inside the folder:
EditorBuildSettings.asset
InputManager.asset
ProjectSettings.asset
QualitySettings.asset
TagManager.asset
TimeManager.asset
AudioManager.asset
DynamicsManager.asset
NetworkManager.asset
Do not add any other files or directories located inside the Library
directory. When creating new assets, make sure both the asset itself and the associated.meta
file is added to version control.
First, let's assume that we have a subversion repository at svn://my.svn.server.com/
and want to create a project atsvn://my.svn.server.com/MyUnityProject
. Then follow these steps to create the initial import in the system:
InitialUnityProject
. You can add any initial assets here or add them later on. Library
directory inside your project folder except.
EditorBuildSettings.asset
InputManager.asset
ProjectSettings.asset
QualitySettings.asset
TagManager.asset
TimeManager.asset
AudioManager.asset
DynamicsManager.asset
NetworkManager.asset
svn import -m"Initial project import" InitialUnityProject svn://my.svn.server.com/MyUnityProject
InitialUnityProject
directory if you wish. svn co svn://my.svn.server.com/MyUnityProject
Library
directory:svn propedit svn:ignore MyUnityProject/Library
EditorBuildSettings.asset
InputManager.asset
ProjectSettings.asset
QualitySettings.asset
TagManager.asset
TimeManager.asset
AudioManager.asset
DynamicsManager.asset
NetworkManager.asset
Library
directory in step 4 above.
svn ci -m"Finishing project import" MyUnityProject
PS:非常感谢Faye的这篇日志,让我更加清晰理解yield究竟是怎么?同样首先接触C++是木有这个玩意的概念的~!~
1、它yield的核心功能是让代码在此返回,下一循环(update)继续执行;
2、循环中的函数在yield处返回,下一帧继续
项目需求是一个应用在启动时需要读取几个配置文件,原始文档自然是excel格式,为了方便,我把它们转存为csv格式,即用逗号分割的文本格式,然后在js脚本中自己解析,倒也轻松,但是问题就是解析得比较慢。
一个100行80多列的文档解析需要几秒钟,这个有点难受了。
更可怕的是unity发布到ios上的程序启动有个限制,超过一定的时间没有加载完成会被系统强制终止。
控制台错误输入如下:
Aug 20 17:00:45 xxx-iPhone SpringBoard[30] <Warning>: xxx failed to launch in time
Aug 20 17:00:45 xxx-iPhone SpringBoard[30] <Warning>: Forcing crash report of xxx[1067]...
Aug 20 17:00:46 xxx-iPhone SpringBoard[30] <Warning>: Finished crash reporting.
Aug 20 17:00:46 xxx-iPhone com.apple.launchd[1] (UIKitApplication:com.xxx.xxx[0xd288][1067]) <Notice>: (UIKitApplication:com.xxx.xxx[0xd288]) Exited: Killed: 9
其中的xxx是代替了个人的一些信息,注意几个关键字:
failed to launch in time (没能及时启动成功)
Exited : Killed : 9 (以kill 9的方式退出, kill 9 是被强制终止)
为解决此问题搜了解决方法,查到有个yield可以做点事情,研究了一下,现将结果记录于此备忘。
yield的核心功能是让代码在此返回,下一循环(update)继续执行
看unity自己的例子:
function Start () {
StartCoroutine("DoSomething", 2.0);
yield WaitForSeconds(1);
StopCoroutine("DoSomething");
}
function DoSomething (someParameter : float) {
while (true) {
print("DoSomething Loop");
// Yield execution of this coroutine and return to the main loop until next frame
yield;
}
}
上面的一段先不管,先看下面一段,循环中的yield会让函数暂时返回,下一帧继续,这个在c++里面是没有的,一开始难以理解,以为类似于MFC里面的Dialog.DoModal(),于是就这么用了:
function Start(){
LoadA();
LoadB();
LoadC();
}
LoadA(), LoadB(), LoadC()分别是一个类似于上面DoSomething的函数,但是我的LoadB是依赖LoadA完成的,就是说如果LoadA没有完成时是不能开始LoadB的,这样用就出错了。
因为LoadA并没有想Dialog.DoModal()一样,而是直接返回了,就是说第一帧,LoadA,LoadB,LoadC都执行了,这非我预想。经过加入各种输出信息,终于明白过来,函数是立即返回的,但是里面的循环工作(yield后面的代码)会在后续帧里面得以继续执行。
为此,需要重新找到LoadA完成的切入点,我是这样做的:
function LoadA()
{
for( var i : int = 0; i < 99; ++i)
{
// do something ......
yield;
}
CallFunctionFinished();
}
然后在CallFunctionFinished()这个函数里面调用LoadB,当然也可以用SendMessage把调用做的低耦合一点。
还有个StartCoroutine,在js里面是可无的,unity的文档中明确说了:
When using JavaScript it is not necessary to use StartCoroutine, the compiler will do this for you. When writing C# code you must call StartCoroutine.
如果要做一个loading的进度条,就只需要在我们读取数据的循环中加入计数,然后在update函数中读取计数显示进度条即可。
学习source,看到了下面一段代码
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
首先这段代码的作用是使当前进程沉睡2S,展现给用户的结果就是画面维持两秒,有个“正在启动”的感觉而已。
其实,之前还有看到过sleep(0)的时候,这又是什么作用呢?
百度之,搜到一篇超有趣的讲解,贴过来,备忘。
我们可能经常会用到 Thread.Sleep 函数来使线程挂起一段时间。那么你有没有正确的理解这个函数的用法呢?思考下面这两个问题: 1、假设现在是 2008-4-7 12:00:00.000,如果我调用一下 Thread.Sleep(1000) ,在 2008-4-7 12:00:01.000 的时候,这个线程会 不会被唤醒? 2、某人的代码中用了一句看似莫明其妙的话:Thread.Sleep(0) 。既然是 Sleep 0 毫秒,那么他跟去掉这句代码相比,有啥区别么?我们先回顾一下操作系统原理。操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法,而Windows则属于抢占式的。在时间片算法中,所有的进程排成一个队列。操作系统按照他们的顺序,给每个进程分配一段时间,即该进程 允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,,当进程用完它的时间片后,它被移到队列的末尾。 所谓抢占式操作系统,就是说如果一个进程得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU 。因此可以看出,在抢占式操作系统中,操作系统假设所有的进程都是“人品很好”的,会主动退出 CPU 。在抢占式操作系统中,假设有若干进程,操作系统会根据他们的优先级、饥饿时间(已经多长时间没有使用过 CPU 了),给他们算出一个总的优先级来。操作系统就会把 CPU 交给总优先级最高的这个进程。当进程执行完毕或者自己主动挂起后,操作系统就会重新计算一次所有进程的总优先级,然后再挑一个优先级最高的把 CPU 控制权交给他。我们用分蛋糕的场景来描述这两种算法。假设有源源不断的蛋糕(源源不断的时间),一副刀叉(一个CPU),10个等待吃蛋糕的人(10 个进程)。 如果是 Unix 操作系统来负责分蛋糕,那么他会这样定规矩:每个人上来吃 1 分钟,时间到了换下一个。最后一个人吃完了就再从头开始。于是,不管这10个人是不是优先级不同、饥饿程度不同、饭量不同,每个人上来的时候都可以吃 1 分钟。当然,如果有人本来不太饿,或者饭量小,吃了30秒钟之后就吃饱了,那么他可以跟操作系统说:我已经吃饱了(挂起)。于是操作系统就会让下一个人接着来。 如果是 Windows 操作系统来负责分蛋糕的,那么场面就很有意思了。他会这样定规矩:我会根据你们的优先级、饥饿程度去给你们每个人计算一个优先级。优先级最高的那个人,可以上来吃蛋糕——吃到你不想吃为止。等这个人吃完了,我再重新根据优先级、饥饿程度来计算每个人的优先级,然后再分给优先级最高的那个人。这样看来,这个场面就有意思了——可能有些人是PPMM,因此具有高优先级,于是她就可以经常来吃蛋糕。可能另外一个人是个丑男,而去很ws,所以优先级特别低,于是好半天了才轮到他一次(因为随着时间的推移,他会越来越饥饿,因此算出来的总优先级就会越来越高,因此总有一天会轮到他的)。而且,如果一不小心让一个大胖子得到了刀叉,因为他饭量大,可能他会霸占着蛋糕连续吃很久很久,导致旁边的人在那里咽口水。。。而且,还可能会有这种情况出现:操作系统现在计算出来的结果,5号PPMM总优先级最高,而且高出别人一大截。因此就叫5号来吃蛋糕。5号吃了一小会儿,觉得没那么饿了,于是说“我不吃了”(挂起)。因此操作系统就会重新计算所有人的优先级。因为5号刚刚吃过,因此她的饥饿程度变小了,于是总优先级变小了;而其他人因为多等了一会儿,饥饿程度都变大了,所以总优先级也变大了。不过这时候仍然有可能5号的优先级比别的都高,只不过现在只比其他的高一点点 ——但她仍然是总优先级最高的啊。因此操作系统就会说:5号mm上来吃蛋糕……(5号mm心里郁闷,这不刚吃过嘛……人家要减肥……谁叫你长那么漂亮,获得了那么高的优先级)。 那么,Thread.Sleep 函数是干吗的呢?还用刚才的分蛋糕的场景来描述。上面的场景里面,5号MM在吃了一次蛋糕之后,觉得已经有8分饱了,她觉得在未来的半个小时之内都不想再来吃蛋糕了,那么她就会跟操作系统说:在未来的半个小时之内不要再叫我上来吃蛋糕了。这样,操作系统在随后的半个小时里面重新计算所有人总优先级的时候,就会忽略5号mm。Sleep函数就是干这事的,他告诉操作系统“在未来的多少毫秒内我不参与CPU竞争”。
看完了 Thread.Sleep 的作用,我们再来想想文章开头的两个问题。对于第一个问题,答案是:不一定。因为你只是告诉操作系统:在未来的1000毫秒内我不想再参与到 CPU竞争。那么1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束;况且,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。与此相似的,Thread有个Resume函数,是用来唤醒挂起的线程的。好像上面所说的一样,这个函数只是“告诉操作系统我从现在起开始参与CPU竞争了”,这个函数的调用并不能马上使得这个线程获得CPU控制权。对于第二个问题,答案是:有,而且区别很明显。假设我们刚才的分蛋糕场景里面,有另外一个PPMM 7号,她的优先级也非常非常高(因为非常非常漂亮),所以操作系统总是会叫道她来吃蛋糕。而且,7号也非常喜欢吃蛋糕,而且饭量也很大。不过,7号人品很好,她很善良,她没吃几口就会想:如果现在有别人比我更需要吃蛋糕,那么我就让给他。因此,她可以每吃几口就跟操作系统说:我们来重新计算一下所有人的总优先级吧。不过,操作系统不接受这个建议——因为操作系统不提供这个接口。于是7号mm就换了个说法:“在未来的0毫秒之内不要再叫我上来吃蛋糕了”。这个指令操作系统是接受的,于是此时操作系统就会重新计算大家的总优先级——注意这个时候是连7号一起计算的,因为“0毫秒已经过去了”嘛。因此如果没有比 7号更需要吃蛋糕的人出现,那么下一次7号还是会被叫上来吃蛋糕。因此,Thread.Sleep(0)的作用,就是“触发操作系统立刻重新进行一次CPU竞争”。竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。这也是我们在大循环里面经常会写一句Thread.Sleep(0) ,因为这样就给了其他线程比如Paint线程获得CPU控制权的权力,这样界面就不会假死在那里。末了说明一下,虽然上面提到说“除非它自己放弃使用 CPU ,否则将完全霸占 CPU”,但这个行为仍然是受到制约的——操作系统会监控你霸占CPU的情况,如果发现某个线程长时间霸占CPU,会强制使这个线程挂起,因此在实际上不会出现“一个线程一直霸占着 CPU 不放”的情况。至于我们的大循环造成程序假死,并不是因为这个线程一直在霸占着CPU。实际上在这段时间操作系统已经进行过多次CPU竞争了,只不过其他线程在获得CPU控制权之后很短时间内马上就退出了,于是就又轮到了这个线程继续执行循环,于是就又用了很久才被操作系统强制挂起。。。因此反应到界面上,看起来就好像这个线程一直在霸占着CPU一样。末了再说明一下,文中线程、进程有点混乱,其实在Windows原理层面,CPU竞争都是线程级的,本文中把这里的进程、线程看成同一个东西就好了
在MSDN中,指Blocks the calling thread until a thread terminates;它的意思是说,把主线程挂起去运行(调用、启用)一个线程当其结束为止。
就相当于我在main()主线程new了一个newThread线程并启用Thread.start();然后Thread.Join(); 只要newThread没有运行结束,主线程将不被继续运行.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace Test
{
class TestThread
{
private static void ThreadFuncOne()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(Thread.CurrentThread.Name +" i = " + i);
}
Console.WriteLine(Thread.CurrentThread.Name + " has finished");
}
static void Main(string[] args)
{
Thread.CurrentThread.Name = "MainThread";
Thread newThread = new Thread(new ThreadStart(TestThread.ThreadFuncOne));
newThread.Name = "NewThread";
for (int j = 0; j < 20; j++)
{
if (j == 10)
{
newThread.Start();
newThread.Join();
}
else
{
Console.WriteLine(Thread.CurrentThread.Name + " j = " + j);
}
}
Console.Read();
}
}
}