[Unity]unity常用框架方法

前言

在unity中,继承自MonoBehaviour的方法会在运行中被unity的框架自动调用一些方法,在我们实际使用的时候,这些方法能方便我们做一些资源初始化,或者一些状态的变化修改逻辑。在此我就简单说说一些基础的方法。

方法介绍

下面介绍一下经常用到的方法的名字
- Awake()
- OnEnable()
- Start()
- FixedUpdate()
- Update()
- LateUpdate()
- OnGUI()
- OnDisable()
- OnDestory()
- OnEnable()
我们在unity里新建一个脚本的时候Unity会自动给我们生成一个继承自MonoBehavior的一个c#脚本,里面会自动生成Awake(),Start(),Update()这三个方法,这三个方法也是我们经常要用到的。

方法调用

上述方法是在脚本挂在gameobject的时候由Unity自动调用的,不管方法被设置为private还是public,都能被unity所执行到,所以这些方法名可以被当做为unity的保留字段,当脚本挂载的gameobject为激死状态的时候,上述方法将不会被执行,只有当物体被激活了才会被执行(当然也可以由自己手动调用);这里需要标注一件事,就是当物体是激死状态下被挂载脚本,这个脚本是不会被触发的,但是当你脚本挂载上去后再去激死激活物体是能执行特定脚本的;所以挂载需要分两个解释,一个是挂上去了,一个是被系统加载到。

方法顺序

上述方法的调用时有一个先后顺序的,所以我们在使用的时候,做一些逻辑判断的时候就可以使用到这样的一个优先顺序的。这个顺序为激活顺序,关闭销毁顺序。

激活顺序

Awake>OnEnable>Start>FixedUpdate>Update>LateUpdate>OnGUI

销毁顺序

注意这里是销毁物体的函数调用
OnDisable>OnDestroy

调用次数

只调用一次

Awake(),Start(),OnDestory()这三个方法为只调用一次。

多次调用

OnEnable(),OnDisable(),FixedUpdate(),Update(),LateUpdate(),OnGUI这些方法为多次调用,但是其中的调用时机也是有所区别的,有的是循环调用:Update,FixedUpdate,LateUpdate这三个方法为循环调用,只要脚本属于在执行状态就会被循环调用。OnEnable为脚本切换为执行状态就会触发,OnDisable为脚本被切换到不可执行状态下触发(可执行和不可执行分为父节点被关闭/打开,自己被关闭/打开,脚本被关闭/打开)。OnGUI这个方法比较特殊只要触碰到整个Unity的界面都会被触发。

方法说明

Awake

Awake这个方法属于脚本挂载到gameobject上的时候只激活一次的方法,为最优先的方法,可用来做信息初始化。当脚本是被关闭状态初始化出来一个激活的gamobject的时候,awake是会被执行的。

OnEnable

这个方法属于脚本被激活和gameobject都被激活的时候就会调用到的一个方法。

Start

这个方法是属于脚本只调用一次的方法,属于脚本和gameobject都被激活的时候才调用的一个方法。

FixedUpdate

是用于独立的帧频运行的一个方法一般用于物理信息计算,属于在固定时间会调用到这个方法的。可以在系统中设置这个调用的时长。Time.fixedDeltaTime 为两个FixedUpdate之间的运行时间

Update

这个方法属于在物体和脚本都激活的状态下,每帧都会执行的一个方法。两个Update方法之间的执行时间可查询Time.deltaTime.

LateUpdate

这个也是在脚本和物体都激活的状态下每帧都会执行的一个方法,在Update后执行,可以做一些跟随一类的事情。两个LateUpdate方法之间的执行时间可查询Time.deltaTime.

OnGUI

这个方法也是必须在物体和脚本都激活的状态下执行的,执行次数可能达到每帧执行多次的。具体情况需要具体探究

OnDisable

这个方法是物体或者脚本被激死的时候都会调用的一个方法。脚本被从物体上删除也会调用到的一个方法或者物体被删除也会调用

OnDestory

这个是物体被删除或者脚本被从物体上删除会调用的一个方法,但是必须得脚本是在执行状态下才会有反应。

补充

在说上述一些方法的时候,说到一个这样的概念,就是有些方法在脚本激死的状态下是不会被激活的,在看官方文档的时候发现脚本在没有Start(),Update(),FixedUpdate()
,LateUpdate(),OnGUI(),OnDisable(),OnEnable(),这几个方法的时候脚本的前面是没有一个复选框的,所以这个时候也是没有脚本激死激活的状态。

[c#]关于c#中委托,匿名函数,事件的浅要解析

委托

委托是一种引用类型,标识对具有特定参数列表和返回类型的方法的引用。在实例化委托时,可以将其实例与任何具有兼容签名和返回类型的方法相关联。通过委托实例调用方法。
委托用于将方法作为参数传递给其它方法。可将任何可访问类或结构中与委托类型匹配的任何方法分配给委托。该方法可以是静态方法,也可以是实例方法。
将方法作为参数进行引用的能力使委托成为定义回调方法的理想选择。
委托有以下属性:

  • 委托类似于c++的函数指针,但委托完全面向对象,不像c++指针会记住函数,委托会同时封装对象实例和方法。
  • 委托允许将方法作为参数进行传递。
  • 委托可用于定义回调方法。
  • 委托可以链接在一起
  • 方法不必与委托类型完全匹配
  • 可使用匿名方法或lambda表达式内联代码块
  • 可以使用?.Invoke()来调用委托方法
    匿名函数和函数实例的问题
    在研究逆变和协变的时候发现一段官方代码

    public delegate R SampleGenericDelegate<A, R>(A a);
    public static Second AFristRSecond(First first)
    {
    return new Second();
    }
    SampleGenericDelegate<Second, First> fse = AFirstRSecond;

    这段代码里,返回值是定义委托的子类,参数是定义委托的父类,所以这个逆变,又是协变,但是我用匿名函数写的时候却没办法成功
    SampleGenericDelegate<Second, First> fse3 = delegate (First a)
    {
    return new Second();
    };
    没有搞明白这个原因,只能归结于是匿名函数和函数实例的不一致。

    .NET包含的委托类型

  • Func<> 通常用于现有转换的请款,也就是说需要将委托参数转换为其他结果时。必须且只有一个返回值的时候用这个,返回值有协变,如果传入参数的话是带有逆变。
  • Action<> 用于需要使用委托参数执行操作的情况。简而言之,无返回值的用这个,如果有传入参数的定义,这个是有逆变out关键词的。
  • Predicate<> 用于需要确定参数是否满足委托条件的情况,必须且只有一个bool值的返回值,必须且只有一个传入参数,传入参数带有逆变。

    委托中的变体

    
    public class First { }
    public class Second : First { }
    public delegate First SampleDelegate(Second a);
    public delegate Second SampleDelegateSecond(Second a);
    public First ASecondRFirst(Second first)
    { return new First(); }
    public Second AsecondRsecond(Second second)
    {return new Second();}
    public First AFirstRFirst(First first)
    {return new First();}
    public Second AFirstRSecond(First first)
    {return new Second();}
- 协变
定义:将返回派生程度较大的派生类型的方法分配给委托(就是把子类返回出来)
在上述代码中体现为:SampleDelegate sa = AsecondRsecond 这个就表现为协变
- 逆变
定义:方法所接受参数的派生类型所具有的派生程度小于委托类型指定的程度(就是传参传父类)
在上述代码中表现为:SampleDelegate sa = AFirstRFirst 这个就表现为逆变

SampleDelegate sa = AFirstRSecond 这个应该是既满足协变,又满足逆变。
- 泛型委托
public delegate R SampleGenericDelegate(A a);
SampleGenericDelegatedGeneric = ASecondRFirst
上述代码为正常的泛型委托
dGeneric = AFirstRSecond;
上述即是协变也是逆变的隐式转换。
- 泛型类型参数中的变体
泛型委托之间的隐式转换,要求类型继承自对方,才可以将泛型委托分配给对方。
泛型类型参数的变体仅支持引用类型
合并委托的时候要求类型完全相同,所以变体委托不应该合并。
泛型委托的协变需要使用关键词out,协变类型只能用作方法返回类型,不能用作方法参数类型;逆变使用关键词in,逆变类型只能用作方法参数类型,不能用作方法返回类型。
可以在同一个委托中支持变体和协变,单这只适用于不同类型的参数。
在做协变和逆变的时候,写不写关键词,协变是可以支持函数实例和匿名函数的委托,逆变可以支持函数实例的的委托,匿名函数的委托就不能支持;这块的文档官方只说了可能会发现可以使用,所以就当一种奇巧淫技使用了。
当不是用关键字来进行逆变和协变的时候,就不能将一个委托分配给另外一个委托了。
```csharp
public delegate R SampleGenericDelegate<out R>();
SampleGenericDelegate1<String> dobject = () => "";
SampleGenericDelegate1<Object> dobject1 = dobject;
//错误
public delegate R SampleGenericDelegate<R>();
SampleGenericDelegate1<String> dobject = () => "";
SampleGenericDelegate1<Object> dobject1 = dobject;</code></pre>
<p>上述代码就体现了这个问题。</p>
<ul>
<li>合并委托(多播委托)
可以通过使用+运算符将多个对象分配到一个委托实例上。多播委托包含已分配委托列表。此多播委托被调用时会依次调用列表中的委托。仅可合并类型相同的委托。对象必须是已经初始化过。当使用匿名函数合并委托的时候,取消是很麻烦的。
<pre><code class="language-csharp">delegate void CustomDel(string s);
void Hello(string s){Console.WriteLine("Hello" + s);}
//错误1
CustomDel c += Hello;
//错误2
CustomDel c = Hello;
CustomDel d;
c += d;</code></pre>
<p>这两个委托都是没有初始化的。所以在合并的时候就不对。</p>
<pre><code class="language-csharp">CustomDel c = Hello;
c += (string s)=> { Console.WriteLine("niming" + s); };
c -= Hello;
c("aaaaa");</code></pre>
<p>上述代码你如果想在多播委托中删除掉匿名函数的委托,你只能再定义一个委托实例,把匿名函数赋值给给委托实例,然后再添加到多播委托上。</p>
<h3>闭包</h3>
<p>委托还能增加一种闭包的概念,这个概念在前面有分析过。</p>
<h2>匿名函数</h2>
<p>在说委托的时候,已经用到了匿名函数,现在具体说下匿名函数的内容
匿名函数是一个“内联”语句或表达式,可在需要委托类型的任何地方使用。可以使用匿名函数来初始化命名委托,或传递命名委托作为方法参数。
可以使用lambda表达式或匿名方法来创建匿名函数。某些类型的lambda表达式可以转换为表达式树类型</p>
<h3>匿名方法</h3>
<p>delegate 运算符创建一个可以转换为委托类型的匿名方法。</p>
<h3>lambda表达式</h3>
<p>只是使用委托的更方便的语法,将声明签名和方法正文,但是在分配到委托之前没有自己的正式标识。</p></li>
<li>表达式lambda,表达式为其主体:
<pre><code class="language-csharp">(input-parameters)=>expression</code></pre>
<p>使用=>从其主体分离lambda参数列表。在左侧指定输入参数,在另一侧输入表达式或语句块。
lambda广泛用于表达式树的构造。
只有当lambda只有一个输入参数的时候,小括号才是可选的;否则括号是必须的。
输入参数类型必须全部为显式或者全部为隐式;否者会编译出错。
表达式lambda的主体可以包含方法调用。不过若要创建在.NET公共语言运行时的上下文之外计算的表达式树,不得在lambda表达式中使用方法调用。在.NET公共语言运行时上下文之外,方法将没有任何意义。</p></li>
<li>语句lambda,语句块作为其主体:
<pre><code class="language-csharp">(inpput-parameters) => {<sequence-of-statements>}</code></pre>
<p>语句lambda与表达式lambda表达式类似,只是语句括在大括号中。
语句lambda的主体可以包含任意数量的语句;但是实际上通常不会多于两个或者三个。
语句 lambda 也不能用于创建表达式目录树。</p>
<h3>异步lambda</h3>
<p>使用关键词async和await。</p>
<h3>lambda 表达式和元组</h3>
<p>c# 7.0起,c#语言提供了对元组的内置支持。可以提供一个元组作为Lambda表达式的参数,同时Lambda表达式也可以返回元组。可通过用括号括住用逗号分隔的组件列表来定义元组。</p>
<pre><code class="language-csharp">Func<Tuple<int, int, int>, Tuple<int, int, int> > doubleThem = ns => (new Tuple<int, int, int> (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3));
var numbers = new Tuple<int, int, int>(2, 3, 4);
var doublednumbers = doubleThem(numbers);</code></pre>
<p>元组类型关键词是Tuple,通常是item1,item2这样的字段命名。但是也可以使用命名组件定义元组。</p>
<pre><code class="language-csharp">Func<(int n1, int n2, int n3), (int, int, int)> doublethem = ns => (2 * ns.n1, 2 * ns.n2, 3 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doublethem(numbers);</code></pre>
<p>上述是使用命名组件定义元组的示例代码。</p>
<h3>捕获lambda表达式中的外部变量和变量范围</h3>
<p>lambda可以引用外部变量。这些变量在定义lambda表达式的方法中或包含lambda表达式的类型中的范围内变量。以这种方式捕获的变量将进行存储以备在lambda表达式中使用,即使在其他情况下,这些变量将超出范围并进行垃圾回收。必须明确地分配外部变量,然后才能将lambda表达式中使用该变量。</p></li>
<li>捕获的变量将不会被作为垃圾回收,直至引用变量的委托符合垃圾回收的条件。</li>
<li>在封闭方法中看不到lambda表达式内引入的变量。</li>
<li>lambda表达式无法从封闭方法中直接捕获in,ref或out参数。</li>
<li>lambda表达式中的return语句不会导致封闭方法返回。</li>
<li>如果相应跳转语句的目标位于lambda表达式块之外,lambda表达式不得包含goto,break或continue语句。同样,如果目标在块内部,在lambda表达式块外部使用跳转语句也是错误的。
<h2>事件</h2>
<p>要定义一个事件,可以在事件类的签名中使用event关键字,并制定事件的委托类型。
还能显式创建包含添加或删除处理程序的属性,创建方法和属性的语法类似,只是替换关键词为add,remove。</p>
<h3>使用委托添加事件</h3>
<pre><code class="language-csharp">
delegate void CustomDel(string s);
event CustomDel cusevnet;
void funcust(string s){</code></pre></li>
</ul>
<p>}
cusevnet += funcust;</p>
<pre><code>### .NET支持事件的委托
```csharp
private EventHandler<T> directoryChanged;
event EventHandler<T> DirectoryChanged
{
    add{ directoryChanged += value;}
    remove{directoryChanged -= value;}
}

EventHandler

是一个预定义的委托,当不生成数据的时候用EventHandler,但是当有数据产生的时候用泛型EventHandler&lt TEventArgs &gtTEventArgs是继承EventArgs的类型。

事件委托的签名

void OnEventRaised(object sender,EventArgs args)

上述代码为.NET事件委托的标准签名。返回类型为void,参数列表包含两个参数:发件人和事件参数。sender的编译类型为system.object,第二个参数通常派生自system.EventArgs的类型,即使事件类型不需要任何参数,你仍要提供者两个参数。应使用特殊值EventArgs.Empty来表示事件不包含任何附加信息。在.NET Core版本中对TEventArgs的定义不再要求必须派生自System.EventArgs的类。

两种事件对比

EventHandler事件是不带返回值的,传入参数只能为固定的两个,而委托事件是可以自定义参数和返回值的。

[编程基础]闭包

前言

在一个机缘巧合之下,了解到了闭包概念,当时对里面的实现感觉没有头绪,所以就暂时放过不去研究了。前段时间看到一本《代码的未来》里面系统性的介绍了一下闭包概念,感觉自己找到点头绪,然后就开始仔细研究一波。

闭包所需要的知识点

  • 函数对象
  • 作用域
  • 生存周期

函数对象

所谓的函数对象:就是作为对象来使用的函数,在c#中为委托(delegate),这里的对象不是面向对象中的那个对象,而是编程语言中的数据对象。

delegate void dhello();
void hello (){
    Console.WriteLine("Hello");
}
dhello d = hello;
d();

上述代码为简单的定义一个委托,然后给委托对象赋值一个函数,然后执行这个对象。在delegate的定义中签名必须和被委托的方法的签名一致,形参也必须得一致。就可以直接调用该方法,或者把它当成参数传递给另一个方法调用。在.NET Framework 2.0引入了匿名委托

delegate void dhello();
dhello d = delegate{
    Console.WriteLine("Hellp");
}
d();

在这里委托正文就是一直表达式,可以不用定义方法名。这样叫做创建的“内联”委托,无需指定任何其他类型或者方法,只需在所需位置内联委托定义就好。
在c# 3.0版本中,我们对上述代码还能在简易点,我们引入lambda表达式来实现上述功能

delegate void dhello();
dhello d = ()=>{
    Console.WriteLine("Hello");
}
d();

这个只是使用委托的更方便的语法。它们将声明签名和方法正文,但在分配到委托之前没有自己的正式标识。与委托不同,可将其作为事件注册的左侧内容或在各种LINQ子句和方法中直接分配。

高阶函数

函数对象最大的用途就是高阶函数。所谓的高阶函数,就是用函数作为参数的函数。

delegate void hello();
delegate void hello1(hello h);
void hello2 (hello h){
h()
}
hello1 h1 = hello2;
hl(()=>{Console.WriteLine("Hello");});

通过将一部分处理以函数对象的形式转移到外部,从而实现了算法的通用性。

函数指针的局限

函数指针的方法体是不能访问外部的局部变量的,如果把这个变量变成全局的是可以的,但是只是为了能访问到而变成一个全局变量是不划算的。

作用域

作用域是指变量的有效范围,也就是某个变量可以被访问的范围。作用域是嵌套的,因此位于内侧的代码块可以访问以其自身为作用域的变量,以及以外侧代码块为作用域的变量。

delegate void hello2(int n);
delegate void hello3(List<int> l, hello2 h2);
void foreached(List<int> list1, hello2 h2)
{
   int i = 0;
   while (i < list1.Count)
   {
      h2(list1[i]);
      i++;
    }
}
 List<int> a1 = new List<int>() { 1, 2, 3, 4, 5 };
 int i = 0;
 hello3 h3 = foreached;
 h3(a1, (int c) => { Console.WriteLine("index" + i + "=" + c);i++; });

在函数对象中能对外部变量i进行访问(引用,更新),这就是闭包的构成要件之一。
按照作用域的思路,可能大家觉得上述闭包性质也是理所当然的。但是我们如果加入另外一个概念-生存周期,结果可能就会出乎意料了。

生存周期

所谓的生存周期,就是变量的寿命。相对于表示程序中变量可见范围的作用域来说,生存周期这个概念指的是一个变量可以在多长的周期范围内存在并被能够被访问。

delegate void hello();
 hello extent()
            {
                int n = 0;
                return () => { n++; Console.WriteLine("n=" + n); };
            }
 hello h = extent();
                h();
                h();
输出:n = 1
      n = 2

这个函数变量在每次执行的时候,局部变量n都会被更新,从而输出逐次累加的的结果。按道理n是在extent函数中声明的,函数都已经执行完毕了。变量脱离了作用域之后不应该就消失吗?从这个结果来看,这个变量在函数执行完毕以后没有消失,还在某一个地方继续存活下来的。
这就是生命周期。也就是说,这个从属于外部作用域的局部变量,被函数对象给“封闭”在里面了。
闭包这个词原本就是封闭的意思。被封闭起来的变量的寿命,与封闭它的函数对象寿命相等。也就是说,当封闭这个变量的函数对象不再被访问,被垃圾回收器回收的以后,这个变量的寿命也就同时终结了。
在函数对象中,将局部变量这一环境封闭起来的结构被称为闭包。

[编程基础]c#托管资源和非托管资源

前言

GC(垃圾回收)实现的时候,对引用资源管理都是对托管资源的管理,而在c#中还有一些非托管资源,现在简单讲下,两者的区别,和对非托管资源是怎么释放内存的。

托管资源

.NET中的类型都是从System.Object类型派生出来的。
分为两大类:
引用类型(reference type),分配在内存堆上。
值类型(value type),分配在堆栈上。

分配内存

值类型在栈里,先进后出,值类型变量的生命有先有后,这个确保了值类型变量在退出作用域以前会释放资源。比引用类型更简单和高效。堆栈是从高地址往低地址分配内存。
引用类型分配在托管堆上,声明一个变量在栈上保存,当实例化一个对象的时候,会把对象的地址存储在这个变量里。托管堆相反,从低地址往高地址分配内存。
初始化新进程的时候,CLR会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。一开始该指针指向托管堆的基址。托管堆上包含了所有的引用类型。从托管堆中分配内存要比非托管内存分配速度快。由于CLR通过为指针添加值来为对象分配内存,所以这几乎和从堆栈中分配内存一样快。另外由于连续分配的新对象在托管堆中是连续存储,所以应用程序可以快速访问这些对象。

释放内存

这个就是GC所做的事情了,参考c#GC。

非托管资源

应用使用完非托管资源后,必须要显式释放这些资源。最常见的非托管资源类型是包装操作系统资源的对象。虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但是无法了解如何发布并清理这些非托管资源。所以清理需要做以下操作:

  • 实现清理模式:需要提供IDisposable.Dispose实现以启用非托管资源的确定性释放。当不再需要此对象时,类型使用者可以调用Dispose。Dispose方法立即释放非托管资源。

  • 在类型使用者忘记调用Dispose的情况下,准备释放非托管资源。有两种方法可实现此目的:

    • 使用安全句柄包装非托管资源。安全句柄派生自System.Runtime.InteropServices.SafeHandle类并包含可靠的Finalize方法。在使用安全句柄时,只需要实现IDisposable接口并在Dispose实现中调用安全句柄的IDisposable.Dispose方法。如果未调用安全句柄Dispose方法,则垃圾回收器将自动调用安全句柄的终结器。
    • 重写Object.Finalize方法。当类型使用者无法调用IDisposable.Dispose以确定性地释放非托管资源时,终止会启动对非托管资源的非确定性释放。

然后,类型使用者可直接调用IDisposable.Dispose实现以释放非托管资源使用的内存。在正确实现Dispose方法时,安全句柄的Finalize方法或Object.Finalize方法的重写会在未调用Dispose方法的情况下阻止清理资源。

[编程基础]c#GC简介

GC的前世今生

GC的概念并非才诞生不久,早在1958年,由大名鼎鼎的图灵奖得主john mccarthy所实现的lisp语言就已经提供了GC的功能,这是GC的第一次出现。lisp的程序员认为内存管理太重要的,所以不能由程序员自己来管理。
但是后来的日子里lisp却没有成气候,采用内存手动管理的语言占据了上风,以c为代表。出于同样的理由,不同的人却又不同的看法,c程序员也认为内存管理太重要的,所以不能由系统来管理,并且讥笑lisp程序慢如乌龟的运行速度。的确,在那个对每一个byte都要精心计算的年代GC的速度和对系统资源的大量占用使很多人都无法接受。而后,1984年由dave ungar开发的smalltalk语言第一次采用了Generational garbage collection的技术,但是amalltalk也没有得到十分广泛的应用。
直到20世纪90年代中期GC才以主角的身份登上历史的舞台,这不得不归功于Java的进步,今日的GC已非吴下阿蒙。Java采用VM(virtual machine)机制,由VM来管理程序的运行当然也包括对GC管理。90年代末期.net出现了,.net采用了和Java类似由CLR(Common Language Runtime)来管理。这两大阵营的出现将人们引入了以虚拟平台为基础的开发时代,GC也在这个时候越来越得到大众的关注。
为什么要使用GC呢?也可以说为什么要使用内存自动管理?有以下一个原因:
1. 提高了软件开发的抽象度;
2. 程序员可以将精力集中在实际的问题上而不用分心来管理内存的问题;
3. 可以使用模块的接口更加的清晰,减少模块间的耦合;
4. 大大减少了内存人为管理不当所带来的bug;
5. 使内存管理更加高效。
总的来说就是GC可以使程序员可以从复杂的内存问题中摆脱出来,从而提高了软件开发的速度,质量和安全性。

什么是GC

GC就是垃圾收集,这里仅就内存而言。Garbage Collector以应用程序的root为基础,遍历应用程序在Heap上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡的,哪些仍然需要被使用。已经不再被引用程序root引用或者别的对象所引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收。

1,Mark-Compact 标记压缩算法

简单的吧.NET的GC算法看作Mark-Compact算法。阶段1:Mark—Sweep 标记清除阶段,先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的;阶段2:Compact 压缩阶段,对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使它们重新从heap基地址开始连续排列,提高局部性。
<a href="https://i.loli.net/2019/10/08/TLm9Uy5Kaq2frvF.png"><img src="https://i.loli.net/2019/10/08/TLm9Uy5Kaq2frvF.png" alt="" /></a>
Heap内存经过回收,压缩之后,可以继续采用前面的heap内存分配方法,即仅用一个指针记录heap分配的起始地址就可以。主要处理步骤:将线程挂起->确定roots->创建reachable objects graph->对象回收->heap压缩->指针修复。可以这样理解roots:heap中对象的引用关系错综复杂(交叉引用,循环引用),形成复杂的graph,roots是CLR在heap之外可以找到的各种入口。
GC搜索roots的地方包括全局对象,静态变量,局部对象,函数调用参数,当前CPU寄存器中的对象指针等。主要可以归为两类:已经初始化了的静态变量,线程仍在使用的对象。Reachable objects:指根据对象引用关系,从roots出发可以到达的对象。从roots出发可以创建reachable objects graph,剩余对象即为unreachable,可以被回收。
发现无法访问的对象时,它就使用内存复制功能来压缩内存中可以访问的对象,释放分配给不可访问对象的地址空间块。
指针修复是因为compact过程中移动了heap对象,对象地址发生变化,需要修复所有引用指针,包括stack,cpu register中的指针以及heap中其他对象的引用指针。传给COM+的托管对象也会成为root,并且具有一个引用计数以兼容COM+的内存管理机制,引用计数为0时,这些对象才可能成为被回收对象。Pinned objects指分配之后不能移动的对象,GC在指针修复时无法修改非托管代码中的引用指针。pinned objects会导致heap出现碎片。

2,托管堆

在垃圾回收器由CLR初始化以后,它会分配一段内存用于存储和管理对象。此内存为托管堆。
每个托管进程都有一个托管堆,进程中的所有线程都在同一个托管堆上为对象分配内存。
堆上分配的对象越小,垃圾回收器必须执行的工作就越少。分配对象时,请勿使用超出你需求的舍入值。
当触发垃圾回收时,垃圾回收器将回收由死对象占用的内存。回收进程会对活动对象进行压缩,以便将它们一起移动,并移除死空间,从而使堆更小一些。这将确保一起分配的对象都位于托管堆上,从而保留它们的局部性。
GC分配两个托管堆:一个用于小型对象(小型对象堆或SOH),一个用于大型对象(大型对象堆或LOH)
对象小于85000字节分配到小型SOH上,大于则分配到LOH上。
触发垃圾回收后,GC将寻找存在的对象并将它们压缩。但是由于压缩成本很高,GC会扫过LOH,列出没有被清除的对象列表以供以后重新使用,从而满足大型对象的分配请求。相邻的被清除对象将组成一个自由对象。

3,代数

虽然对象的生命周期因应用而异,但对于大多数应用来说,大部分的对象在创建不久即变成垃圾。因此针对不同age的对象也就不难理解了。
在.NET垃圾回收器中分三代,0,1,2。
小对象始终在第0代中进行分配,或者根据它们的生存期,可能会提升为第一代或者第二代。
大型对象始终在第二代中分配。
回收一代时,同时也会回收它前面所有代。
用户代码分配只能在SOH中的第0代或LOH中分配。只有GC可以在第一代和第二代中分配对象。
在mono中为写屏障来记录老代对新代的引用。

4,Finalization Queue和Freachable Queue

这两个队列和.NET对象所提供的Finalize方法有关。这两个队列并不用于存储真正的对象。而是存储一组指向对象的指针。当程序中使用new操作符在Managed Heap上分配空间时,GC会对其进行分析,如果该对象含有Finalize方法则在Finalization Queue中添加一个指向该对象的指针。
在GC被启动以后,经过Mark阶段分辨出哪些是垃圾。再在垃圾中搜索,如果发现垃圾中有被Finalization Queue中的指针所指向的对象。则把这个对象从垃圾中分离出来,并将指向它的指针移动到Freachable Queue中。这个过程被称为对象的复生(Resurrecction)。那为什么要救活它呢?因为这个对象的Finalize方法还没有被执行,所以还不能死去。当Freachable Queue中的指针指向的对象执行完Finalize方法以后,这个指针就会从队列中剔除,这个对象就能在下一次GC中被回收了。
.NET Framework的System.GC类提供了控制Finalize的两个方法,ReRegisterForFinalize和SuppressFinalize。前者是请求系统完成对象的Finalize方法,后者是请求系统不要完成对象的Finalize方法。ReRegisterForFinalize方法其实就是将指向对抗的指针重新添加到Finalization Queue中。如果在对象的Finalize方法中调用ReRegisterForFinzlize方法,这样就会形成一个在堆上永远不会死去的对象。

5,弱引用

如果应用程序代码访问一个正由该程序使用的对象时,垃圾回收器就不能回收该对象,那么就认为这个应用程序对该对象有强引用。
弱引用允许应用程序访问对象,同时也允许垃圾回收器收集相应的对象。如果不存在强引用,则弱引用的作用时间只限于收集对象前的一个不确定时间段。使用弱引用时,应用程序仍可以对该对象进行强引用,这样做可防止该对象被收集。但始终存在这样的风险:垃圾回收器在重新建立强引用之前先处理改对象。
若要对某对象建立弱引用,请使用要跟踪的对象实例创建WeakRefernce。然后将Target属性设置为该对象,将该对象的原始引用设置为null。

 aa a = new aa();
            a.a = 2;
            WeakReference c = new WeakReference(a);
            a = null;
            GC.Collect();
            Console.WriteLine(c.Target != null);
            Console.WriteLine(c.IsAlive);
            Console.WriteLine(c.Target);
/*
延时执行if判断的时候,c.Target是为null的
 var t1 = Task.Run(async delegate { await Task.Delay(5000); if (c.IsAlive)
                {
                }
            });
*/
/*
这段代码如果执行的话,在上一个GC期间c.Target是不为Null的,但是如果不执行的话,c.Target是null
  if (c.IsAlive)
                {
            }
*/
            GC.Collect();
/*
在GC调用完以后c.target为null。
*/
          Console.WriteLine(c.IsAlive);
            Console.ReadKey();

WeakRefernce也是一个引用对象,当这个对象也在被引用的时候,期内部的target是不会被回收的。

短弱引用和长弱引用

  • 短弱引用:
    垃圾回收功能回收对象后,短弱引用的目标将会变成null。弱引用本身是托管对象,与其他任何托管对象一样需要经过垃圾回收。
  • 长弱引用:
    在对象的Finalize方法已调用后,长弱引用获得保留。这样,便可以重新创建该对象,但该对象仍保持不可预知的状态,仍然可能在下一次的GC中被回收掉。若要使用长弱引用,请在WeakReference构造函数中指定true。
    如果对象类型不包含Finalize方法,或者Finalize方法没有被执行(调用了GC.SuppressFinalize();),那么将被应用为短弱引用。弱引用只在目标被收集前有效,运行终结器后可以随时收集目标。
    如果要建立强引用并重新使用对象,请将WeakReference的Target属性强行转换为对象类型。使用前请先判断Target是否为null。

[编程基础]luaGC简介

前言

在研究了一段时间的GC后,发现自己使用的两种语言,lua和c#的GC实现分别是两种高级GC算法,一个是增量式,一个是分代式。前者是标记清除,后者是标记压缩。

luaGC

在lua中,GC为标记清除增量式的,系统管理着所有已经创建了的对象,每个对象都有对其他对象的引用。root集合代表着已知的系统级别的对象引用。我们从root出发,就可以访问到系统引用到的所有对象。而没有被访问到的对象就是垃圾对象,需要被销毁。

三色垃圾回收

在GC中我们把所有的对象分为三个状态:
1. White状态,也就是待访问状态。表示对象还没有被垃圾回收的标记过程访问到。
2. Gray状态,也就是待扫描状态。表示对象已经被垃圾回收标记过程访问到了,但是对象本身对于其他对象的引用还没有进行遍历访问
3. Black状态,也就是已扫描状态。表示对象已经被访问到了,并且也已经遍历了对象本身对其他对象的引用。
基本的算法可以描述如下:

当前所有对象都是White状态;
将root集合引用到的对象从White设置成Gray,并放到Gray集合中;
while(Gray集合不为空)
{
    从Gray集合中移除一个对象0,并将0设置成Black状态;
    for(0中每一个引用的对象01){
        if(01在White状态){
            将01从White设置成Gray,并放到Gray集合中;
        }
    }
}
for(任意一个对象0){
    if(0在white状态)
        销毁对象0;
    else
        将0设置成White状态;
}

Increment Garbage Collection

上面的算法如果一次性执行,在对象很多的情况下,会执行很长时间,严重影响程序本身的响应速度。其中一个解决办法:把上面的算法分步执行,这样每个步骤所消耗的时间就比较小了。我们可以将上述的算法改为以下几个步骤。
首先标示所有root对象:
1. 当前所有对象都是White状态;
2. 将root集合引用到的对象从White设置成Gray,并且放在Gray集合中。
遍历访问所有gray对象。如果超出了本次计算量上限,退出等待下一次遍历:

while(Gray集合不为空,并且没有超出本次计算量的上限){
从Gray集合中移除一个对象0,并将0设置成Black状态;
for(0中每一个引用到的对象01){
if(01在White状态){
将01从White设置成Gray,并放到Gray集合中;
}
}
}

销毁垃圾对象:

for(任意一个对象0){
    if(0在White状态)
    销毁对象0;
    else
    将0设置成White状态;
}

在每个步骤之间,由于程序可以正常执行,所以会破坏当前对象之间的引用关系。black对象表示已经被扫描的对象,所以他应该不可能引用到一个white对象。当程序的改变使得一个black对象引用到一个white对象时,就会造成错误。解决这个问题的办法就是设置barrier。barrier在程序正常运行过程中,监控所有的引用改变。如果一个black对象需要引用一个white对象,存在两种处理办法:
1. 将white对象设置成gray,并添加到gray列表中等待扫描。这样等于帮助整个GC的标示过程向前推进了一步。
2. 将black对象改成gray,并添加到gray列表中等待扫描。这样等于使整个GC的标识过程后退了一步
这种回收垃圾回收方式被称为“Incremental Garbage Collection”(简称“IGC”)lua所采用的就是这种方法。使用“IGC”并不是没有代价的。IGC所检测出来的垃圾对象集合比实际的集合要小,也就是说,有些在GC过程中变成垃圾的对象,有可能在本轮GC中检测不到。不过这些残余的垃圾对象一定会在下一轮GC被检测出来,不会造成泄漏。

GCObject

lua使用union GCObject来表示所有的垃圾回收对象:

union GCObject{
    GCheader gch;
    union TString ts;
    union Udata u;
    union Closure cl;
    struct Table h;
    struct Proto p;
    struct UpValue uv;
    struct lua_state th;
}

#define CommonHeader GCObject *next;lu_byte tt;lu_byte marked

typedef struct GCheader {
    CommonHeader;
}GCheader;

marked这个标志用来记录对象与GC相关的一些标志位,其中0和1用来表示对象的white状态和垃圾状态。当垃圾回收的标识阶段结束后,剩下的white对象就是垃圾对象。由于lua并不是立即清除这些垃圾对象,而是一步一步逐渐清除,所以这些对象还会在系统中存在一段时间。这就需要我们能够区分同样为white状态的垃圾对象和非垃圾对象。lua使用两个标志位来表示white,就是为了高效的解决这个问题。这个标志位会轮流被当作white状态标志,另一个表示垃圾状态。在global_Static中保存着一个currentwhite,来表示当前是哪个标志位用来标识white。每当GC标识阶段完成,系统会切换这个标志位。这样原来为white的所有对象不需要遍历就会变成垃圾对象,而真正的white对象则使用新的标志位标识。
第二个标志位用来表示black状态,而既非white也非black也就是gray状态。
除了short string 和open upvalue之外,所有的GCObject都是通过next被串接到全局状态global_State中的allgc链表上。我们可以通过遍历allgc链表来访问系统中的所有GCObject,short string 被字符串标单独管理,open upvalue会在被close时也连接到allgc上。

引用关系

垃圾回收过程通过对象之间的引用关系来标识对象。以下是lua对象之间在垃圾回收标识过程中需要遍历的引用关系:

所有字符串对象,无论长串还是短串,都没有对其他对象的引用。
uesdata对象会引用到一个metatable和一个env table。
Upval对象通过v引用一个TValue,再通过这个TValue间接引用一个对象。在open状态下,这个v指向stack上的一个TValue。在close状态下,v指向Upval自己的TValue。
table对象会通过key,value引用到其他对象,并且如果数组部分有效,也会通过数组部分引用。并且table会引用一个metatable对象。
lua closure会引用到proto对象,并且会通过upvalue数组引用到Upval对象。
c closure会通过upvalues数组引用到其他对象,这里的upvalue与lua closure的upvalue完全不是一个意思。
Proto对象会引用到一些编译器产生的名称,常量,以及内嵌于本Proto中的Proto对象。
thread对象通过stack引用其他对象

barrier

在igc的mark阶段,为了保证所有black对象都不会引用white对象这个不变性,需要使用barrier。
barrier被分为“向前”和“向后”两种。
luaC_barrier_函数用来实现“向前”的barrier。“向前”的意思是当一个black对象需要引用一个white对象时,立刻mark这个white对象。这样white对象就变成gray对象,等待下一步的扫描。这也就是帮助GC向前标识一步。luaC_barrier_函数被用在以下引用变化处:

  • 虚拟机执行过程中或者通过api修改close upvalue对其他对象的引用
  • 通过api设置userdata或table的metatable引用
  • 通过api设置userdata的env table引用
  • 编译构建proto对象过程中proto对象对其他编译产生对象的引用

luaC_barrierback_函数用来实现“后退”的barrier。“向后”的意思就是当一个black对象引用一个white对象时,将已经扫描过的black对象再次变成gray对象,等待重新扫描。这也就是把gc的mark后退一步。luaC_barrierback_目前只用来监控table的key和value对象引用的变化。table是lua中最主要的数据结构,练全局变量都是被保存在一个table中,所以table的变化是比较频繁的。并且同一个引用可能被反复设置成不同的对象。对table的引用使用“向前”的barrier,逐个扫描每次引用变化的对象,会造成很多不必要的消耗。而使用“向后”的barrier就等于将table分成了“未变”和“已变”两种状态。只要一个table改变了一次,就将它变成gray,等待重新扫描。被变成gray的table在被重新扫描之前,无论引用再发生多少次变化也都无关紧要了。
引用关系变化最频繁的要数thread对象了。thread通过stack引用其他对象,而stack作为运行期栈,在一直不停地被修改。如果要监控这些引用变化,肯定会造成执行效率严重下降。所以lua并没有在所有的stack引用变化处加入barrier,而是直接假设stack就是变化的。所以thread对象就算被扫描完成,也不会被设置成black,而是再次设置成gray,等待再次扫描。

Upvalue

Upvalue对象在垃圾回收中的处理是比较特殊的。
对于open状态的upvalue,其v指向的是一个stack上有TValue,所以open upvalue与thread的关系非常紧密。引用到open upvalue的只可能是其从属的thread,以及lua closure。如果没有lua closure引用这个open upvalue,就算他一定被thread引用着,也已经没有实际意义了。应该被回收掉。也就是说thread对于open upvalue的引用完全是一个弱引用。所以lua没有将open upvalue当作一个独立的可回收对象,而是将其清理工作交给从属的thread对象来完成。在mark过程中,open upvalue对象只使用white和gray两个状态,来代表是否被引用到。通过上面的引用关系可以看到,有可能引用open upvalue的对象只可能被lua closure引用到。所以一个gray的open upvalue就代表当前有lua closure正在引用它,而这个lua closure不一定在这个thread的stack上面。在清扫阶段,thread对象会遍历所有从属自己的open upvalue。如果不是gray,那就说明没有lua closure引用这个open upvalue,可以被销毁。
当退出upvalue的语法域或者thread被销毁,open upvalue会被close。所有close upvalue与thread已经没有弱引用关系,会被转化为一个普通的可回收对象,和其他对象一样进行独立的垃圾回收。

__GC

对于lua5.0以后的版本支持userdata,它是可以带有__gc方法,当userdata被回收时会调用这个方法。所以一遍标记是不够的。不能简单的把变成垃圾的userdata简单剔除,那样就无法正确的调用__gc了。所以标记流程需要分两个阶段做。第一阶段把包括userdata在内的死亡对象剔除出去。然后在死对象中找回有__GC方法的,对它们再做一次标记复活相关的对象,这样才能保证userdata的__gc可以正确运行。执行完__gc的userdata最终会在下一轮gc中释放(如果没有在__gc中复活)。userdata有一个单向标记,标记__gc方法是否有运行过,这可以确保userdata的__gc只会执行一次,即使在__gc中复活(重新被root集合引用),也不会再次分离出来反复运行finalizer。也就是说,运行过finalizer的userdata就永久变成了一个没有finalizer的userdata了。

[编程基础]异步,并发,协程原理

linux操作系统在设计上将虚拟空间划分为用户空间和内核空间,两者做了隔离是相互独立的,用户空间给应用程序使用,内核空间给内核使用。

1。异步

应用程序和内核

内核具有最高权限,可以访问受保护的内存空间,可以访问底层的硬件设备。而这些是应用程序所不具备的,但应用程序可以通过调用内核提供的接口来间接访问或操作。所谓的常见的IO模型就是基于应用程序和内核之间的交互所提出来的。以一次网络IO请求过程中的read操作为例,请求数据会先拷贝到系统内核的缓冲区(内核空间),再从操作系统的内核缓冲区拷贝到应用程序的的地址空间(用户空间)。而从内核空间将数据拷贝到用户空间的过程中,会经历两个阶段:
- 等待数据准备
- 拷贝数据
也正是因为有了这两个阶段,才提出来各种网络I/O模型。

同步和异步

同步:在发出一个同步调用时,在没有得到结果之前,该调用就不返回
异步:在发出一个异步调用后,调用者不会立刻得到结果,该调用就返回了

同步和异步的概念描述的是应用程序与内核的交互方式,同步是指应用程序发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行;而异步是指应用程序发起I/O请求后仍继续执行,当内核I/O操作完成后会通知应用程序,或者调用应用程序注册的回调函数。就是函数执行来同步操作,那后面的程序就在这个同步操作没有结束,是不会再执行的。执行来异步操作,就是不管你操作有没有结束,函数后续的都会继续执行。

阻塞和非阻塞

阻塞调用:是指调用结果返回之前,调用者会进入阻塞状态等待。只有在得到结果之后才会返回
非阻塞调用:是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

阻塞和非阻塞的概念描述的是应用程序调用内核IO操作的方式,阻塞是指I/O操作需要彻底完成后才返回用户空间,而非阻塞是指I/O操作被调用后立刻返回用户一个状态值,无需等到I/O操作完成。而这里主要将的是结果的概念。如果阻塞操作没有完成,就任何返回值都没有。非阻塞的话,不管有没有完成,都会给一个返回值的。
常见的网络I/O模型大概四种
1. 同步阻塞IO(Blocking IO)
2. 同步非阻塞IO(Non-blocking IO)
3. IO多路复用(IO Multiplexing)
4. 异步IO(Asynchronous IO)

IO多路复用

多路I/O复用模型是利用select,poll,epll可以同时监察多个流的I/O事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是指轮询那些真正发出流事件的流),并且只依次顺序的处理就绪的流,这种做法就避免流大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一线程。采用多路I/O复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)。IO多路复用用的是异步阻塞。
#二,并发
并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
并发和并行但区别:
- 并发(concurrency):逻辑上具备同时处理多个任务但能力。
- 并行(parallesim):物理上在同一时刻执行多个并发任务,依赖多核处理器等物理设备。
多线程或多进程是并行的基本条件但是单线程也可用到协程做到并发。通常情况下,用多进程来实现分布式和负载平衡,减轻单进程垃圾回收压力;用多线程抢夺更多的处理器资源;用协程来提高处理器时间片利用率。现代系统中,多核CPU可以同时运行多个不同的进程或者线程。所以并发程序可以是并行的,也可以不是。

三,协程

1,线程模型

在现代计算机结构中,先后提出过两种线程模型:用户级线程(user-level threads)和内核级线程(kernel-level threads)。所谓用户级线程是指:应用程序在操作系统提供的单个控制流的基础上,通过在某些控制点(比如系统调用)上分离出一些虚拟的控制流,从而模拟多个控制流的行为。由于应用程序对指令流的控制能力相对较弱,所以用户级线程之间的切换往往受线程本身行为以及线程控制点选择的影响。线程是否能公平的获得处理器时间取决于这些线程的代码特征。而且,支持用户级线程的应用程序代码很难做到跨平台移植,以及对于多线程模型的透明。用户级线程模型的优势是线程切换效率高,因为它不涉及系统内核模式和用户模式之间的切换;另一个好处是应用程序可以采用适合自己特点的线程选择算法,可以根据应用程序的逻辑来定义线程的优先级,当线程数量很大时,这一优势尤为明显。但是这同样会增加应用程序代码的复杂性。有一些软件包可以减轻程序员的负担。
内核级线程往往指操作系统提供的线程语义,由于操作系统对指令流有完全的操控能力,甚至可以通过硬件中断来强迫一个进程或线程暂停执行,以便把处理器时间移交给其他进程或线程,所以,内核级线程有可能应用各种算法来分配处理器时间。线程可以有优先级,高优先级的线程被优先执行,它们可以抢占正在执行的低优先级线程。在支持线程语义的操作系统中,处理器的时间通常通常是按线程而非进程来分配,因此,系统有必要维护一个全局的线程标,在线程表中记录每个线程的寄存器,状态以及其他一些信息。然后系统在适当的时候挂起一个正在执行的线程,选择一个新的线程在当前处理器上继续执行。由于时间点的执行代码可能分布在操作系统的不同位置,所以在现代操作系统中,线程调度往往比较复杂,其代码通常分布在内核模块的各处
内核级线程的好处是:应用程序无须考虑是否要在适当的时候把控制权交给其他线程,不必担心自己霸占处理器而导致其他线程得不到处理器时间。应用程序只要按照正常的指令流来实现自己的逻辑就好了,内核会妥善地处理好线程之间共享处理器的资源分配问题。然而,这种对应用程序的便利也是有代价的,即,所有的线程切换都是在内核模式下完成的,因此,对于在用户模式下运行的线程来说,一个线程被切换出去,以及下次轮到它的时候再被切换进来,要设计两种模式切换:从用户模式切换到内核模式,在从内核模式切换回用户模式
除了线程切换的开销是一个考虑因素以外,线程的创建和删除也是一个重要的考虑指标。当线程的数量较多时,这部分的开销是相当可观的,虽然线程的创建和删除比起进程要轻量得多,但是一个进程内建立起一个线程的执行环境,数据结构的初始化工作,以及完成与系统环境相关的一些初始化工作,这些负担是不可避免的。另外线程数量较多时,伴随而来的线程切换开销也必然随之增加。所以,当应用程序或者系统进程需要的线程数量可能比较多的时候,通常可采用线程池为一种优化措施。
在支持内核级线程的系统环境中,进程可以容纳多个线程,这导致了多线程程序设计模型,由于多个线程在同一个进程环境中,它们共享了几乎所有的资源,所以线程之间的通信要方便和高效得多,这往往是进程间通信所无法比拟的,但是,这种便利性也很容易使线程之间因同步不正确而导致数据破坏,而且这种错误存在不确定性,因而相对来说难以发现和调试。

2,什么是协同式和抢占式

许多协同式多任务操作系统,也可以看成协程运行系统。说到协同式多任务系统,一个常见的误区是认为协同式调度比抢占式调度“低级”,因为我们所熟悉的操作系统都是从协同式调度过度到抢占式多任务系统的。实际上,调度方式并无高下,完全取决于应用场景。抢占式系统允许操作系统剥夺进程执行权限,抢占控制流,因而天然适合服务器和图像操作系统,因为调度器可以优先保证对用户交互和网络事件的快速相应。协同式调度则等到进程时间片用完或系统调用时转移执行权限,因此适合实时或分时等等对运行时间有保障的系统。
另外,抢占式系统依赖于CPU的硬件支持。因为调度器需要“剥夺”进程的控制权,就意味着调度器需要运行在比普通进程高的权限上,否则任何任何“流氓”进程都可以去剥夺其他进程了。只有CPU支持了执行权限后,抢占式才成为可能。而协同式多任务适用于那些没有处理器权限支持的场景,这些场景包含资源受到限制的嵌入式系统和实时系统。在这些系统中,程序均以协程的方式运行。调度器负责控制流的让出和恢复。通过协程的模型,无需硬件支持,我们就可以在一个“简陋”的处理器上实现一个多任务的系统。

协程的基本概念

“协程”可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。总的来说,协程为协同任务提供来一个运行时抽象,这种抽象非常适合于协同多任务调度和数据流处理。在现代操作系统和编程语言中,因为用户态线程切换比内核态线程小,协程成为来一种轻量级的多任务模型。
从编程角度上看,协程的思想本质上就是控制流的主动让出和恢复机制,迭代器常被用来实现协程,所以大部分的语言实现的协程都有yield关键字。

协程,进程,线程的特点以及区别

进程

  • 进程是资源分配的最小单位
  • 进程间不共享内存,每个进程拥有自己独立的内存
  • 进程间可以通过信号,信号量,共享内存,通道,队列等来通信
  • 新开进程开销大,并且CPU切换进程成本也大
  • 进程由操作系统调度
  • 多进程方式比多线程更加稳定

线程

  • 线程是进程执行流的最小单位
  • 线程来自于进程,一个进程下可以开多个线程
  • 每个线程都有自己一个栈,不共享栈,但多个线程能共享一个属于进程但堆
  • 线程因为是在一个进程内,所以内存可以共享
  • 线程也是由操作系统调度,线程是CPU调度的最小单位
  • 新开线程开销成本比进程小,CPU切换线程成本也比进程小
  • 某个线程发生致命错误的时候,会导致整个进程崩溃
  • 进程间读写变量存在锁的问题处理起来相对麻烦

协程

  • 对于操作系统来说只有,进程,线程,协程是有应用程序显式调度,非抢占式
  • 协程的执行最终还是要靠线程,应用程序来调度协程选择合适的线程来获取执行权
  • 切换非常快,开销成本低。一般占用栈大小原小于线程,所以可以开更多的协程
  • 协程比线程更轻量级

[编程基础]GC高级算法

在讨论完三大基础算法以后,我们现在来看看这基础算法的结合以后做成的高级算法。其中最具有代表性的就是:分代回收,增量回收,和并行回收,这三种。

1:分代回收

首先,我们来聊聊高级GC算法中最重要也是最常用的一种,即分代回收。由于GC和程序处理的本质是无关的,因此它所消耗的时间越短越好。分代回收的目的,正是为了在程序运行期间,将GC所消耗的时间尽量缩短。分代回收的基本思路,是利用来一般程序所具备的性质,即大部分对象都会在短时间内成为垃圾,而进过一定时间依然存活的对象往往拥有较长的寿命。如果寿命长的对象更容易存活下来,寿命短的对象则会被很快遗弃,那么如果对分配不久,诞生时间较短的“年轻”对象进行重点扫描,应该可以更有效的回收大部分垃圾。

在分代回收中,对象按照生成时间进行分代,刚诞生不久的年轻对象划分为新生代,而存活来较长时间的对象划分为老生代。根据具体实现方式的不同,可能还会划分更多的代。如果上述关于对象寿命的假说成立的话,那么只要对新生代对象进行扫描,就可以回收遗弃对象中很大一部分。像这种只扫描新生代对象的回收操作,被成为小回收。小回收中在扫描的时候如果遇到属于老生代的对象时,就不会对这个对象进行递归扫描了。这样一来,需要扫描的对象数量也会减少。然后把回收一次以后残留的对象划分到老生代中。

分代回收

上图中,任何地方都没有再被引用的对象F将会通过大回收进行回收。

上图(1)中的D被老生代E所引用,如果根据我们上面所有,如果在回收标记的时候,不对老生代中的对象进行递归引用标记操作的话,那D将不被任何对象引用,那将在一次GC中被回收掉。那这样的话,就就出现我们不期望的结果。所以我们需要做一次操作来,实现老生代对新生代的引用,这个操作一个记录,就是上图中的记录集,这个记录集就是对老生代对新生代引用变更的一个记录。在做小回收的时候,这个引用集将作为一个根来对待。

要让分代回收正确的工作,必须使记录集的内容保持更新。因此,在老生代到新生代的引用产生的瞬间,就必须对该引用进行记录,而负责执行这个操作的子程序,需要被嵌入到所有涉及对象更新操作的地方。这种检查程序需要对所有涉及修改对象内容的地方进行保护,因此被成为写屏障。

随着程序的不断运行,老生代区域中的“死亡”对象也在不断增加。为了避免这些“死亡”对象对内存的白白占用,所以需要有时候对所有的区域都进行一次扫描回收,这个操作叫做大回收或者完全回收。分代回收通过减少GC中扫描对象数量,达到减少GC带来平均中断时间的效果。不过由于还是要经历大回收的,所有最大中断时间并没有得到什么改善。

2:增量回收

在对实时性要求很高的程序中,比起缩短GC的平均中断时间,往往更重视缩短GC的最大中断时间。在这些实时性要求很高的程序中,必须能够对GC所产生的中断时间做出预测。在一般的GC算法中,做出这样的保证是不可能的,因为GC产生的中断时间与对象的数量和状态有关。因此,为了维持程序的实时性,不等到GC全部完成,而是将GC划分多个部分逐一执行。这种方式就叫做增量回收。在增量回收中,由于GC的过程是渐进的,在回收过程中程序本身也会继续运行,对象之间的引用关系也可能会发生变化。如果已经完成扫描和标记的对象被修改,对新的对象产生引用,这个新对象就不会被标记,这样明明还被引用的却会被回收。所以在增量回收中也采用了写屏障。当已经被标记的对象的引用发生变化时,通过写屏障会将新被引用的对象作为扫描的起点记录下来。由于增量回收的过程是分布渐进式的,可以将中断时间控制在一定长度之内。另一方面,由于中断操作需要消耗一定的时间,GC消耗的总时间会相应的增加。

3:并行回收

在现在的计算机中,一块芯片搭载多个cpu核心的多核处理器已经是一个司空见惯的东西了。那么在这种环境下,就需要利用多线程来充分发挥多CPU的性能,并行回收正是通过最大限度利用多CPU的处理能力来进行GC操作的一种方式。并行回收的基本原理是:是在原有的程序运行的同时进行GC操作,这一点和增量回收是相似的。因此也会遇到增量回收相同的问题。为了解决这个问题也需要写屏障来对当前的状态信息保持更新。

[编程基础]GC基础算法

基本术语

1.垃圾(Garbage)

就是需要被回收的对象

如果程序可能会直接或者间接地引用一个对象,那么这个对象就被视为“存活”,与之相反的,没有被引用到的就被视为“死亡”。将这些“死亡”对象找出来,然后作为垃圾进行回收,这就是GC的本质。

2.根(root)

就是判断对象是否可被引用的起始点。

至于哪里才是根,不同语言,不同编辑器都有不同的规定,但基本上是将变量和运行栈作为根。

三大基础GC算法

1.标记清除法/标记压缩法

标记清除是最早开发出的GC算法。原理非常简单,首先从根开始将可能被引用的对象用递归的方式进行标记,然后将没有标记到的对象作为垃圾进行回收。

标记清除算法

图中显示了标记清除算法的大致原理,1阶段显示了随着程序运行而分配出一些对象的状态,一个对象对其他的对象引用。2阶段开始执行GC操作,从根开始对可能被引用的对象打上“标记”,大多数情况下,这种标记通过对象内部的标志(flag)来实现。3阶段,被标记的对象所引用的对象也打上标记,一直重复这样的操作,就可以把从根开始可能被间接引用到的对象都打上标记。此阶段为标记阶段。

标记阶段完成时,被标记的对象就被视为“存活”对象,4阶段把所有对象都扫描一遍,将没有被标记的对象进行回收,这一阶段为清除阶段。

在扫描的同时,还需要将存活的标记状态清理掉,以便下一次GC操作做好准备,标记清除的消耗时间和存活对象数与对象总数的总和相关。

标记压缩算法,是标记清除算法的变形,他不是将没被标记的对象清除,而是不断压缩。

2:复制收集算法

标记清除算法有一个很大的问题,在分配大量对象的时候,当存活很少一部分的时候,还是需要对全部对象进行一次扫描,就算是不能在用的也要进行扫描,所需要用到的时间大大超过必要值。复制收集算法则试图克服这一问题,在复制收集算法里,会从根结点开始把被引用和间接引用的对象复制到另外一个空间,然后把原空间给回收。

复制收集

(1)为开始GC时候的内存状态,(2)在原对象空间以外再开辟了一个空间,然后把根引用到的对象(A)给复制过去,(3)把A所引用的对象也给复制过去,递归向下,把所有被根间接引用的对象都复制到新的空间内,而“死亡”对象就还留在旧空间内。(4)回收旧的对象空间,使用新的对象空间。这样就把所有的死亡对象都给释放掉了,留下的还存活的对象。

复制收集对比标记清除中,只有一开始标记的时候是需要递归遍历的,后面标记清除中的扫描所有的对象进行清除这步是不需要的。所有减少了不必要的扫描开销。

但是复制对象的内存开销会比较大,如果存活的对象比较多的情况下,这种算法就十分消耗内存。

还有一个好处就是,关系比较近的对象,可能会放在距离较近的内存空间,可以提高程序运行性能。

3:引用计数法

引用计数方式是GC算法中最简单,也是最容易实现的一种。它的原理:在每个对象中保存该对象的引用计数,当引用发生增减的时候对引用计数进行更新。当引用计数为0的时候,该对象将不被引用,因此可以释放相应的内存空间。

引用计数

图中(1)部分中,记录着所有对象都保存着自己被多少个其他对象进行引用的数量。(2)中对象引用发生变化时,引用计数也跟着变化。当引用b到d的引用失效,d的引用就为0,将被回收,所以d所引用对象的引用计数也将相应的减少。(3)中对所有引用计数为0的对象进行释放。在这个GC过程中,不需要对所有的对象进行扫描。

在这个GC算法中当对象不被引用的瞬间就会被释放。在其他GC算法中,要预测对象是否能被释放是一个很困难的事,但是在引用计数中,当为0就能被释放了。而且这个释放操作是针对每个对象个别执行的,因此对比其他GC算法,中断时间也比较短。

引用计数最大的缺点就是,不能释放被循化引用的对象

如上图所示,三个对象没有被其他对象所引用,但是相互之间循环引用了。因此它们永远都不会被释放,

还有一个问题就是,必须在引用计数发生变化的时候做出正确的增减,如果出现问题,就会出现内存错误,少加了就会释放过早,多加了就会出现内存泄漏。

最后一个缺点,引用计数管理不适合并行处理,如果多个线程同时对引用计数进行更新,引用计数可能会产生不一致的问题。为了避免这种情况的发生,对引用计数的操作必须采用独占的方式来进行。如果引用操作频繁发生,每次都要使用加锁等并发控制机制的话,其开销也是不可小觑的

[unity3D]assetbundle卸载方案

前言

关于ab包的卸载,最重要的一部分就是实例化以后的gameobject还依赖这个ab包的资源导致ab包卸载以后gameobject会出现资源丢失,但是如果不被卸载,内存就会持续增加。现阶段有两个方案1:引用计数,被引用了几次;2:弱引用,销毁gameobject内存回收,引用自动消除。

assetbundle卸载原理

当场景中的object被移除后,unity不会自动卸载相关的asset,所以asset的卸载需要我们自己来调用。Asset卸载清理可以是特定时刻卸载,也能手动强制卸载。

调用方法为Assetbundle.Unload(bool)和Resource.UnLoadUnusedAssets这两个方法。

AssetBundle.Unload(true)

卸载实例化的Asset,如果其中的某些asset还在被object所引用,就会出现丢失情况

通过这种方法卸载,资源会被卸载的很彻底,完全从内存移除即使还在代码中引用,也会被移除。

AssetBundle.UnLoad(false)

切断ab与其asset之间的关联,不会卸载asset,所以引用不会丢失,asset还在内存中,但是切断例联系,所以再次从ab包中实例化asset的时候不会返回已经初始化过的asset,而是重新实例化,就会出现asset的冗余。

Resource.UnloadUnusedAssets

卸载不被引用的asset,如果asset被gameobject引用,不会卸载。

被场景引用,不会被卸载

被代码引用,不会被卸载。

调用的消耗很大,该函数的主要作用是查找并卸载不再使用的资源。

Asset的引用计数卸载方案

主要是当加载asset的资源的时候,加载一次计数+1,卸载一次计数-1,当减到不用当时候就可以unloadAsset了。

在做UI当资源管理当时候,因为当前所用的资源都是这个window窗口的。所以可以用一个资源栈来做。当打开一个窗口当时候,做一个标志位,在这个标志位以上的资源都是这个窗口所使用加载的。当关闭窗口的时候,把这个阶段的资源全部卸载掉。使用栈就需要窗口的打开和关闭是一一对应的。

Asset的弱引用卸载方案

关键词WeakReference。这个类的作用就是可以在GC的时候释放对象,回收内存。当我们把Asset做弱引用,那当产生一次GC的时候,我们就可以调用Resource.UnloadUnusedAssets来回收Asset。这个方法的危险点就是在于调用Resource.UnloadUnusedAssets的时候消耗很大,所以调用的时机得把握好。AssetBundle.UnLoad(false)也可以,但是释放对象得走一次GC,c#的GC自动调用的时间不知道,手动调用的话会使内存一下增加很多。