用于记录C#知识要点。
参考:CLR via C#、C#并发编程、MSDN、百度
记录方式:读每本书,先看一遍,然后第二遍的时候,写笔记。
CLR:公共语言运行时(Common Language Runtime)是一个可由多种编程语言(VB、F#等)使用的公共语言运行库。
托管模块:编译源码会生成托管模块,他是标准的PE文件。包含:PE32头、CLR头、IL中间语言。
元数据:CLR除了生成IL外,还会生成元数据。元数据总与包含IL代码的文件相关联。
程序集:是一个或多个模块/资源的逻辑性分组。是重用、安全性、版本控制的最小单元。程序集可以是DLL或者可执行程序。
FCL:Framework类库(Framework Class Library)一组DLL程序集的统称。
CTS:通用类型系统,制定的一个正式规范描述类型定义和行为的系统。
CLS:公共语言规范,详细定义了一个最小功能集。只有支持这个功能集,生成的类型才能兼容其他组件。
OOP:面向对象编程(Object-Oriented Programming)软件的一种设计模式,基于封装、继承、多态这三个概念进行开发。
CSP:组件软件编程(Component Software Programming)OOP发展到极致的产物,讲代码形成组件模块。
JIT: 即时编译器
两种程序集
CLR支持两种程序集:弱命名程序集 和 强命名程序集。
他们结构完全相同,均包含PE文件、PE32头、CLR头、元数据、清单表、IL。
区别在于强命名程序集使用发布者的公钥/私钥进行了非对称加密签名。可以部署到任何地方。
两种部署
程序集可以采用两种方法进行部署:私有或全局。
私有部署指应用程序目录或某个子目录。弱命名程序集只能私有方式进行部署。
全局部署指部署到一些公认位置的程序集。CLR在查找程序集时,会检查这些位置。
CLR要求每个类型最终都从 System.Object 类型派生。所以每个类型都会有基本方法。
类型安全:CLR最重要的特性之一,运行时CLR总能知道对象的类型是什么。调用GetType即可知道对象的确切类型。
类型转换
隐式转换与显式转换。如派生类转换为基类,称为隐式转换,不需要操作。
//隐式转换 Object o = new Int32(); //显式转换 Int32 i = (Int32)o;
is 和 as 操作符转换。
is 判断是否可以转换。可以返回 true。
as 是is的简化写法。会判断是否可以转换,如果不可以返回null,可以则返回转换后的结果。
基元类型:编译器直接支持的数据类型称为基元类型,编译器会自动映射到对应对象。例如string 会映射到 System.String
checked 和 unchecked 基元操作
对基元类型进行算术运算操作,可以造成溢出。编译器默认不会检查溢出,checked操作符进行检查。unchecked操作符不进行检查。
checked { Byte b = 100; b = (Byte)(b + 300); }
uint i = unchecked((UInt16)(-1));
CLR支持两种类型:引用类型和值类型。
引用类型:从托管堆分配,new操作符返回对象内存地址。每次初始化都会进行内存分配,所以效率会变慢。
值类型:一般在线程栈上,不包含指向指针。不受GC控制,缓解了托管堆的压力。
任何称为类的类型,都是引用类型。值类型都称为结构或枚举。
值类型比引用类型轻的原因是,不用分配内存,不被GC,不会通过指针引用。
装箱:值类型转换成引用类型,称为装箱。分别进行三步:1.分配内存。2.赋值字段到堆中。3.返回对象地址。
拆箱:引用类型转换值类型,称为拆箱。分别进行两步:1.获取已装箱的地址。2.将字段包含的值从堆复制到栈的实例中。
拆箱效率比装箱低得多。
哈希码:Object提供GetHashCode可以获得任何对象的哈希码。
dynamic基元类型:表示对象的操作将在运行的时候解析。可以绕过类型安全监测。
static void Main(string[] args) { dynamic value; for (int demo = 0; demo < 2; demo++) { value = (demo == 0) ? (dynamic)5 : (dynamic)"A"; M(value); } Console.ReadLine(); } private static void M(int n) { Console.WriteLine("Int类型{0}", n); } private static void M(string s) { Console.WriteLine("String类型{0}",s); }
成员的可访问性
private 只能由定义类型或嵌套类型的方法访问。
protected 只能由定义类型、嵌套类型、派生类型中访问。
internal 只能由定义程序集中的方法访问
protected internal 任何嵌套、派生、程序集中的方法访问。
public 可以由任何程序集访问。
静态类:static 定义不可实例化的类。不能应用于结构。静态方法不会进行GC。
静态类不能创建实例、不能实现任何接口、只能定义静态成员、不能作为字段,方法参数或局部变量使用。
静态类从程序启动时就会一直占用内存。只有确定不需要实例化、继承等特性的时候,才使用静态类。
分部类:partial可以将代码分散到一个或多个源代码中。
分部类的优点:源代码控制方便,在同一个类中分解成不同的逻辑单元,代码拆分。
组件版本控制关键字
abstract 抽象方法,指派生类型必须重写并实现这个成员。
virtual 虚方法,指已定义实现,可由派生类型重写。
override 实现抽象,表示正在重写类型的成员。
sealed 密封类,不能被派生重写。
new 表示该成员与基类中相似的成员无关系。public new void ToString()
常量:定义以后不能修改。public const int result = 3;
只读:运行时可以赋值一次。public readonly int result = 3;
字段修饰符
static 静态字段
const 常量字段,只在声明的时候写入。
readonly 只读字段,只在构造的时候写入。
volatile 易变类型,保证原子性读取
构造方法在元数据表中,始终叫做.ctor。构造器,不能被继承。所以不能添加版本控制关键字。
抽象类默认构造函数访问为protected。其余均默认为public。静态类没有构造函数。
this()可显示调用另一个此类中另一个构造函数。public SomeType(string x):this(){}
base()可显示调用基类的构造函数。public SomeType():base(){}
操作符重载
在类里面可以重载操作符,重载后可直接对类进行操作。
重载方法必须是public 和 static方法。添加operator标志,来进行操作符选择。
Complex co1 = new Complex(1, 2); Complex co2 = new Complex(3, 4); Console.WriteLine((co1 + co2).a); public static Complex operator +(Complex c1, Complex c2) { c1.a += c2.b; return c1; }
转换操作符重载
在类里面可以重载转换操作符,重载后可显示或隐式转换类。
转换操作符必须是public 和 static方法,并且必须使用operator标记。
隐式转换:implicit
public static implicit operator Rational(Int32 num) { return new Rational(num); } Rational r1 = 5;
显式转换:explicit
public static explicit operator Int32(Rational r) { return r.ToInt32(); } Int32 r2 = (Int32)r1;
扩展方法
C#只支持扩展方法,不能扩展属性、事件、操作符等等。
扩展方法第一个参数前面必须有this。必须在非泛型静态类中声明。静态类必须为顶级静态类,单独一个文件作用域。
如果命名空间不同,则需要using引用dll。
下面是StringBuilder的扩展,添加了一个show方法
public static class StringBuilderExtensions { public static void Show(this StringBuilder sb) { Console.WriteLine("这是扩展方法,{0}",sb.ToString()); } } StringBuilder sb = new StringBuilder("我是参数"); sb.Show();
泛型接口扩展
public static void ShowItems<T>(this IEnumerable<T> collection) { foreach (var item in collection) { Console.WriteLine(item); } } "Chenxy".ShowItems();
委托扩展
public static void InvokeAndCatch<T>(this Action<object> d, Object o) where T : Exception { try { d(o); } catch (T) { } } //Action<Object> action = delegate (object o) { Console.WriteLine(o.GetType()); }; Action<Object> action = o => Console.WriteLine(o.GetType()); action.InvokeAndCatch<NullReferenceException>(null);
稍微说说这个委托扩展,这个的效果就是把空对象这个异常类给吞噬了。然后采用匿名或者lambda调用。
可选参数:参数后面使用 = 赋值。则调用的时候,如未填写此参数,自动赋值默认参数。DateTime dt = default(DateTime)
default:给类型赋值默认值。
命名参数:调用方法时,可使用名字进行选择性赋值。例如:M(s: "B"); public void M(String s = "A")
隐式变量:var 要求编译器根据表达式推断具体数据类型。只能声明在方法内部。
out:不指望调用者在调用方法之前初始化好对象。被调用的方法不能读取参数的值,需要在返回前写入这个值。
ref:调用者必须在调用方法前初始化参数的值,被调用的方法可以读取值以及写入。
可变数量:params 只能用于方法参数的最后一个。可以传递多个相同类型的参数。params Object[] objects
声明方法参数类型,应指定最弱类型,宁愿要接口也不要基类。
例如:处理一组数据,参数最好用接口 IEnumerable<T> 不要使用强类型List 或 ICollection
原因:可以传任何继承IEnumberable接口的类型,例如字符串。更加灵活,适合更广泛的场景。
尽量做到参数基类,返回值派生类。例如
//好 public void Main<T>(IEnumerable<T> collection) { } //不好 public void Main<T>(IList<T> collection) { } //好 public void Pro(Stream str) { } //不好 public void Pro(FileStream str) { } //好 public FileStream Open() { } //不好 public Stream Open() { }
面向对象设计原则之一数据封装,意味着类型的字段永远不应该公开,否则很容易破坏对象的状态。
强烈建议将所有字段都设为private。如果允许用户获取或设置,则公开一个方法。称为访问器(accessor)
自动实现属性:封装字段的操作,并定义获取和设置方法。public string name {get;set;}
对象初始化器:构造一个对象并初始化一些公共属性(字段)。
Employee em = new Employee() { name = "", old = "" }; //可以省略前面的括号 Employee em1 = new Employee { name = "", old = "" }; //集合初始化器 var table = new Dictionary<string, int> { { "",1 }, { "",2 } };
匿名类型:可以用很简洁的语法来自动声明不可变的含有一组属性的类型。
//匿名类型 var ol = new { Name = "Chenxy", Year = 1993 }; //匿名数组 var people = new[] { new { Name="Chenxy",Year=1993 }, new { Name="Chenxy1",Year=1992 } };
匿名类型可以配合LINQ做投影
IEnumerable<Employee> list = new List<Employee> { new Employee { name="chenxy",old="1991" }, new Employee { name = "chenxy", old = "1992" } }; var query = from li in list select new { Year = li.old }; foreach (var item in query) { Console.WriteLine(item.Year); }
索引器重载:对类型的[] 操作进行重载。需要使用this关键字来进行操作。
public Employee this[Int32 i] { get { return new Employee { name = "索引方法", old = i.ToString() }; } set { } } var query = new Employee() { }; Console.WriteLine(query[1].old);
事件类型
事件:定义事件成员的类型允许通知其他对象发生了特定的事情。例如点击后执行方法,就是通过事件实现。
事件模型以委托为基础,委托是调用回调方法的一个类型安全的方式。对象凭借回调方法接收通知。
主要是使用委托机制,在之前配置好事件调用,然后收到新信息的时候,执行委托。约定消息定义后缀使用EventArgs
//消息 internal class NewEventArgs : EventArgs { public string str_name { get; set; } public NewEventArgs(string name) { str_name = name; } } //委托 internal class MailManager { public event EventHandler<NewEventArgs> NewMail; //收到信息 public void Go(string value) { OnMail(new NewEventArgs(value)); } protected virtual void OnMail(NewEventArgs e) {
EventHandler<NewEventArgs> temp = Volatile.Read(ref NewMail);
if (temp != null)
temp(this, e);
} } //回调 internal class Fax { public Fax(MailManager n) { n.NewMail += delegate (object sender, NewEventArgs e) { Console.WriteLine(e.str_name); }; } } MailManager mail = new MailManager(); Fax f = new Fax(mail); mail.Go("接收到新消息");
定义委托程序需要event关键字。EventHandler<T> 表示将处理不包含事件数据的事件的方法。T 数据类型,传递事件源与数据对象。
Volatitle.Read 原子性读取,强迫在调用发生的时候进行读取。避免多线程操作会删除此数据。
泛型:是CLR提供的一种特殊机制,支持另一种形式的代码重用,即算法重用。
泛型的优势如下:
源代码保护:使用泛型算法的开发人员不需要访问算法的源代码。
类型安全:将泛型算法应用于一个具体类型时,只有与指定数据类型兼容的对象才能用算法。
更清晰的代码:在使用的时候,减少了使用强制转换的操作。
更佳的性能:减少装箱操作,将以值类型来进行操作。
建议使用泛型集合类,原因如下
使用非泛型集合类,无法得到类型安全保证。
泛型集合类,性能更加。
委托的每个泛型类型参数都可以标记为协变量或逆变量。泛型类型参数可以是以下任何一种。
不变量:类型参数不能修改。
逆变量:类型参数可以从一个类更改为它的某个派生类,由大变小。使用in关键字标记。
协变量:类型可以从一个类更改为它的某个基类,由小变大。使用out关键字标记。
例如:public delegate TResult Func<in T,out TResult>(T arg);
Func<Object, ArgumentException> fn = null; Func<String,Exception> fn1 = null; fn1 += fn;
fn1 的 逆变让String可以变成 Object,协变让 Exception 可以变成ArgumentException。
泛型类型约束
CLR支持称为约束的机制,作用是限制能指定成泛型实参的类型数量。使用where 关键字来定义。
主要约束:类型参数可以指定零个或一个主要约束。主要约束可以是代表非密封类的一个引用类型。例如:Stream
还有两个特殊的主要约束:class 和 struct。
次要约束:类型参数可以指定零个或多个次要约束。次要约束代表接口类型。例如:IEnumerable
还有一种特殊的类型参数约束:允许指定类型实参当作约束。例如
public static List<TBase> ConverIList<T, TBase>(IList<T> list) where T : TBase{} ConverIList<String, Object>();
其中TBase是T的类型约束。
构造器约束:类型参数可以指定零个或一个构造器约束,必须实现公共无参构造器的非抽象类型。new()
泛型变量无法与其他类型进行显示转换。必须使用 as 操作符,来进行转换。
public static void Go<T>(T obj) { string s = (string)obj; //报错:无法转换 string ss = obj as string; //正确 }
泛型类型无法设置为Null。因为有可能包含值类型,设为null是不可能的。
但可以使用 default 关键字,来设置成默认类型。如果是引用类型则设置成null,如果是值类型则设置成0.
接口
因为CLR不支持多继承,所以提供接口功能来实现 缩水版 的多继承关系。一个类可以继承多个接口。
接口就是对一组方法进行统一命名,方法不会实现。当类继承接口的时候,必须实现对应的方法。
使用 interface 关键字来定义接口。约定接口类型名称以大写字母I开头。
编译器要求将实现接口方法标记为public,CLR要求将接口方法标记为virtual。派生类可以重写。
不显示标记virtual,会自动标记为virtual和sealed。派生类不能重写
派生类不能重写sealed的接口方法。但派生类可以继承同一个接口,并提供对应的实现。在对象上调用接口方法时,会调用对应的实现。
public sealed class Program { static void Main(string[] args) { Base b = new Derived(); b.Dispose(); //Base's Dispose ((IDisposable)b).Dispose(); //Derived's Dispose Console.ReadLine(); } internal class Base : IDisposable { public void Dispose() { Console.WriteLine("Base's Dispose"); } } internal class Derived : Base, IDisposable { new public void Dispose() { Console.WriteLine("Derived's Dispose"); } } }
Base 和 Derved,都继承了IDisposable接口,并实现了不同的方法。当将b显示转换为IDisposable对象的时候,就会调用Derived对应的方法。
new 关键字可重写方法,并替换原先的接口方法。
基类还是接口?:属于 和 能 关系,如果基类和派生类建立不起属于的关系,就不用基类而用接口。
Net Framework中,字符总是表示成 16位的 Unicode代码。每个字符都是 System.Char 结构。
String代表一个不可变的顺序字符集,直接派生Object,是引用类型。
许多编程语言都讲String视为基元类型,不允许使用new关键字从构造对象。必须使用简化语法。
转移机制,不建议直接写 \r \n 这种。相反提供平台敏感的NewLine属性,Environment.NewLine。在任何平台上都能工作。
逐字字符串:采用这种方式,所有字符都被视为字符串的一部分。需要用 @ 来声明同一个字符串。string file @"c:\windows\Notepad.exe";
字符串不可变。字符串一经创建便不能更改,不能变长,变短或修改字符。
但允许在字符串上执行各种操作,而不实际地更改字符串。
由于String类型代表不可变字符串,对字符串的每一个操作,都会分配堆,会在GC中进行回收。FCL提供了StringBuilder类型,对字符串进行高效动态处理。
并可以返回处理好的String对象。StringBuilder就是创建String对象的特殊构造器。
StringBuilder 是可变字符串。大多数成员都可以改变字符串内容。并不会分配堆。
ToString 允许三种格式化输出:G常规,D十进制,G十六进制。
枚举类型定义了一组“符号名称/值”配对。使用enum来标识。例如
public enum Color { White, //0 Red //1 }
枚举类型的优势
更容易编写、阅读、维护。有了枚举类型,符号名称可在代码中随便使用。不用担心含义。一旦对应值改变,代码也可以简单编译,不需要修改源码。
枚举类型是强类型。如传递额外内容,编译器会报错。
枚举类型是从System.ValueType派生,所以是值类型。枚举类型不能定义方法、属性、事件。不过可用扩展方法添加枚举类型。
IsDefined可以判断数值是否包含于枚举类型中
Console.WriteLine(Enum.IsDefined(typeof(Color),"Red"));//True
位标志定义了一组集合。使用enum来标识,并添加 Flags 特性。
public sealed class Program { static void Main(string[] args) { Color col = Color.White | Color.Red; Console.WriteLine(col.ToString());//White, Yello Console.ReadLine(); } [Flags] public enum Color { White = 0x001, Red, Yello = 0x002 } }
如果数值没有对应的符号,ToString方法会监测Flags特性。如果有则会将它视为一组标志。如果没有则会返回数值。
可以定义没有Flags特性的类型,并用F标记ToString则可以获得正确的字符串。
数组:允许将多个数据项作为集合来处理的机制。CLR支持一维、多维、交叉数组,数组均是引用类型。
Double[,] myDoubles = new Double[10, 20]; //二维数组 String[, ,] myStrings = new String[5, 3, 10]; //三维数组 //交叉数组 String[][] myPloygons = new String[3][]; myPloygons[0] = new String[10]; myPloygons[1] = new String[5];
数组初始化
string[] name = { "chenxy", "ccc" }; var names = new[] { new { Name = "Chenxy" }, new { Name = "ccc" } };
数组拷贝
Int32[] ildim = { 1, 2, 3, 4, 5 }; Object[] obldim = new Object[ildim.Length]; Array.Copy(ildim, obldim, ildim.Length);
所有数组都隐式实现IEnumerable,ICollection,IList。
委托
Net Framework提供了称为 委托 的类型安全机制,用于提供回调函数的机制。委托使用delegate来标识。
public sealed class Program { public delegate void Feeback(int value); static void Main(string[] args) { Feeback f1 = new Feeback(o => Console.WriteLine("第一个回调{0}", o)); Feeback f2 = new Feeback(o => Console.WriteLine("第二个回调{0}", o)); f1 += f2; f1(123); Console.ReadLine(); } }
所有操作都是类型安全的。将方法绑定到委托时,都允许引用类型的协变性和逆变性。值类型不允许。
定义委托类型,可以加载参数和返回值类型一样的方法。new Feedback 使用Lambda 表示式,进行匿名方法。
设置好委托链,进行调用即可实现回调函数。
关联两个委托可以使用方法:Delegate.Combine、Delegate.Remove 或者 +=、-+操作符。委托会重载这两个操作符来实现委托链。
委托定义不要太多,因为委托类型的特殊性,如果参数一样的话,不需要定义太多的委托。目前支持的泛型委托有两种。
Action 无返回值可设置参数,目前支持16个泛型参数。
Func 有返回值,可设置参数,目前支持16个泛型参数。
public sealed class Program { static void Main(string[] args) { //Lambda表达式 Action<string, int> ac1 = (o, t) => Console.WriteLine("测试字符串{0},值{1}", o, t); //Linq表达式 Action<string, int> ac2 = delegate(string o, int t) { Console.WriteLine("测试字符串{0},值{1}", o, t); }; //方法 Action<string, int> ac3 = Method1; ac1("陈星宇", 1993); ac2("陈星宇", 1993); ac3("陈星宇", 1993); Func<string, int, string> fc1 = delegate(string o, int t) { string result = string.Format("测试Func,字符串 {0},值 {1}", o, t); return result; }; Console.WriteLine(fc1("陈星宇", 1993)); Console.ReadLine(); } public static void Method1(string o, int t) { Console.WriteLine("测试字符串{0},值{1}", o, t); } }
关于C#委托的简化语法
1.不需要构造委托对象,C#编译器可以自己进行推断方法类型。不需要构造委托对象。例如
public static void Callback() { ThreadPool.QueueUserWorkItem(SomeAsync, 5); //public delegate void WaitCallback(object state) } public static void SomeAsync(Object o) { Console.WriteLine(o); }
QueueUserWorkItem 定义接受的委托对象 是 WaitCallback,可以不用new 一个结构,直接传相同类型的方法即可。
2.不需要定义回调方法,允许以内联的方式写回调方法的代码,不必在他自己的方法中写。
public static void Callback() { ThreadPool.QueueUserWorkItem(obj => Console.WriteLine(obj), 5); //public delegate void WaitCallback(object state) }
如不需要参数的Lambda表达式可以写为:()=>Console.writle(); 也可以定义out/ref参数,(out obj)=>Console...
3.局部变量不需要手动包装到类中即可传回调方法
当委托在单独的一个方法的时候,计算出的变量值传回调用方法是很麻烦的。但是Lambda表达式则可以直接赋值。
public static void Callback() { int numToDo = 10; Int32[] squares = new Int32[numToDo]; AutoResetEvent done = new AutoResetEvent(false); for (int i = 0; i < squares.Length; i++) { ThreadPool.QueueUserWorkItem(obj => { int num = (int)obj; squares[num] = num * num; if (Interlocked.Decrement(ref numToDo) == 0) done.Set(); }, i); } done.WaitOne(); for (int n = 0; n < squares.Length; n++) { Console.WriteLine("Index {0},Square {1}",n,squares[n]); } }
可以在委托中,给squares 赋值。如果是单独的方法,则会很麻烦。
委托和反射
针对不知道回调方法需要多少个参数的这种情况,可以进行动态委托类型的创建。
internal delegate Object Two(Int32 n1, Int32 n2); public sealed class Program { static void Main(string[] args) { string[] arg = { "ConsoleApplication1.Two", "Add", "123", "321" }; //判断委托是不是在类型中存在 Type delType = Type.GetType(arg[0]); if (delType == null) return; //定义一个委托,后期动态创建这个委托 Delegate d; //获取类型中的方法。 MethodInfo mi = typeof(Program).GetTypeInfo().GetDeclaredMethod(arg[1]); //根据方法创建对应的委托对象 d = mi.CreateDelegate(delType); //创建一个对象容器 Object[] callbackArgs = new Object[arg.Length - 2]; for (int a = 2; a < arg.Length; a++) { callbackArgs[a - 2] = int.Parse(arg[a]); } //动态调用方法 Object result = d.DynamicInvoke(callbackArgs); Console.WriteLine(result); Console.ReadLine(); } private static Object Add(Int32 n1, Int32 n2) { return n1 + n2; } }
使用Delegate 用来接受创建的动态委托,根据方法的CreateDelegate来进行创建。最后使用委托的DynamicInvoke来动态调用。
定制特性
特性:可以宣告式的为自己的代码构造添加注解来实现特殊功能。允许为每一个元数据表记录定义和应用信息。
C#允许将特性应用如下的目标元素。
[assembly: SomeAttr] //应用程序集 [module:SomeAttr] //应用模块 namespace DemoArray { [type:SomeAttr] //应用类型 internal sealed class SomeType<[typevar:SomeAttr] T> //应用泛型类型变量 { [field:SomeAttr] //应用字段 public Int32 SomeField = 0; [return:SomeAttr] //应用返回值 [method:SomeAttr] //应用方法 public Int32 SomeMethod( [param:SomeAttr] //应用参数 Int32 SomeParam) { return SomeParam; } [property:SomeAttr] //应用属性 public string SomeProp { [method:SomeAttr] //应用get访问器 get { return null; } } [event:SomeAttr] //应用事件 [field:SomeAttr] //应用编译器生成的字段 [method:SomeAttr] //应用编译器生成的add & remove方法 public event EventHandler SomeEvent; } internal class SomeAttrAttribute : Attribute { } }
特性是一个类型实例,必须直接或间接的从公共抽象类System.Attribute派生。特性必须有公共构造器,才能够创建。下面这个例子
[DllImport("Kernel", CharSet = CharSet.Auto, SetLastError = true)]
参数Kernel,是构造器参数。在特性中称为:定位参数。而且是强制性的,应用特性的时候,必须填写指定参数。
CharSet,SetLastError 用于设置字段或属性的参数。在特性中称为:命名参数。这种参数是可选的。
自定义特性
[AttributeUsage(AttributeTargets.Enum, Inherited = false, AllowMultiple = true)] sealed class FlagsAttribute : Attribute { public FlagsAttribute() { } }
AttributeUsage 可利用它告知编译器定制特性的合法应用范围。例子中,这个特性只能用于Enum。
Inherited 指特性在应用于基类的时候,是否同时应用于派生类和重写的方法。
AllowMultiple 是否能为一个实例,指定多个对象。
检测定制特性类
System.Reflection 提供了三个静态扩展方法来获取与目标相关的特性:IsDefined,GetCustomAttributes,GetCustomAttribute
如果只想判断目标是否应用一个特性,应调用IsDefined。因为它比其他两个更高效。不会构造特性对象,不会调用构造器,也不会设置字段和属性。
要构造特性对象,必须调用GetCustomAttributes 或 GetCustomAttribute方法。每次调用该方法都会构造特性对象并返回。
IsDefined:如果至少有一个指定的特性实例与目标关联,就返回true。因为不构造特性对象,效率很高。
GetCustomAttributes:返回应用目标的指定特性对象集合。如没有就返回空集合。该方法常用于AllowMultiple设为true的特性。
GetCustomAttribute:返回应用于目标的指定特性类的实例。如果没有就返回Null。如果定义多个特性会抛出异常。该方法通常用于AllowMultiple设为false的特性。
将一个类传给这些方法的时候,会监测是否应用了指定的特性类或派生类。如果只是想搜索一个具体的特性类,可以考虑将自己的特性类 sealed。
下面做一个获取特性的例子
[Serializable] public sealed class Program { static void Main(string[] args) { ShowAttributes(typeof(Program)); Console.ReadLine(); } /// <summary> /// 显示特性 /// </summary> /// <param name="attributeTarget">获取成员属性信息。扩展方法之一</param> private static void ShowAttributes(MemberInfo attributeTarget) { var attributes = attributeTarget.GetCustomAttributes<Attribute>();
bool result = attributeTarget.IsDefined(typeof(SerializableAttribute), false); Console.WriteLine("特性应用 {0}:{1}",attributeTarget.Name,(attributes.Count() == 0 ? "None" : String.Empty)); foreach (Attribute attribtue in attributes) { if (attribtue is SerializableAttribute) { Console.WriteLine("Serializable TypeId={0}",((SerializableAttribute)attribtue).TypeId); } } Console.WriteLine(); } }
分析一下:首先GetCustomAttributes扩展方法可以用于。Assembly 程序集,MemberInfo 类型成员,Module 模块成员,ParameteInfo 方法成员。
我们是设置的特性是类型成员,所以ShowAttribtues接受MemberInfo。接下来就显而易见了,遍历集合然后is判断转换,最后显示转换拿属性。搞定!
条件特性类:设置特性只会在指定条件下执行。避免因特性过多造成系统更大,损害系统性能。System.Diagnostics.ConditionalAttribtue
[Conditional("TEST")] #define TEST
如果发现向目标添加此特性,只会在此定义TEST符号的前提下,编译器才会运行特性。#define是C#预处理指令,必须写在页面最上方。
C#预处理指令,可以在编译代码的时候进行预处理操作。包括定义环境,IF判断,抛出异常等等。C# 预处理指令
可空类型:使值类型可以设为null,使用?操作符。例如:Int32? a = null;
空接合操作符:使用??操作符。获取两个操作数,如果左边不为null,就返回这个操作数的值。否则返回右边操作数的值。可以用于引用类型和可空类型。
Int32 b = null ; Int32 x = b ?? 123;
异常和状态管理
Net Framework异常处理机制,是使用Microsoft Windows提供的结构化异常处理(Structured Exception Handling,SEH)机制构建的。
先通过一个例子,来看看异常处理机制的写法。
private void SomeMethod() { try { // 需要检测异常的代码 } catch (InvalidOperationException) { // 从InvalidOperationException恢复的代码放在这里 } catch (IOException) { // 从IOException恢复的代码放在这里 } catch { //除了上面这两个异常,其他所有异常恢复代码 //需要重新抛出异常 throw; } finally { //此处代码总是执行的,不管有没有异常 } }
try:如果代码需要执行一般性的资源清理操作,就放到try块中。try 必须和finally 或 catch 配合使用,不允许单独出现。
catch:包含响应一个异常需要执行的代码。一个try可以关联多个catch块。圆括号中的表达式称为:捕捉类型。必须是从Exception派生的类型。
finally:包含的是保证会执行的代码。一般在此模块中执行资源清理操作。
throw:可以抛出异常位置。当捕捉到异常时,会记录捕捉位置。当throw抛出异常时,CLR会重置异常起点。单独使用throw关键字,会重新抛出相同的错误。
如果方法无法完成任务的时候,就应抛出一个异常。抛出异常需要考虑两个问题:
1.是抛出什么Exception派生类型。应选择一个有意义的类型。可直接使用FCL定义好的类型,也可以自己定义类型,但是必须从Exception中派生。
2.向异常类型传递什么字符串信息。抛出异常的时候,应包含一条字符串信息,说明为什么无法完成任务。
异常使用提供的设计规范
1.善用finally块:先用finally清理已经成功启动的操作,再返回至调用者或者执行finally快之后的代码。使用lock,using,foreach会自动生成try/finally块。
2.不要什么都捕捉:捕捉异常表明你预见到该异常,理解它为什么发生,并知道如何处理它。所以绝对不允许捕捉 Exception 所有异常。
如果吞噬异常而不重新抛出,应用程序会不知道已经出错,还是会继续运行,造成不可预测的结果和潜在的安全隐患。
3.得体的从异常中恢复:由于能预料到这些异常,所以可以写一些代码,允许应用程序从异常中得体的恢复并继续运行。例如:
public string CalculateSpreadsheetCell(Int32 row, Int32 column) { string result; try { result = "";//计算电子表格单元格中的值 } catch (DivideByZeroException) { result = "除以零的值不能显示"; } catch (OverflowException) { result = "计算值过大不能显示"; } return result; }
4.发生不可恢复的异常时回滚部分完成的操作:如出现异常,应用程序应回滚已部分完成的操作,将文件恢复为任何对象序列化之前的状态。例如:
public void SerializeObject(FileStream fs, IFormatter formatter, Object rootObj) { Int64 beforeSerialization = fs.Position; //保存当前流的位置 try { formatter.Serialize(fs, rootObj); //将对象图序列化到文件中 } catch //捕捉所有异常,只有失败的时候才对流进行重置 { fs.Position = beforeSerialization;//任何事情出错,就将文件恢复到一个有效状态。 fs.SetLength(fs.Position); //截断文件 throw; //抛出相同异常 } }
5.隐藏实现细节来维系协定:有时候需要捕捉异常并抛出不同的异常。抛出的异常,应该是具体异常。
未处理异常
CLR调用栈中向上查找与抛出对象相符的catch块。如果没有匹配的话,就发生一个 未处理异常。CLR检测到进程中的任何未处理异常,都会终止进程。
托管堆和垃圾回收
只要写类型安全的代码,应用程序就不可能会出现内存被破坏的情况。
大多数类型都无需资源清理,垃圾回收器会自动释放内存。如果需要尽快清理资源,可以调用Dispose方法。
CLR要求所有对象都从托管堆分配,进程初始化时,CLR划出一个地址空间区域作为托管堆。一个区域被非垃圾对象填满后,CLR会分配更多的区域。
这个过程一直重复,直到整个进程地址空间被占满。32位进程最多分配1.5GB,64位进程最多分配8TB
new操作符会导致CLR执行以下步骤:
1.计算类型的字段所需的字节数
2.加上对象的开销所需的字节数。
3.CLR检测区域中是否有分配对象所需的字节数。分配对象只需在指针上加一个值,速度非常快。
垃圾回收算法(GC)
当调用new关键字的时候,如果没有足够的地址空间来分配该对象。发现空间不够,CLR就执行垃圾回收。
CLR使用引用跟踪算法,此算法只关心引用类型的变量,因为只有这种变量才能引用堆上的对象。值类型变量直接包含值类型实例。
根:我们将引用类型变量称为根。引用类型变量,包括类的静态和实例字段,或者方法的参数和局部变量。
开始GC的时候,首先会暂停进程中的所有线程。防止检查期间访问对象并更改其状态。
然后CLR进入GC的标记阶段。这个阶段CLR遍历堆中的所有对象,将同步快索引字段的一位设为0。表明所有对象都应该删除。
然后CLR检查所有活动根,查看他们引用了那些对象。如果一个根包含null,CLR忽略这个跟并继续检查下个根。
任何跟如果引用了堆上的对象,CLR会标记那个对象设为1。
检查完毕后,已标记的对象不能被垃圾回收,因为至少有一个根在引用它们。所以这种对象是可达的,因为应用程序中不存在使对象能被再次访问的根。
CLR知道标记以后,就会进行压缩阶段。会压缩所有幸存下来的对象,使它们占用连续的内存空间。防止空间碎片化的问题。
CLR的GC是基于代的垃圾回收器,它对你的代码做出以下几点假设
1.对象越新,生存期越短。
2.对象越老,生存期越长。
3.回收堆的一部分,速度快于回收整个堆。
托管堆在初始化时不包含对象。添加到堆的对象称为第0代对象。均为新对象,垃圾回收器从未检查过它们。
CLR初始化第0代对象选择一个预算容量。如果分配一个新对象造成第0代超过预算,就必须启动一次垃圾回收。
垃圾回收过后,第0代就不包含任何对象了。在垃圾回收机制中存活下来的根,就跑到第一代对象里面了。当再次回收的时候,依然还是回收第0代对象。
垃圾回收过程中,会检查第一代的预算容量。如果第一代小于预算容量,就回收第0代。
只有第一代超出预算,才会检查第一代中的对象。如果第一代对象满了,就会自动划分到第二代。一共只有三代,0 1 2
垃圾回收触发条件
检查第0代超过预算时,触发一次GC。
显示调用System.GC 的 Collect方法。
Windows报告低内存情况。
CLR正在卸载AppDomain。
CLR正在关闭。
大对象:超过85000字节的对象,称为大对象。大对象分配不同的地址,并且GC不会进行压缩,大对象总是第2代。
特殊清理类型
CLR提供称为终结的机制,也称为析构方法。允许对象在被判定为垃圾之后,在对象内存被回收之前执行一些代码。任何包装本机资源的类型都支持终结。
internal sealed class SomeType { ~SomeType() { //这里写执行代码 } }
CLR寄宿和AppDomain
寄宿:使任何应用程序都能利用CLR的功能。使现有的应用程序至少能部分使用托管代码编写。托管代码就是托管模块,可以转换为IL。
Microsoft为CLR定义了一个标准的COM接口,并为该接口和COM服务器分配GUID。安装Net Framework时,会在Windows注册表中注册。
AppDomain:CLR COM服务器初始化时会创建一个AppDomain。他是一组逻辑容器(应用程序域)。也是默认的AppDomain只有在Windows进程终止时才会被销毁。
除了默认的AppDomain还可以加载额外的AppDomain。具体功能如下
一个AppDomain的代码不能直接方位另一个AppDomain的代码创建的对象。只能使用“按引用封送”
AppDomain可以卸载,同时卸载包含的程序集
AppDomain可以单独保护,可以保证这些代码不会被破坏。
AppDomain可以单独配置,关联一组配置设置。
这个确实是看不太明白。就不继续完善这个模块的知识点了。
程序集加载和反射
程序集加载:Assembly.Load();导致CLR向程序集应用一个版本绑定重定向策略,并在GAC(全局程序集缓存)中查找程序集。如果没找到,就去应用程序的基目录、私有目录去找。
如果传递的是弱命名程序集,就不会执行重定向策略。如果Load找到指定的程序集,就会返回Assembly对象的引用。
Assembly.LoadFrom 允许传递一个URL作为实参。如果传递的是一个Internet位置,CLR会下载文件。
反射的性能
反射是非常强大的机制,与允许在运行时发现并使用编译时还不了解的类型及其成员。但是也有两个缺点:
反射造成编译时无法保证类型安全。由于反射严重依赖字符串,所以会丧失编译时的类型安全。比如:Type.GetType("int") 反射不认识基元类型,只可以写System.Int32
反射速度慢。使用反射时,类型及其成员的名称在编译时未知。如果用字符串的话,反射机制会不停的执行字符串搜索。
发现程序集中定义的类型
private static void Marshalling() { Assembly a = Assembly.Load(typeof(Program).Assembly.FullName); foreach (Type item in a.ExportedTypes) { Console.WriteLine(item.FullName); } }
构造类型实例
获得Type对象引用后,可以远程构造该类型的实例。FCL提供以下几个方法
1.System.Activator 的 CreateInstance方法:传递一个Type对象引用,也可以传递标识类型的String。返回新对象的引用。
2.System.Activator 的 CreateInstanceFrom方法:必须通过字符串参数来指定类型及其程序集。返回对象引用。
3.System.AppDomain:允许指定那个在AppDomain中构造对象,不过不是托管代码不推荐使用。
4.System.Reflection.ConstructorInfo 的 Invoke实例方法:使用一个Type对象引用,可以绑定到一个特定的构造器。可以调用Invoke方法,返回新对象的引用。
如何查询类型的成员并显示成员信息
字段、构造器、方法、属性、事件和嵌套类型都可以定义成类型的成员。FCL包含抽象基类SYstem.Reflection.MemberInfo 封装了所有类型成员都通用的一组属性。
对每个类型调用DeclaredMembers属性返回派生对象构成的集合。然后显示对应的成员信息。
public sealed class Program { static void Main(string[] args) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly a in assemblies) { Show(0, "Assembly:{0}", a); foreach (Type t in a.ExportedTypes) { Show(1, "Type:{0}", t); foreach (MemberInfo mi in t.GetTypeInfo().DeclaredMembers) { string typeName = string.Empty; if (mi is Type) typeName = "Type"; Show(2,"{0}:{1}",typeName,mi); } } } Console.ReadLine(); } private static void Show(Int32 indent, String format, params Object[] args) { Console.WriteLine(new String(' ', 3 * indent) + format, args); } }
调用类型的成员
发现类型定义的成员后可调用它们。不同的类型成员调用方法也不一致。
FieldInfo 调用GetValue获取字段的值 调用SetValue设置字段的值
ConstructorInfo 调用Invoke构造类型的实例并调用构造器
MethodInfo 调用Invoke来调用类型的方法
PropertyInfo 调用GetValue来调用的属性的get访问器方法 调用SetValue来调用属性的Set访问器方法
EventInfo 调用AddEventHandler来调用事件的add访问器方法 调用RemoveEventHandler来调用事件的remove访问器方法
看一个整体的例子
public sealed class Program { private Int32 m_someField; public override string ToString() { return m_someField.ToString(); } public Int32 SomeProp { get { return m_someField; } set { m_someField = value; } } public static event EventHandler SomeEvent; public Program(ref Int32 x) { x *= 2; } static void Main(string[] args) { #region 反射构造函数 Type ctorArg = Type.GetType("System.Int32&"); //ref 的 类型 ConstructorInfo ctor = typeof(Program).GetTypeInfo().DeclaredConstructors.First(c => c.GetParameters()[0].ParameterType == ctorArg); Object[] arg = new Object[] { 12 }; Object obj = ctor.Invoke(arg); Console.WriteLine("调用构造函数,返回的值:{0}", arg[0]); #endregion #region 读取字段 FieldInfo fi = obj.GetType().GetTypeInfo().GetDeclaredField("m_someField"); fi.SetValue(obj, 33); Console.WriteLine("调用字段 {0}", fi.GetValue(obj)); #endregion #region 调用方法 MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString"); string s = mi.Invoke(obj, null) as string; Console.WriteLine("ToString: {0}", s); #endregion #region 读写属性 PropertyInfo pi = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomeProp"); pi.SetValue(obj, 2, null); Console.WriteLine("属性:{0}", pi.GetValue(obj, null)); #endregion #region 事件添加或删除委托 EventInfo ei = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent"); EventHandler en = delegate(Object sender, EventArgs e) { Console.WriteLine("我是事件"); }; ei.AddEventHandler(obj, en); SomeEvent("", null); #endregion Console.ReadLine(); } }
转换成GetTypeInfo,然后在进行获取对应的成员。
运行时序列化
序列化是将对象或对象图转换成字节流的过程。反序列化是将字节流转换回对象图的过程。
序列化的优势
1.应用程序状态可以轻松保存到磁盘文件或数据库中,并在应用程序下次运行时恢复。
2.一组对象可轻松复制到系统的剪贴板,再粘贴回一个或另一个应用程序。
3.一组对象可克隆并放到一边作为“备份”;与此同时,用户操作一组“主”对象。
4.一组对象可轻松地通过网络发送给另一台机器上运行的过程。
5.可以方便的进行加密和压缩处理。
序列化小例子
public sealed class Program { static void Main(string[] args) { var objectGraph = new List<string> { "A", "B", "C" }; Stream stream = SerializeToMeroy(objectGraph); stream.Position = 0; objectGraph = null; objectGraph = DeserializeFromMeroy(stream) as List<string>; foreach (var item in objectGraph) { Console.WriteLine(item); } Console.ReadLine(); } private static MemoryStream SerializeToMeroy(Object objectGraph) { MemoryStream stream = new MemoryStream(); BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, objectGraph); return stream; } private static Object DeserializeFromMeroy(Stream stream) { BinaryFormatter formatter = new BinaryFormatter(); return formatter.Deserialize(stream); } }
FCL提供两个格式化器:BinaryFormatter、SoapFormatter。序列化对象图只需要调用格式化器的Serialize方法,并向他们传递两样东西。
对流对象的引用,以及对想要序列化的对象图的引用。
流对象标识了序列化好的字节应该放到哪里。他是System.IO.Stream抽象基类派生的任何类型的对象。例如:MemoryStream,FileStream,NetworkStream
对象引用可以是任何东西,可以引用集合,并且可以嵌套引用。
可以通过序列化机制,来进行对象的深拷贝。
private static Object DeepClone(Object original) { using (MemoryStream stream = new MemoryStream()) { BinaryFormatter formatter = new BinaryFormatter(); formatter.Context = new StreamingContext(StreamingContextStates.Clone); formatter.Serialize(stream, original); stream.Position = 0; return formatter.Deserialize(stream); } }
深拷贝与浅拷贝:他俩的区别只是引用类型的处理方式不同。
浅拷贝:只复制对象的基本类型,对象类型,仍属于原来的引用
深拷贝:不紧复制对象的基本类,同时也复制原对象中的对象.就是说完全是新对象产生的
如果需要序列化,必须添加SerializeAttribute特性。这个特性只能应用于引用类型、值类型、枚举类型、委托类型。其中枚举和委托不用显示定义特性。
SerializeAttribute特性,不会被派生类继承。所以父类标记序列化,子类不适用。如果子类序列化,父类没有序列化,也不能进行。
一般建议将大多数类型都设置可序列化,但因为序列化会读取私有对象。如果类型包含敏感或安全数据,就不应使类型变得可序列化。
NonSerializedAttribute 可以指出类型中不应序列化的字段,该特性只能应用于类型中的字段,并且会被派生类继承。
OnDeserializedAttribute 每次反序列化后,都会调用标记的方法。
OnDeserializingAttribtue 每次反序列化时,都会调用标记的方法。
OnSerializedAttribute 每次序列化后,都会调用标记的方法。
OnSerializingAttribute 每次序列化时,都会调用标记的方法。
使用这四个方法的时候,必须获取一个StreamingContext 流上下文。并返回void,方法名随意。应将方法声明private,以免被普通方法调用。
OptionalFieldAttribute 新增字段使用此特性,可以绕过反序列化时因字段数量不同的异常。
StreamingContext 流上下文
一个对象可能需要知道它要在什么地方反序列化。就可以使用流上下文,结构包含一组位标志、一个对象引用。位标志指明要序列化/反序列化的来源。
BinaryFormatter和SoapFormatter定义了Streaming类型的属性Context。可以设置额外引用对象,例如深拷贝,就是使用流上下文来通知序列化器的。
使用线程的理由
1.可响应性:Windows为每个进程提供它自己的线程,确保发生死循环的应用程序不会影响到其他应用程序。在图形管理界面中,可以将工作交给一个线程,又不影响UI。
2.性能:由于Windows每个CPU调度一个线程,多个CPU会并发执行这些线程,所以同时执行多个操作能提升性能。只有多CPU才能得到提升。
每个线程都分配了从0(最低)到31(最高)的优先级。系统决定为CPU分配那个线程时,以一种轮流方式调度他们。优先级高的会先调度。
直到没有最高优先级的线程,才会往下进行调度。这种情况称为 饥饿。较高的优先级占用了太多的CPU时间,造成较低的线程无法运行。
应用程序可以改变相对线程的优先级,向Thread的Priority属性传递ThreadPriority枚举类型定义的5个值之一。Lowest,BelowNormal,Normal,AboveNormal,Highest。
前台线程和后台线程
CLR将线程视为前台线程或后台线程。当一个进程的所有前台线程停止运行时,会强制终止扔在运行的任何后台线程。并不会抛出异常。
每个AppDomain都可以运行一个单独的应用程序,而每个应用程序都有自己的前台线程。如果程序退出,造成他的前台线程终止,则CLR仍需保持活动并运行,使其他应用程序退出。
小例子:区分前台线程和后台线程。前台线程停止,则后台线程会强制停止。
public sealed class Program { static void Main(string[] args) { Thread t = new Thread(Worker); t.IsBackground = true; t.Start(); Console.WriteLine("结束"); } private static void Worker() { Thread.Sleep(1000); Console.WriteLine("只有前台线程才能输出"); Console.ReadLine(); } }
在线程的生存期中,任何时候都可以从前台线程转变成后台线程。尽量避免使用前台线程,不然会造成进程一直不终止。
线程是非常宝贵的资源,最好的使用方法是CLR线程池。线程池自动为你管理线程的创建和销毁。线程池创建的线程将为各种任务重用,你的应用程序只需要几个线程就可以完成全部工作。
创建和销毁线程是一个昂贵的操作,要消耗大量时间。太多的线程也会浪费内存资源。操作系统必须调度可运行的线程并执行上下文切换,所以太多线程还对性能不理。
为了改善这种情况,CLR包含了代码来管理它自己的线程池,是你的应用程序能够使用的线程集合。线程池由CLR控制的所有AppDomain共享。每个CLR一个线程池。
CLR初始化时,线程池中是没有线程的。会在内部维护一个操作请求队列。执行异步操作时,就调用某个方法,将记录项追加到线程池的队列中。如果线程池中没有线程,就创建一个新线程。
创建新线程会造成一定的性能损失,然而当线程完成任务后,线程不会被销毁。会返回线程池中,在哪里进入空闲状态,等待响应的另一个请求。由于不销毁自身,所以不再产生额外的性能损失。
如果你的应用程序发出请求的速度超过了线程池线程处理它们的速度,就会创建额外的线程。当线程池空闲时,会自动释放资源。因为是空闲时间释放,所以对自己性能损失不大。
将异步操作放入线程池,可以调用ThreadPool类定义的QueueUserWorkItem方法。他会向线程池添加一个工作项。工作项就是回调方法,必须匹配WaitCallback委托类型。
小例子,演示如何让一个线程池以异步的方式调用一个方法。
ThreadPool.QueueUserWorkItem(Worker, 5); private static void Worker(Object state) { Console.WriteLine("线程池输出参数:{0}", state); Thread.Sleep(1000); }
执行上下文
每个线程都关联了一个执行上下文的数据结构。执行上下文包括的东西有安全设置、宿主身份、逻辑调用上下文数据。线程执行他的代码时,一些操作会受到线程执行上下文设置的影响。
理想情况下,当一个线程使用另一个线程执行任务时,前者的执行上下文应流向后者。这就确保了后者执行的任何操作都是相同的安全设置和宿主设置。
默认情况下,CLR自动造成初始线程执行上下文流向任何线程。会自动传给辅助线程。这会对性能造成一定影响,因为执行上下文包含大量信息,收集、复制到辅助线程,耗费不少时间。
ExecutionContext类,允许你控制线程的执行上下文,如何从一个线程流向另一个线程。可以阻止上下文流动,提高性能。
小例子,演示如何阻止执行上下文流动。使用CallContext程序集。
static void Main(string[] args) { CallContext.LogicalSetData("Name","Jeffrey"); ThreadPool.QueueUserWorkItem(state => Console.WriteLine("Name={0}",CallContext.LogicalGetData("Name"))); ExecutionContext.SuppressFlow(); ThreadPool.QueueUserWorkItem(state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); ExecutionContext.RestoreFlow(); Console.ReadLine(); }
协作式取消和超时
FCL提供了标准的取消模式操作。这个模式是协作式的,意味着要取消的操作必须显示支持取消。对于长时间运行的计算限制操作,支持取消是一件很棒的事情。
取消操作首先需要创建一个 CancellationTokenSource对象。这个对象包含了和管理取消有关的所有状态。
构造好以后,可从它的Token属性获得一个或多个CancellationToKen实例。并传给你的操作,使操作可以取消。
小例子,进行计数操作,手动停止
public sealed class Program { static void Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000)); Console.ReadLine(); cts.Cancel(); Console.ReadLine(); } private static void Count(CancellationToken token, Int32 countTo) { for (int count = 0; count < countTo; count++) { if (token.IsCancellationRequested) { Console.WriteLine("结束"); break; } Console.WriteLine(count); Thread.Sleep(1000); } } }
可调用CancellationTokenSource的Register方法,登记一个或多个在取消时调用的方法。
static void Main(string[] args) { var cts1 = new CancellationTokenSource(); cts1.Token.Register(()=>Console.WriteLine("取消时会调用此方法")); cts1.CancelAfter(10000); Console.ReadLine(); }
CancelAfter可以在指定时间后进行取消操作。
线程池的方法有很多技术限制。最大的问题是没有机制让你知道操作在什么时候完成,也没有机制在操作完成时获得返回值。为了克服这些限制,引入了 任务 的概念。
使用任务做相同的事情
ThreadPool.QueueUserWorkItem(Worker, 5); new Task(Worker, 5).Start(); Task.Run(() => Worker(5));
调用Task构造器需要传一个Action或Action<Object>委托,调用Run时可以传递一个Action或Func<T>委托。不管是构造器还是Run方法,都可以传递取消任务。
还可以向构造器传递一些 TaskCreationOptions 标志来控制 Task 的执行方式。枚举中定义的部分是提议,部分内容总会被采纳。
等待任务返回结果
class Program { static void Main(string[] args) { Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 1000); t.Start(); t.Wait(); Console.WriteLine("The Sum is : " + t.Result); Console.Read(); } private static Int32 Sum(Int32 n) { Int32 sum = 0; for (; n > 0; n--) { checked { sum += n; } } return sum; } }
线程调用Wait方法时,系统检查线程要等待的Task是否开始执行。如果开始会阻塞线程,直到Task结束。
如果Task还没有执行,系统可能使用调用Wait的线程来执行Task。
除了单个等待这个方法以外,Task还提供了两个静态方法。
WaitAny 方法允许等待一个Task对象数组,直到数组中任何的Task对象完成。方法返回数组索引,指明完成的是那个Task对象。超时返回-1;
WaitAll 方法允许等待一个Task对象数组,直到所有Task对象都完成,WaitAll 返回true,超时则返回false。
取消任务
使用CancellationTokenSource 取消Task。例子:
class Program { static void Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); Task<int> t = Task.Run(() => Sum(cts.Token, 100000), cts.Token); cts.Cancel(); try { Console.WriteLine("The Sum is:" + t.Result); } catch (AggregateException x) { x.Handle(e => e is OperationCanceledException); Console.WriteLine("Sum was Canceled"); } Console.Read(); } private static Int32 Sum(CancellationToken ct, Int32 n) { Int32 sum = 0; for (; n > 0; n--) { ct.ThrowIfCancellationRequested(); checked { sum += n; } } return sum; } }
ct.ThrowIfCancellationRequested(); 定时检查操作是否已取消。如果取消了,就会抛出OperationCanceledException。
因为任务有办法表示完成,任务能返回一个值。所以需要采取一种方式将已完成的任务和出错的任务区分开。
任务取消的话,首先会抛出AggregateException 这个异常,所以先捕获这个异常,然后将任何Operation.Exception对象都视为已处理
创建Task构造器的时候,将CancellationToken传给构造器,进行两者关联。
如果在Task开始前取消,会抛出异常。如果已经开始,那么只有显示支持取消才能够在执行期间取消。
Task对象关联的取消任务,在Task构造委托Action中,没有办法拿到。为此,最简单的办法就是使用Lambda表达式,作为闭包变量传递,如例。
任务完成时自动启动新任务
使用Task的ContinueWith方法,可以在任务完成时启动另一个任务。他不会阻塞任何线程:
static void Main(string[] args) { Task<int> t = Task.Run(() => Sum(CancellationToken.None, 10000)); Task cwt = t.ContinueWith(task => Console.WriteLine(t.Result)); Console.Read(); }
使用TaskContinuationOptions枚举值,指定后续任务执行状态。例如:指定只有在第一个任务抛出未处理异常的时候才执行、只有顺利完成的时候才执行。
默认情况下,如果不指定状态标志,则新任务无论如何都会执行。
任务父子关系
任务可以创建父子关系,父任务创建多个Task对象,除非所有子任务结束运行,否则父任务不认为已经结束。
static void Main(string[] args) { Task<Int32[]> parent = new Task<int[]>(()=> { var result = new Int32[3]; new Task(()=>result[0] = Sum(100),TaskCreationOptions.AttachedToParent).Start(); new Task(() => result[1] = Sum(200), TaskCreationOptions.AttachedToParent).Start(); new Task(() => result[2] = Sum(300), TaskCreationOptions.AttachedToParent).Start(); return result; }); parent.Start(); var cwt = parent.ContinueWith(p=>Array.ForEach(parent.Result,Console.WriteLine)); Console.Read(); }
使用AttachedToParent进行关联操作,将延续任务制定成子任务。
Task.TaskStatus 值,制定了其生存期的状态。首次构造对象,它的状态是Created,任务启动时,它的状态为WaitingToRun等等
为了简化生存期状态,Task提供了几个Boolean属性,用来判断状态。例如:IsCanceled,IsFaulted。
例如判断一个Task是否成功完成,最简单的办法是:if(task.Status == TaskStatus.RunToCompletion){...}
有时需要创建一组共享相同配置的Task对象,为了避免将相同的参数传给每个Task的构造器。可创建一个任务工厂来封装通用的配置。
Tasks 命名空间定义了一个 TaskFactory 类型和 TaskFactory<T> 要向任务工厂传递希望任务具有的CancellationToKen取消、TaskScheduler队列、TaskCreationOptions、TaskContinuationOptions
下面例子演示使用任务工厂
static void Main(string[] args) { Task parent = new Task(() => { var cts = new CancellationTokenSource(); var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); var childTasks = new[] { tf.StartNew(()=>Sum(cts.Token,1000)), tf.StartNew(()=>Sum(cts.Token,2000)), tf.StartNew(()=>Sum(cts.Token,int.MaxValue)) }; //任务抛出异常,取消其余子任务 for (int task = 0; task < childTasks.Length; task++) { childTasks[task].ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted); } //从为出错/未取消的任务中,将返回的最大值用另一个任务来进行显示 tf.ContinueWhenAll( childTasks, completedTasks => completedTasks.Where(t => !t.IsFaulted && !t.IsCanceled).Max(t => t.Result), CancellationToken.None) .ContinueWith(t => Console.WriteLine("max is {0}", t.Result),TaskContinuationOptions.ExecuteSynchronously); }); //子任务完成后,显示任何未处理的异常 parent.ContinueWith(p => { StringBuilder sb = new StringBuilder("The following :" + Environment.NewLine); foreach (var e in p.Exception.Flatten().InnerExceptions) { sb.AppendLine(" " + e.GetType().ToString()); } Console.WriteLine(sb.ToString()); }, TaskContinuationOptions.OnlyOnFaulted); parent.Start(); Console.Read(); }
创建一个任务工厂,该工厂创建了三个Task对象。他们都共享取消标记。都被视为其父任务的子任务,创建的所有任务都以同步的方式执行。都使用默认的TaskScheduler
所有子任务都调用TaskFactory的StartNew方法来创建,使用该方法可以很方便地创建并启动子任务。
通过一个循环告诉每个子任务,如果抛出未处理的异常,就取消其他仍在运行的所有子任务。
在TaskFactory上调用ContinueWhenAll,创建所有子任务完成后启动的一个任务,传递一个CancellationToken.None使任务不能取消。
处理完所有结果以后,返回最大值。
性能的提升
一些常见的编程情形可通过任务提高性能。为简化编程,Tasks.Parallel类封装了这些情形,它内部使用Task对象。主要针对循环和并行执行进行简化。
例如,不要像下面这样处理集合中的所有项:
for(int i=0;i<1000;i++) Dowork(i); //每次循环调用一次Dowork
使用Parallel类的For方法,采取多线程辅助完成
Parallel.For(0, 50, i => Dowork(i));
foreach 同理
IEnumerable<int> collection = new List<int> { }; Parallel.ForEach(collection, i => Console.WriteLine(i));
并行执行多个方法
Parallel.Invoke( () => { Thread.Sleep(1000); Console.WriteLine(123); }, () => { Thread.Sleep(1000); Console.WriteLine(123); }, () => { Thread.Sleep(1000); Console.WriteLine(123); } );
Parallel的所有方法都让调用线程参与处理。但是并不是所有的for都要替换成Parallel.For。前提条件是:工作项必须能并行执行,如果需要顺序执行,就不要使用这个方法。
另外,要避免会修改任何共享数据的工作项,否则多个线程同时处理可能会损坏数据。虽然可以添加线程同步锁,但这样就只有一个线程访问数据,无法带来并行的好处。
定时操作类
Threading 命名空间定义了要给 Timer类,可用它让一个线程池线程定时调用一个方法。
也可以使用Task的静态Delay方法。例子
private static async void Status() { while (true) { Console.WriteLine(DateTime.Now); await Task.Delay(2000); } } Status();
PLINQ 多线程并发的集合遍历查询
internal class Program { private static void Main(string[] args) { List<int> list = new List<int> { 0, 1, 2, 3, 4, 5, 6 }; var result = from p in list.AsParallel() select p; foreach (var item in result) { Console.WriteLine(item); } Console.Read(); } }
使用多线程进行查询,输出的结果是杂乱无章的。所以如果不需要顺序输出,则使用PLINQ代替LINQ性能会更佳。
C#的异步函数
它允许使用少量线程执行大量操作。与线程池结合,异步操作允许利用机器中所有的CPU。
一旦方法标记为async,编译器就会将方法的代码转换成实现了状态机的一个类型。这就允许线程执行状态机中的一些代码并返回,方法不需要一直执行到结束。
使用await操作符会在Task对象上调用 ContinueWith,向它传递用于恢复状态机的方法。
异步函数存在以下限制
1.不能将应用程序的Main方法转变成异步函数。另外构造器、属性访问器、事件访问器不能转变成异步
函数。
2.异步函数不能使用任何out 或 ref 参数
3.不能在catch,finally或unsafe快中使用 await操作符
4.不能在await操作符之前获得一个支持线程所有权或递归的锁,并在await操作符之后释放它。因为await之前的代码由一个线程执行,之后的代码则可能由另一个线程执行。在C#lock语句中使用await,编译器会报错。
5.在查询表达式中,await操作符只能在初始from子句的第一个集合表达式中使用,或在join子句集合表达式中使用。
FCL中许多类型都支持异步函数。规范要求为方法名附加Async后缀。在FCL中,支持I/O操作的许多类型都提供了xxxAsync方法。
有时异步函数需要执行密集的、计算限制的处理,再发起异步操作。如果通过GUI线程来调用函数,UI就会突然失去响应,好长时间才能恢复。
另外如果操作以同步方式完成,那么UI失去显影的时间还会更长。在这种情况下,可以利用Task的静态Run方法从其他线程中执行异步函数。
static void Main(string[] args) { Task.Run(async () => { await Status(); }); Console.Read(); } private static async Task Status() { while (true) { Console.WriteLine(DateTime.Now); await Task.Delay(2000); } }
msdn关于线程同步
在应用程序中使用多线程的好处是每个线程都可以异步执行,对于Windows应用程序,耗时的任务可以在后台执行。而使应用程序窗口和控件保持响应。
然而,线程的异步特性意味着必须协调对资源(文件、网络连接、内存)的访问。否则,两个或更多的线程可能在同一时间访问相同的资源,而每个线程都不知道其他线程的操作。结果将产生不可预知的数据破坏。
对于整数数据类型的简单操作,可以用Interlocked类的成员来实现线程同步。
对于其他所有数据类型和非线程安全的资源,只有使用lock 结构才能安全的执行多线程处理。
lock 语句
可以用来确保代码块完成运行,而不会被其他线程中断。这是通过在代码块运行期间为给定对象获取互斥锁来实现的。
lock有一个作为参数的对象,在该参数后面还有一个一次只能由一个线程执行的代码块。例如:
internal class Program { private static object lockThis = new object(); private static void Main(string[] args) { lock (lockThis) { Console.WriteLine("这里执行一些操作"); } Console.Read(); } }
提供给lock关键字的参数必须为基于引用类型的对象,该对象用来定义锁的范围。上面的例子中,锁的范围是这个方法,因为函数外不存在任何对对象lockThis的引用。
如果确实存在此类引用,锁的范围将扩展到该对象。严格地说,提供的对象只是用来唯一地标识多个线程共享的资源,可以是任何类的实例。
lock关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。
lock关键字可确保当一个线程位于代码的临界区时,另一个线程不会进入该临界区。如果其他线程尝试进入锁定的代码,则它将一直等待,直到该对象被释放。
lock关键字在快开始的时候调用Enter,在快结尾的时候调用Exit。
如果一个容器对象被多个线程使用,则可以将该容器传递给lock,而lock后面的同步代码将访问该容器。
只要其他线程在访问容器前先锁定该容器,则对该对象的访问将是安全同步的。应避免锁定public类型,否则实例将超出代码的控制范围。
最佳的做法是定义private对象来锁定,或 private static 对象变量来保护所有实例所共有的数据。
如果该实例可以被公开访问,如 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一个对象。
出于同样的原因,锁定公共数据类型也可能导致问题。锁定字符串尤其危险,因为字符串被CLR暂留。这意味着整个程序中任何给定字符串都只有一个实例。
就是这同一个对象表示了所有运行的应用程序域所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置锁,就将锁定应用程序中该字符串的所有实例。
最好锁定不会被暂留的私有或受保护成员。某些类提供专门用于锁定的成员。
使用lock关键字通常比使用 Monitor 类更可取,一方面因为更简洁,另一个方面lock确保了即使受保护代码引发异常,也可以释放基础监视器。
但使用lock同步应该注意一下五点:
1.同步对象在需要同步的多线程中是可见的同一个对象
2.在非静态方法中,静态变量不应作为同步对象
3.值类型对象不能作为同步对象
4.避免对字符串作为同步对象
5.降低同步对象的可见性
看一个稍微正经一点的例子:开10个线程,并且让他们做一个循环计算,让每个循环暂停5毫米用于测试循环中破坏资源。每个线程都访问一个共享资源,并且会改变这个资源。
internal class Program { public static long total = 0; private static object lockThis = new object(); private static void Main(string[] args) { Stopwatch stop = new Stopwatch(); stop.Start(); Program pro = new Program(); Task parent = new Task (() => { for (int i = 0; i <= 10; i++) { new Task(obj => { pro.add(obj); }, (i + 1), TaskCreationOptions.AttachedToParent).Start(); } }); parent.Start(); parent.ContinueWith(p => { stop.Stop(); Console.WriteLine(stop.Elapsed); }); Console.Read(); } public void add(object text) { total = 0; for (int i = 0; i < 50; i++) { total++; Thread.Sleep(5); } Console.WriteLine("第{0}个线程:" + total, text); } }
PS:因为想计算加lock和不加lock的时间,使用线程池的话,不知道何时完成,所以在这里使用Task异步。看一下运行结果程序
执行2.49秒。我对同样的数据,进行同样的计算,但是得到不同的结果。是因为有其他线程破坏了 total 变量。导致最后输出有问题。
那么我们使用 lock 锁,改成下面的样子。
public void add(object text) { lock (lockThis) { total = 0; for (int i = 0; i < 50; i++) { total++; Thread.Sleep(5); } Console.WriteLine("第{0}个线程:" + total, text); } }
这个时候,我们把total锁定,只有一个线程可以访问。再看一下结果。
线程同步了,但是加锁的程序比不加锁的程序,慢了7倍。所以,程序中是否需要加锁要慎重的考虑。
与锁相比的另一个等待机制,就是信号同步。
信号同步机制中涉及的类型都继承自抽象类 WaitHandle。这些类型有 EventWaitHandle(AutoResetEvent,ManualResetEvent)、Semaphore、Mutex
AutoResetEvent:通知正在等待的线程已发生事件。他们都继承WaitHandle,维护一个由内核产生的布尔对象(阻滞状态),如果为false,那么在它上面等待的线程就阻塞。
可以调用类型的Set方法将其值设置为true,解除阻塞。下面用一个简单的例子,来演示信号同步
internal class Program { private static void Main(string[] args) { AutoResetEvent auto = new AutoResetEvent(false); Thread tWork = new Thread(() => { Console.WriteLine("此时,开始进入等待状态"); auto.WaitOne(); //阻塞线程 Console.WriteLine("我结束等待状态了"); }); tWork.Start(); Console.Read(); auto.Set(); //我发送结束阻塞 Console.Read(); } }
ManualResetEvent 与 AutoResetEvent的区别是:后者在发送信号完毕后(调用Set方法)会自动将自己的阻滞状态设置为false。而前者需要进行手动设定。
例如,如果同时两个线程进行阻塞状态,AutoResetEvent 在发送信号后,会自动设置为false。所以只会有一个线程收到,另一个线程不会受到。
这种时候,使用 ManualResetEvent 就可以实现。
internal class Program { private static void Main(string[] args) { ManualResetEvent auto = new ManualResetEvent(false); Thread tWork = new Thread(() => { Console.WriteLine("此时,开始进入等待状态"); auto.WaitOne(); //阻塞线程 Console.WriteLine("我结束等待状态了"); }); Thread tWork1 = new Thread(() => { Console.WriteLine("1.此时,开始进入等待状态"); auto.WaitOne(); //阻塞线程 Console.WriteLine("1.我结束等待状态了"); }); tWork.Start(); tWork1.Start(); Console.Read(); auto.Set(); //我发送结束阻塞 } }
信号同步机制,也可以进行模拟心跳操作。
internal class Program { private static void Main(string[] args) { AutoResetEvent auto = new AutoResetEvent(false); Thread tWork = new Thread(() => { while (true) { bool re = auto.WaitOne(3000); if (!re) { Console.WriteLine("没有收到心跳"); } } }); tWork.IsBackground = true; tWork.Start(); Console.Read(); auto.Set(); //我发送结束阻塞 } }
多个线程同时访问共享数据时,线程同步能防止数据损坏。如果一些数据由两个线程访问,但不可能同我们时接触到数据,就完全用不到线程同步。
不需要线程同步是最理想的情况,因为线程同步存在许多问题:
1.它比较繁琐,而且很容易写错。
2.它们会损害性能,获取和释放锁是需要时间的。
3.它们一次只允许一个线程访问资源。阻塞一个线程会造成更多的线程被创建。进一步损害性能。
线程同步是一个很不好的事情,应该尽可能的避免进行线程同步操作。
FCL保证所有的静态方法都是线程安全的。这意味着加入两个线程同时调用一个静态方法,不会发生数据被破坏的情况。
线程安全的方法意味着何在两个线程试图同时访问数据时,数据不会被破坏。例如:Math有一个静态的Max方法
public static Int32 Max(Int32 vall, Int32 val2) { return(vall < val2) ?val2 : vall; }
这个方法是线程安全的,即使它没有获取任何锁。Int32是值类型,传给Max的两个Int32值会复制到方法内部。多个线程可以同时调用Max方法,互不干扰。
FCL不保证实例方法是线程安全的。假如每个实例方法都需要获取和释放一个锁,那么会造成在最终时刻你的应用程序只有一个线程在运行。
基元用户模式和内核模式构造基元是指可以在代码中使用的最简单的构造。基元模式的构造有两种:用户模式和内核模式。
应当尽量使用用户模式构造,它们的速度要明显快于内核模式的构造。因为是使用特殊的CPU命令来协助线程。这意味着协调是在硬件中发生的。但也意味着操作系统检测不到线程在用户模式上阻塞。不会认为阻塞,而且CPU指令只阻塞线程相当短的时间。
缺点是:只有Windows操作系统内核才能停止一个线程的运行。用户模式中运行的线程可能被系统抢占,但线程会以最快的速度再次调度。所以,想要取得资源但暂时取不到的线程会一直自旋。这会浪费大量CPU时间。
内核模式的构造是由Windows操作系统自身提供的。所以,它们要求在应用程序的线程中调用由操作系统内核实现的函数。
他有一个重要的优点,线程通过内核模式的构造获取其他线程拥有的资源时,Windows会阻塞线程以避免浪费CPU时间。当资源变得可用时,会恢复线程,允许它访问资源。
对于在构造上等待的线程,如果拥有这个构造的线程一直不释放它,前者就可能一直阻塞。
如果用户模式构造,线程将一直在一个CPU上运行,我们称为 活锁
如果内核模式构造,线程将一直阻塞,我们称为 死锁
针对阻塞而言,死锁 优于 活锁,因为活锁会浪费CPU时间和内存,死锁只浪费内存
理想状态下,应该结合着两个锁的长处。没有竞争的情况下,构造不会阻塞。存在竞争的情况下,希望被操作系统内核阻塞。这种构造称为:混合构造。
CLR保证以下数据类型的变量读写是原子性的:Boolean,Char,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single以及引用类型。这意味着变量中的所有字段都一次性读取或写入。
原子性:所有字段都一次性读取或写入。例如:当一个线程改变一个变量时,变量会一次性(原子性)的改变。另一个线程不可能看到处于中间状态的值。
两种基元用户模式线程同步构造
易变构造:在特定的时间,在包含一个简单数据类型的变量上执行原子性的读或写操作。
互锁构造:在特定的时间,在包含一个简单数据类型的变量上执行原子性的读和写操作。
编译器会将高级构造转换成低级构造。C#编译器将你的C#构造转换成中间语音(IL),然后JIT将IL转换成本机CPU指令,然后由CPU亲自处理这些指令。
在转换过程中,C#编译器、JIT编译器,甚至CPU自身都可以优化你的代码。例如如下代码,就会被优化掉。
internal class Program { private static void Main(string[] args) { int value = (1 * 100) - (50 - 2); for (int i = 0; i < value; i++) { Console.WriteLine("这里永远不会被执行,所以会被编译器优化掉"); } Console.Read(); } }
Volatitle类提供了两个静态方法,会禁止C#编译器、JIT编译器和CPU平常执行的一些优化。
Volatitle.Write 方法强迫location 中的值在调用时写入。此外,按照编码顺序,之前的加载和存储操作必须在调用Volatitle.Write 之前发生。
Volatitle.Read 方法强迫location 中的值在调用时读取。此外,按照编码顺序,之后的加载和存储操作必须在调用Volatitle.Read 之后发生。
总结:当线程通过共享内存相互通信时,调用Volatitle.Writel 来写入最后一个值,调用 Volatitle.Read 来读取第一个值。
internal sealed class ThreadsSharingData { private int m_flag = 0; private int m_value = 0; public void Thread1() { m_value = 5; Volatile.Write(ref m_flag, 1); } public void Thread2() { if (Volatile.Read(ref m_flag) == 1) { Console.WriteLine(m_value); } } }
例如此例,Writel 写入最后一个值,Read 读取 第一个值。分析一下这段代码。
Volatitle.Write 调用确保在它之前的所有写入。调用之前的写入可能被优化成以任意顺序执行。
Volatitle.Read 调用确保在它之后的所有读取。调用之后的读取可能被优化成以任何顺序执行。
volatile 关键字,可以应用任何类型的静态或实例字段。JIT编译器确保对易变字段的所有访问都是以一边读取或写入的方法执行。
Interlocked 每个方法都执行一次原子读取以及写入操作。调用某个Interlocked方法之前的任何变量都卸载这个Interlocked方法调用之前执行,读取则在调用之后读取。
internal sealed class ThreadsSharingData { private int m_flag = 0; private int m_value = 0; public void Thread1() { m_value = 5; Interlocked.Add(ref m_flag, 1); } public void Thread2() { if (Interlocked.Decrement(ref m_flag) == 0) { Console.WriteLine(m_value); } } }
自旋锁 可以在某种情况下,阻止所有线程,只允许其中一个对字段进行操作。例如
internal struct SimpleSpinLock { private Int32 m_ResourceInUse; public void Enter() { while (true) { if (Interlocked.Exchange(ref m_ResourceInUse, 1) == 0) return; Thread.Sleep(10000); } } public void Leave() { Volatile.Write(ref m_ResourceInUse, 0); } } m_sl.Enter(); //不会同时访问这里 Console.WriteLine("这里不会同时被访问到"); m_sl.Leave();
使用while来进行一个自旋操作。并且使用m_resourceInUse进行标注,使用过变成1,未使用变成0。这样可以确保只有一个线程会访问到一个资源。
但是这种自旋锁会浪费CPU时间,阻止CPU做其他更有用的工作。