CSharp基础-LINQ标准查询运算符

  标准查询运算符是组成 LINQ 模式的方法。 这些方法中的大多数都作用于序列;其中序列指其类型实现 IEnumerable<T> 接口或 IQueryable<T> 接口的对象。 标准查询运算符提供包括筛选、投影、聚合、排序等在内的查询功能。

查询表达式语法

参考:

标准查询运算符的查询表达式语法

按执行方式的分类

参考:

标准查询运算符按执行方式的分类

对数据排序

方法名 描述 C# 查询表达式语法
OrderBy 按升序对值排序。 orderby
OrderByDescending 按降序对值排序。 orderby … descending
ThenBy 按升序执行次要排序。 orderby …, …
ThenByDescending 按降序执行次要排序。 orderby …, … descending
Reverse 反转集合中元素的顺序。 不适用。

参考:

对数据排序

集运算

参考:

集运算

筛选数据

方法名 描述 C# 查询表达式语法
OfType 根据其转换为特定类型的能力选择值。 不适用。
Where 选择基于谓词函数的值。 where

参考:

筛选数据

限定符运算

参考:

限定符运算

投影运算

方法名 描述 C# 查询表达式语法
选择 投影基于转换函数的值。 select
SelectMany 投影基于转换函数的值序列,然后将它们展平为一个序列。 使用多个 from 子句

参考:

投影运算

数据分区

参考:

数据分区

联接运算

方法名 描述 C# 查询表达式语法
联接 根据键选择器函数联接两个序列并提取值对。 join … in … on … equals …
GroupJoin 根据键选择器函数联接两个序列,并对每个元素的结果匹配项进行分组。 join … in … on … equals … into …

参考:

联接运算

对数据分组

方法名 描述 C# 查询表达式语法
GroupBy 对共享通用属性的元素进行分组。
每组由一个 IGrouping<TKey,TElement> 对象表示。
group … by 或 group … by … into …
ToLookup 将元素插入基于键选择器函数的
Lookup<TKey,TElement>(一种一对多字典)。
不适用。

参考:

对数据分组

生成运算

参考:

生成运算

相等运算

参考:

相等运算

元素运算

参考:

元素运算

转换数据类型

方法名 描述 C# 查询表达式语法
AsEnumerable 返回类型化为 IEnumerable 的输入。 不适用。
AsQueryable 将(泛型)IEnumerable 转换为(泛型)IQueryable。 不适用。
Cast 将集合中的元素转换为指定类型。 使用显式类型化的范围变量。
例如:from string str in words
OfType 根据其转换为指定类型的能力筛选值。 不适用。
ToArray 将集合转换为数组。 此方法强制执行查询。 不适用。
ToDictionary 根据键选择器函数将元素放入 Dictionary<TKey,TValue>。
此方法强制执行查询。
不适用。
ToList 将集合转换为 List。 此方法强制执行查询。 不适用。
ToLookup 根据键选择器函数将元素放入 Lookup<TKey,TElement>(一对多字典)。 此方法强制执行查询。 不适用。

参考:

转换数据类型

串联运算

参考:

串联运算

聚合运算

方法名 描述 C# 查询表达式语法 详细信息
聚合 对集合的值执行自定义聚合运算。 不适用。 Enumerable.Aggregate
Queryable.Aggregate
平均值 计算值集合的平均值。 不适用。 Enumerable.Average
Queryable.Average
计数 对集合中元素计数,可选择仅对满足
谓词函数的元素计数。
不适用。 Enumerable.Count
Queryable.Count
LongCount 对大型集合中元素计数,可选择仅对
满足谓词函数的元素计数。
不适用。 Enumerable.LongCount
Queryable.LongCount
最大值 确定集合中的最大值。 不适用。 Enumerable.Max
Queryable.Max
最小值 确定集合中的最小值。 不适用。 Enumerable.Min
Queryable.Min
Sum 对集合中的值求和。 不适用。 Enumerable.Sum
Queryable.Sum

参考:

聚合运算

参考:

标准查询运算符概述 (C#)

https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.enumerable.oftype?view=netframework-4.7.2

LINQ 简介

  LINQ(语言集成查询,Language Integrated Query) 是 Visual Studio 2008 和 .NET Framework 3.5 版中引入的一项创新功能。它在对象领域和数据领域之间架起了一座桥梁。语言集成查询 (LINQ) 是一系列直接将查询功能集成到 C# 语言的技术统称。

  LINQ 最明显的“语言集成”部分就是查询表达式。 查询表达式采用声明性查询语法编写而成。 使用查询语法,可以用最少的代码对数据源执行筛选、排序和分组操作。 可使用相同的基本查询表达式模式来查询和转换 SQL 数据库、ADO .NET 数据集、XML 文档和流以及 .NET 集合中的数据。

查询表达式概述

  • 查询表达式可用于查询并转换所有启用了 LINQ 的数据源中的数据。
  • 查询表达式中的变量全都是强类型,尽管在许多情况下,无需显式提供类型,因为编译器可以推断出。
  • 只有在循环访问查询变量后,才会执行查询(例如,在 foreach 语句中)
  • 一些查询操作(如 CountMax)没有等效的查询表达式子句,因此必须表示为方法调用。
  • 查询表达式可被编译成表达式树或委托,具体视应用查询的类型而定。 IEnumerable<T> 查询编译为委托。 IQueryableIQueryable<T> 查询编译为表达式树。

LINQ 中的查询语法和方法语法

  在编译代码时,查询语法必须转换为针对 .NET 公共语言运行时 (CLR) 的方法调用。 这些方法调用会调用标准查询运算符(名称为 Where、Select、GroupBy、Join、Max 和 Average 等)。 可以使用方法语法(而不查询语法)来直接调用它们。

查询语法

1
2
3
4
5
6
//Query syntax:
IEnumerable<int> numQuery1 =
from num in numbers
where num % 2 == 0
orderby num
select num;

方法语法

1
2
//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);

Lambda 表达式

在 C# 中,=>lambda 运算符(读为“转到”)。

当我们的Lambda表达式里面用到了外部变量的时候,编译器会为这个Lambda生成一个类,在这个类中包含了我们表达式方法。

1
2
3
4
5
6
7
8
9
//Query syntax:
IEnumerable<int> numQuery1 =
from num in numbers
where num % 2 == 0
orderby num
select num;

//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);

  在上面的示例中,请注意,条件表达式 (num % 2 == 0) 作为内联参数传递给 Where 方法:Where(num => num % 2 == 0). 此内联表达式称为 lambda 表达式。 可采用匿名方法、泛型委托或表达式树的形式编写原本必须以更繁琐的形式编写的代码。

  Lambda 的主体与查询语法中或任何其他 C# 表达式或语句中的表达式完全相同;它可以包含方法调用和其他复杂逻辑。 “返回值”就是表达式结果。

某些查询只能采用方法语法进行表示,而其中一些查询需要 lambda 表达式。

对象和集合初始值设定项

通过对象和集合初始值设定项,初始化对象时无需为对象显式调用构造函数。 初始值设定项通常用在将源数据投影到新数据类型的查询表达式中。 假定一个类名为 Customer,具有公共 NamePhone 属性,可以按下列代码中所示使用对象初始值设定项:

1
2
3
4
Customer cust = new Customer { Name = "Mike", Phone = "555-1212" }; 
var newLargeOrderCustomers = from o in IncomingOrders
where o.OrderSize > 5
select new Customer { Name = o.Name, Phone = o.Phone };

述代码也可以使用 LINQ 的方法语法编写:

1
var newLargeOrderCustomers = IncomingOrders.Where(x => x.OrderSize > 5).Select(y => new Customer { Name = y.Name, Phone = y.Phone });

LINQ 查询

所有 LINQ 查询操作都由以下三个不同的操作组成:
获取数据源。
创建查询。
执行查询。

数据源

基本规则很简单:LINQ 数据源是支持泛型 IEnumerable<T> 接口或从中继承的接口的任意对象。

查询

在 LINQ 中,查询变量本身不执行任何操作并且不返回任何数据。 它只是存储在以后某个时刻执行查询时为生成结果而必需的信息。

查询执行

延迟执行

查询的实际执行将推迟到在 foreach 语句中循环访问查询变量之后进行。 此概念称为延迟执行,下面的示例对此进行了演示:

1
2
3
4
5
//  Query execution. 
foreach (int num in numQuery)
{
Console.Write("{0,1} ", num);
}

在应用程序中,可以创建一个检索最新数据的查询,并可以按某一时间间隔反复执行该查询以便每次检索不同的结果。

强制立即执行

对一系列源元素执行聚合函数的查询必须首先循环访问这些元素。 CountMaxAverageFirst 就属于此类查询。由于查询本身必须使用 foreach 以便返回结果,因此这些查询在执行时不使用显式 foreach 语句。 另外还要注意,这些类型的查询返回单个值,而不是 IEnumerable 集合。 下面的查询返回源数组中偶数的计数:

1
2
3
4
5
6
var evenNumQuery =
from num in numbers
where (num % 2) == 0
select num;

int evenNumCount = evenNumQuery.Count();

要强制立即执行任何查询并缓存其结果,可调用 ToListToArray 方法。

1
2
3
4
5
6
7
8
9
10
11
12
List<int> numQuery2 =
(from num in numbers
where (num % 2) == 0
select num).ToList();

// or like this:
// numQuery3 is still an int[]

var numQuery3 =
(from num in numbers
where (num % 2) == 0
select num).ToArray();

基本 LINQ 查询操作

获取数据源

使用 from 子句引入数据源:

1
2
3
//queryAllCustomers is an IEnumerable<Customer>
var queryAllCustomers = from cust in customers
select cust;

可通过 let 子句引入其他范围变量。

1
2
3
4
5
6
7
8
9
var earlyBirdQuery =
from sentence in strings
let words = sentence.Split(' ')
from word in words
let w = word.ToLower()
where w[0] == 'a' || w[0] == 'e'
|| w[0] == 'i' || w[0] == 'o'
|| w[0] == 'u'
select word;

筛选

筛选器使查询仅返回表达式为 true 的元素。 将通过使用 where 子句生成结果。

1
2
3
var queryLondonCustomers = from cust in customers
where cust.City == "London"
select cust;

中间件排序

orderby 子句根据要排序类型的默认比较器,对返回序列中的元素排序。

1
2
3
4
5
var queryLondonCustomers3 =
from cust in customers
where cust.City == "London"
orderby cust.Name ascending
select cust;

要对结果进行从 Z 到 A 的逆序排序,请使用 orderby…descending 子句。

分组

group 子句用于对根据指定的键所获得的结果进行分组。

使用 group 子句结束查询时,结果将以列表的形式列出。 列表中的每个元素都是具有 Key 成员的对象,列表中的元素根据该键被分组。 在循环访问生成组序列的查询时,必须使用嵌套 foreach 循环。 外层循环循环访问每个组,内层循环循环访问每个组的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// queryCustomersByCity is an IEnumerable<IGrouping<string, Customer>>
var queryCustomersByCity =
from cust in customers
group cust by cust.City;

// customerGroup is an IGrouping<string, Customer>
foreach (var customerGroup in queryCustomersByCity)
{
Console.WriteLine(customerGroup.Key);
foreach (Customer customer in customerGroup)
{
Console.WriteLine(" {0}", customer.Name);
}
}

如果必须引用某个组操作的结果,可使用 into 关键字创建能被进一步查询的标识符。 下列查询仅返回包含两个以上客户的组:

1
2
3
4
5
6
7
// custQuery is an IEnumerable<IGrouping<string, Customer>>
var custQuery =
from cust in customers
group cust by cust.City into custGroup
where custGroup.Count() > 2
orderby custGroup.Key
select custGroup;

联接

在 LINQ 中,join 子句始终作用于对象集合,而非直接作用于数据库表。

1
2
3
4
var innerJoinQuery =
from cust in customers
join dist in distributors on cust.City equals dist.City
select new { CustomerName = cust.Name, DistributorName = dist.Name };

在 LINQ 中,不必像在 SQL 中那样频繁使用 join,因为 LINQ 中的外键在对象模型中表示为包含项集合的属性。 例如 Customer 对象包含 Order 对象的集合。 不必执行联接,只需使用点表示法访问订单:

1
from order in Customer.Orders...

选择(投影)

select 子句生成查询结果并指定每个返回的元素的“形状”或类型。当 select 子句生成除源元素副本以外的内容时,该操作称为投影。

使用 LINQ 进行数据转换

语言集成查询 (LINQ) 不只是检索数据。 它也是用于转换数据的强大工具。 通过使用 LINQ 查询,可以使用源序列作为输入,并通过多种方式对其进行修改,以创建新的输出序列。通过排序和分组,你可以修改序列本身,而无需修改这些元素本身。

select 可以执行下列任务

  • 将多个输入序列合并为具有新类型的单个输出序列。
  • 创建其元素由源序列中每个元素的一个或多个属性组成的输出序列。
  • 创建其元素由对源数据执行的操作结果组成的输出序列。
  • 创建其他格式的输出序列。 例如,可以将数据从 SQL 行或文本文件转换为 XML。

将多个输入联接到一个输出序列中

可以使用 LINQ 查询创建包含元素的输出序列,这些元素来自多个输入序列。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class Student
{
public string First { get; set; }
public string Last {get; set;}
public int ID { get; set; }
public string Street { get; set; }
public string City { get; set; }
public List<int> Scores;
}

class Teacher
{
public string First { get; set; }
public string Last { get; set; }
public int ID { get; set; }
public string City { get; set; }
}
class DataTransformations
{
static void Main()
{
// Create the first data source.
List<Student> students = new List<Student>()
{
new Student { First="Svetlana",
Last="Omelchenko",
ID=111,
Street="123 Main Street",
City="Seattle",
Scores= new List<int> { 97, 92, 81, 60 } },
new Student { First="Claire",
Last="O’Donnell",
ID=112,
Street="124 Main Street",
City="Redmond",
Scores= new List<int> { 75, 84, 91, 39 } },
new Student { First="Sven",
Last="Mortensen",
ID=113,
Street="125 Main Street",
City="Lake City",
Scores= new List<int> { 88, 94, 65, 91 } },
};

// Create the second data source.
List<Teacher> teachers = new List<Teacher>()
{
new Teacher { First="Ann", Last="Beebe", ID=945, City="Seattle" },
new Teacher { First="Alex", Last="Robinson", ID=956, City="Redmond" },
new Teacher { First="Michiyo", Last="Sato", ID=972, City="Tacoma" }
};

// Create the query.
var peopleInSeattle = (from student in students
where student.City == "Seattle"
select student.Last)
.Concat(from teacher in teachers
where teacher.City == "Seattle"
select teacher.Last);

Console.WriteLine("The following students and teachers live in Seattle:");
// Execute the query.
foreach (var person in peopleInSeattle)
{
Console.WriteLine(person);
}

Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
The following students and teachers live in Seattle:
Omelchenko
Beebe
*/

选择每个源元素的子集

有两种主要方法来选择源序列中每个元素的子集:

1,若要仅选择源元素的一个成员,请使用点操作。

1
2
var query = from cust in Customers  
select cust.City;

2,要创建包含多个源元素属性的元素,可以使用带有命名对象或匿名类型的对象初始值设定项。

1
2
var query = from cust in Customer  
select new {Name = cust.Name, City = cust.City};

将内存中对象转换为 XML

LINQ 查询可以方便地在内存中数据结构、SQL 数据库、ADO.NET 数据集和 XML 流或文档之间转换数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void Main()
{
// Create the data source by using a collection initializer.
// The Student class was defined previously in this topic.
List<Student> students = new List<Student>()
{
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores = new List<int>{97, 92, 81, 60}},
new Student {First="Claire", Last="O’Donnell", ID=112, Scores = new List<int>{75, 84, 91, 39}},
new Student {First="Sven", Last="Mortensen", ID=113, Scores = new List<int>{88, 94, 65, 91}},
};

// Create the query.
var studentsToXML = new XElement("Root",
from student in students
let scores = string.Join(",", student.Scores)
select new XElement("student",
new XElement("First", student.First),
new XElement("Last", student.Last),
new XElement("Scores", scores)
) // end "student"
); // end "Root"

// Execute the query.
Console.WriteLine(studentsToXML);

// Keep the console open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Root>  
<student>
<First>Svetlana</First>
<Last>Omelchenko</Last>
<Scores>97,92,81,60</Scores>
</student>
<student>
<First>Claire</First>
<Last>O'Donnell</Last>
<Scores>75,84,91,39</Scores>
</student>
<student>
<First>Sven</First>
<Last>Mortensen</Last>
<Scores>88,94,65,91</Scores>
</student>
</Root>

对源元素执行操作

  输出序列可能不包含源序列中的任何元素或元素属性。 输出可能是使用源元素作为输入参数而计算得出的值序列。

备注:
  如果查询将被转换为另一个域,则不支持在查询表达式中调用方法。 例如,不能在 LINQ to SQL 中调用普通的 C# 方法,因为 SQL Server 没有用于它的上下文。 但是,可以将存储过程映射到方法并调用这些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void Main()
{
// Data source.
double[] radii = { 1, 2, 3 };

// Query.
IEnumerable<string> query =
from rad in radii
select $"Area = {rad * rad * Math.PI:F2}";

// Query execution.
foreach (string s in query)
Console.WriteLine(s);

// Keep the console open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}

/* Output:
Area = 3.14
Area = 12.57
Area = 28.27
*/

LINQ 查询操作中的类型关系

  LINQ 查询操作在数据源、查询本身及查询执行中是强类型化的。 查询中变量的类型必须与数据源中元素的类型和 foreach 语句中迭代变量的类型兼容。 此强类型保证在编译时捕获类型错误,以便可以在用户遇到这些错误之前更正它们。

不转换源数据的查询

linq_flow1.png

转换源数据的查询

linq_flow2.png

linq_flow3.png

让编译器推断类型信息

关键字 var 可用于查询操作中的任何本地变量。

linq_flow4.png

参考:

https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/let-clause

语言集成查询 (LINQ)

协变和逆变都是术语,协变指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,逆变指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型。

规律总结:

“子类”向“父类”转换,即协变;(可以使用的范围变大)
“父类”向“子类”转换,即逆变;(可以使用的范围变小)

变体

协变和逆变统称为“变体”(Variant)。 未标记为协变或逆变的泛型类型参数称为“固定参数” 。

规则:

通常,协变类型参数可用作委托的返回类型,而逆变类型参数可用作参数类型。
对于接口,协变类型参数可用作接口的方法的返回类型,而逆变类型参数可用作接口的方法的参数类型。

有关公共语言运行时中变体的事项的简短摘要:

  • 在 .NET Framework 4中,Variant 类型参数仅限于泛型接口和泛型委托类型。
  • 泛型接口或泛型委托类型可以同时具有协变和逆变类型参数。
  • 变体仅适用于引用类型;如果为 Variant 类型参数指定值类型,则该类型参数对于生成的构造类型是不变的。
  • 变体不适用于委托组合。 也就是说,在给定类型 Action<Derived>Action<Base> 的两个委托的情况下,无法将第二个委托与第一个委托结合起来,尽管结果将是类型安全的。 变体允许将第二个委托分配给类型 Action的变量,但只能在这两个委托的类型完全匹配的情况下对它们进行组合。

Covariance:协变
使你能够使用比原始指定的类型派生程度更大的类型。

Contravariance:逆变
使你能够使用比原始指定的类型更泛型(派生程度更小)的类型。

Invariance:不变
这意味着,你只能使用原始指定的类型;固定泛型类型参数既不是协变类型,也不是逆变类型。

关键字

协变类型参数用out关键字

可以将协变类型参数用作属于接口的方法的返回值,或用作委托的返回类型。 但不能将协变类型参数用作接口方法的泛型类型约束。如果接口的方法具有泛型委托类型的参数,则接口类型的协变类型参数可用于指定委托类型的逆变类型参数。

逆变类型参数用 in 关键字

可以将逆变类型参数用作属于接口的方法的参数类型,或用作委托的参数类型。 也可以将逆变类型参数用作接口方法的泛型类型约束。

注意,只有接口类型和委托类型才能具有 变体(Variant) 类型参数。 接口或委托类型可以同时具有协变和逆变类型参数。

C# 不允许违反协变和逆变类型参数的使用规则,也不允许将协变和逆变批注添加到接口和委托类型之外的类型参数中。 MSIL 汇编程序 不执行此类检查,但如果你尝试加载违反规则的类型,则会引发 TypeLoadException 。

协变out(泛型修饰符)

利用协变类型参数,你可以执行非常类似于普通的多态性的分配

1
2
IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d;

1,协变泛型接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Covariant interface.
interface ICovariant<out R> { }

// Extending covariant interface.
interface IExtCovariant<out R> : ICovariant<R> { }

// Implementing covariant interface.
class Sample<R> : ICovariant<R> { }

class Program
{
static void Test()
{
ICovariant<Object> iobj = new Sample<Object>();
ICovariant<String> istr = new Sample<String>();

// You can assign istr to iobj because
// the ICovariant interface is covariant.
iobj = istr;
}
}

2,协变泛型委托:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Covariant delegate.
public delegate R DCovariant<out R>();

// Methods that match the delegate signature.
public static Control SampleControl()
{ return new Control(); }

public static Button SampleButton()
{ return new Button(); }

public void Test()
{
// Instantiate the delegates with the methods.
DCovariant<Control> dControl = SampleControl;
DCovariant<Button> dButton = SampleButton;

// You can assign dButton to dControl
// because the DCovariant delegate is covariant.
dControl = dButton;

// Invoke the delegate.
dControl();
}

逆变in(泛型修饰符)

由于 lambda 表达式与其自身所分配到的委托相匹配,因此它会定义一个方法,此方法采用一个类型 Base 的参数且没有返回值。 可以将结果委托分配给类型类型 Action<Derived> 的变量,因为 T 委托的类型参数 Action<T> 是逆变类型参数。 由于 T 指定了一个参数类型,因此该代码是类型安全代码。

1
2
3
Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());

1,逆变泛型接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Contravariant interface.
interface IContravariant<in A> { }

// Extending contravariant interface.
interface IExtContravariant<in A> : IContravariant<A> { }

// Implementing contravariant interface.
class Sample<A> : IContravariant<A> { }

class Program
{
static void Test()
{
IContravariant<Object> iobj = new Sample<Object>();
IContravariant<String> istr = new Sample<String>();

// You can assign iobj to istr because
// the IContravariant interface is contravariant.
istr = iobj;
}
}

2,逆变泛型委托:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Contravariant delegate.
public delegate void DContravariant<in A>(A argument);

// Methods that match the delegate signature.
public static void SampleControl(Control control)
{ }
public static void SampleButton(Button button)
{ }

public void Test()
{
// Instantiating the delegates with the methods.
DContravariant<Control> dControl = SampleControl;
DContravariant<Button> dButton = SampleButton;

// You can assign dControl to dButton
// because the DContravariant delegate is contravariant.
dButton = dControl;

// Invoke the delegate.
dButton(new Button());
}

泛型接口中的变体

具有协变类型参数的泛型接口

从 .NET Framework 4开始,某些泛型接口具有协变类型参数;例如: IEnumerable<T>IEnumerator<T>IQueryable<T>IGrouping<TKey,TElement>。 由于这些接口的所有类型参数都是协变类型参数,因此这些类型参数只用于成员的返回类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Collections.Generic;

class Base
{
public static void PrintBases(IEnumerable<Base> bases)
{
foreach(Base b in bases)
{
Console.WriteLine(b);
}
}
}

class Derived : Base
{
public static void Main()
{
List<Derived> dlist = new List<Derived>();

Derived.PrintBases(dlist);
IEnumerable<Base> bIEnum = dlist;
}
}

具有逆变泛型类型参数的泛型接口

从 .NET Framework 4开始,某些泛型接口具有逆变类型参数;例如: IComparer<T>IComparable<T>IEqualityComparer<T>。 由于这些接口只具有逆变类型参数,因此这些类型参数只用作接口成员中的参数类型。

IComparer<T>.Compare 方法的实现基于 Area 属性的值,所以 ShapeAreaComparer 可用于按区域对 Shape 对象排序。
该示例创建 SortedSet<T> 对象的 Circle ,使用采用 IComparer<Circle> 的构造函数。 但是,该对象不传递 IComparer<Circle>,而是传递一个用于实现 ShapeAreaComparerIComparer<Shape> 对象。 当代码需要派生程度较大的类型的比较器 (Shape) 时,该示例可以传递派生程度较小的类型的比较器 (Circle),因为 IComparer<T> 泛型接口的类型参数是逆变参数。

Circle 中添加新 SortedSet<Circle>对象时,每次将新元素与现有元素进行比较时,都会调用 IComparer<Shape>.Compare 对象的IComparer(Of Shape).Compare 方法。 方法 (Shape) 的参数类型比被传递的类型 (Circle) 的派生程度小,所以调用是类型安全的。 逆变使 ShapeAreaComparer 可以对派生自 Shape的任意单个类型的集合以及混合类型的集合排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
using System.Collections.Generic;

abstract class Shape
{
public virtual double Area { get { return 0; }}
}

class Circle : Shape
{
private double r;
public Circle(double radius) { r = radius; }
public double Radius { get { return r; }}
public override double Area { get { return Math.PI * r * r; }}
}

class ShapeAreaComparer : System.Collections.Generic.IComparer<Shape>
{
int IComparer<Shape>.Compare(Shape a, Shape b)
{
if (a == null) return b == null ? 0 : -1;
return b == null ? 1 : a.Area.CompareTo(b.Area);
}
}

class Program
{
static void Main()
{
// You can pass ShapeAreaComparer, which implements IComparer<Shape>,
// even though the constructor for SortedSet<Circle> expects
// IComparer<Circle>, because type parameter T of IComparer<T> is
// contravariant.
SortedSet<Circle> circlesByArea =
new SortedSet<Circle>(new ShapeAreaComparer())
{ new Circle(7.2), new Circle(100), null, new Circle(.01) };

foreach (Circle c in circlesByArea)
{
Console.WriteLine(c == null ? "null" : "Circle with area " + c.Area);
}
}
}

/* This code example produces the following output:

null
Circle with area 0.000314159265358979
Circle with area 162.860163162095
Circle with area 31415.9265358979
*/

泛型委托中的变体

在 .NET Framework 4中, Func 泛型委托(如 Func<T,TResult>)具有协变返回类型和逆变参数类型。 Action 泛型委托(如 Action<T1,T2>)具有逆变参数类型。 这意味着,可以将委托指派给具有派生程度较高的参数类型和(对于 Func 泛型委托)派生程度较低的返回类型的变量。

备注:

Func 泛型委托的最后一个泛型类型参数指定委托签名中返回值的类型。 该参数是协变的(out 关键字),而其他泛型类型参数是逆变的(in 关键字)。

Variant 泛型接口和委托类型的列表

参考:Variant 泛型接口和委托类型的列表

参考:

泛型中的协变和逆变

out(泛型修饰符)

in(泛型修饰符)

HashSet<T> 是把对象保存在一个Hash表中的,查找时根据对象的Hash值直接定位对象;
List<T> 是把对象放到数组里,查找时是一个一个元素遍历的。

List<T> 里面是线序集,HashSet<T> 里面是散列表。与 Dictionary<K,V> 相比,List<T> 可以看成下标到值的映射,HashSet<T> 可以看成值自己到自己的映射。
判断一个值是否存在,List<T> 相当于是用值去找下标,要遍历一遍容器;
HashSet<T> 相当于用映射前的值去找映射后的值,只需要计算出来值的hash,然后直接访问就可以。

HashSet<T> 时间复杂度O(1),
List<T> 时间复杂度O(n)。

而且 HashSet<T> 无序不重,和 List<T> 完全不同。

按特定顺序存储项的集合 List<T>

快速确定集合中是否包含某个对象 HashSet<T>

集合是.NET FCL(Framework Class Library)中很重要的一部分。.net的集合诸如(System.Array类以及 System.Collections命名空间)数组、列表、队列、堆栈、哈希表、字典甚至(System.Data下)DataSetDataTable,还有2.0中加入的集合的泛型版本(System.Collections.GenericSystem.Collections.ObjectModel)。4.0中引入的有效线程安全操作的集合(System.Collections.Concurrent)。

集合接口

28211801-aeaddc8243394d34851a127aad881a29.png

IEnumerable 和 IEnumberator

1
2
3
4
5
6
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}

IEnumerator定义了遍历集合的基本方法,以便实现单向向前的访问集合中的每一个元素。而IEnumerable只有一个方法GetEnumerator即得到遍历器。

1
2
3
4
public interface IEnumerable
{
IEnumerator GetEnumerator();
}

注意:我们经常用的foreach即是一种语法糖,实际上还是调用Enumerator里面的Current和MoveNext实现的遍历功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
List<string> list = new List<string>()
{
"test1",
"test2",
"test3",
"test4",
"test5"
};

// Iterate the list by using foreach
foreach (var buddy in list)
{
Console.WriteLine(buddy);
}

// Iterate the list by using enumerator
List<string>.Enumerator enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}

foreach和enumerator到IL中最后都会被翻译成enumerator的MoveNext和Current。

IEnumerable支持foreach语句。

通过yield来实现返回enumerator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TestList : IEnumerable
{
private string[] data = new string[]
{
"test1",
"test2",
"test3",
"test4",
"test5"
};

public IEnumerator GetEnumerator()
{
foreach (var str in data)
{
yield return str;
}
}
}

private static void Main(string[] args)
{
var testLists = new TestList();
foreach (var str in testLists)
{
Console.WriteLine(str);
}

Console.Read();
}

ICollection<T>ICollection

ICollection 接口是 System.Collections 命名空间中类的基接口,ICollection<T>是所有泛型版本集合的基接口。所有的的集合类都直接或间接的继承他们。

ICollection 又继承 IEnumerable,来提供方便的枚举功能。ICollectionIEnumerable 多支持一些功能,不仅仅只提供基本的遍历功能,还包括:

  • 统计集合和元素个数
  • 获取元素的下标
  • 判断是否存在
  • 添加元素到未尾
  • 移除元素等等。

ICollectionICollection<T> 略有不同,ICollection 不提供编辑集合的功能,即Add和Remove。包括检查元素是否存在Contains也不支持。

IList和IList

IList 则是直接继承自 ICollectionIEnumerable。所以它包括两者的功能。

选择集合

微信截图_20181227105636.png

微信截图_20181227110233.png

详细请参考:https://docs.microsoft.com/zh-cn/dotnet/standard/collections/

注意非泛型类集合在.NET 2.0时代被泛型类集合代替。

Dictionary<TKey,TValue>

注解:将项存储为键/值对以通过键进行快速查找

1
2
3
4
5
6
非泛型集合选项:Hashtable(根据键的哈希代码组织的键/值对的集合。)

线程安全或不可变集合选项:
ConcurrentDictionary<TKey,TValue>
ReadOnlyDictionary<TKey,TValue>
ImmutableDictionary<TKey,TValue>

List

注解:按索引访问项

1
2
3
4
5
6
7
非泛型集合选项:
Array
ArrayList

线程安全或不可变集合选项:
ImmutableList<T>
ImmutableArray

Queue

注解:使用项先进先出 (FIFO)

1
2
3
4
5
非泛型集合选项:Queue

线程安全或不可变集合选项:
ConcurrentQueue<T>
ImmutableQueue<T>

Stack

注解:使用数据后进先出 (LIFO)

1
2
3
4
5
非泛型集合选项:Stack

线程安全或不可变集合选项:
ConcurrentStack<T>
ImmutableStack<T>

LinkedList

注解:按顺序访问项

SortedList<TKey,TValue>

注解:已排序的集合

1
2
3
4
5
非泛型集合选项:SortedList

线程安全或不可变集合选项:
ImmutableSortedDictionary<TKey,TValue>
ImmutableSortedSet<T>

HashSet 和 SortedSet

注解:数学函数的一个集

HashSet<T> 类提供高性能的设置操作。 集是不包含重复元素的集合,其元素无特定顺序。
SortedSet<T> 对象在插入和删除元素时维护排序顺序,而不会影响性能。 不允许重复元素。 不支持更改现有项的排序值,这可能导致意外行为。

1
2
3
线程安全或不可变集合选项:
ImmutableHashSet<T>
ImmutableSortedSet<T>

ObservableCollection

注解:删除集合中的项或向集合添加项时接收通知。 (实现 INotifyPropertyChanged 和 INotifyCollectionChanged)

参考:

C#集合类型大盘点

https://docs.microsoft.com/zh-cn/dotnet/standard/collections/

.NET集合总结

  C# 语言和公共语言运行时 (CLR) 的 2.0 版本中添加了泛型。 泛型将类型参数的概念引入 .NET Framework,这样就可以设计具有以下特征的类和方法:在客户端代码声明并初始化这些类和方法之前,这些类和方法会延迟指定一个或多个类型。 例如,通过使用泛型类型参数 T,可以编写其他客户端代码能够使用的单个类,而不会产生运行时转换或装箱操作的成本或风险。

泛型:为所存储或使用的一个或多个类型具有占位符(类型形参)的类、结构、接口和方法。 泛型集合类可以将类型形参用作其存储的对象类型的占位符;类型形参呈现为其字段的类型和其方法的参数类型。 泛型方法可将其类型形参用作其返回值的类型或用作其形参之一的类型。

  泛型类和泛型方法兼具可重用性、类型安全性和效率,这是非泛型类和非泛型方法无法实现的。 泛型通常与集合以及作用于集合的方法一起使用。通过创建泛型类,可在编译时创建类型安全的集合。

泛型概述(优点)

  • 使用泛型类型可以最大限度地重用代码、保护类型安全性以及提高性能。
  • 避免了装箱和拆箱操作。
  • 编译时类型检查。
  • 泛型最常见的用途是创建集合类。
  • .NET Framework 类库在 System.Collections.Generic 命名空间中包含几个新的泛型集合类。 应尽可能使用这些类来代替某些类,如 System.Collections 命名空间中的 ArrayList。
  • 可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
  • 可以对泛型类进行约束以访问特定数据类型的方法。
  • 在泛型数据类型中所用类型的信息可在运行时通过使用反射来获取。

泛型类型参数

参考:.NET 中的泛型

从反射的角度来说,泛型类型和普通类型之间的区别在于泛型类型具有与之关联的一组类型形参(若是泛型类型定义)或类型实参(若是构造类型)。

例如:

1
2
3
GenericList<T>; // T 类型形参
// 若要使用 GenericList<T>,客户端代码必须通过指定尖括号内的类型参数来声明并实例化构造类型(构造泛型类型)。
GenericList<float> list1 = new GenericList<float>(); // float 类型实参

类型参数的约束

  约束告知编译器类型参数必须具备的功能。 在没有任何约束的情况下,类型参数可以是任何类型。 编译器只能假定 Object 的成员,它是任何 .NET 类型的最终基类。 如果客户端代码尝试使用约束所不允许的类型来实例化类,则会产生编译时错误。 通过使用 where 上下文关键字指定约束。

下表列出了七种类型的约束:

约束 说明
where T : struct 类型参数必须是值类型。 可以指定除 Nullable 以外的任何值类型。
where T : class 类型参数必须是引用类型。 此约束还应用于任何类、接口、委托或数组类型。
where T : unmanaged 类型参数不能是引用类型,并且任何嵌套级别均不能包含任何引用类型成员。
where T : new() 类型参数必须具有公共无参数构造函数。 与其他约束一起使用时,new() 约束必须最后指定。
where T : <基类名> 类型参数必须是指定的基类或派生自指定的基类。
where T : <接口名称> 类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。
where T : U 为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。

  通过约束类型参数,可以增加约束类型及其继承层次结构中的所有类型所支持的允许操作和方法调用的数量。 设计泛型类或方法时,如果要对泛型成员执行除简单赋值之外的任何操作或调用 System.Object 不支持的任何方法,则必须对该类型参数应用约束。 例如,基类约束告诉编译器,仅此类型的对象或派生自此类型的对象可用作类型参数。 编译器有了此保证后,就能够允许在泛型类中调用该类型的方法。

可以对同一类型参数应用多个约束,并且约束自身可以是泛型类型,如下所示:

1
2
3
4
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
// ...
}

注意:

  在应用 where T : class 约束时,请避免对类型参数使用 == 和 != 运算符,因为这些运算符仅测试引用标识而不测试值相等性。

约束多个参数

可以对多个参数应用多个约束,对一个参数应用多个约束,如下例所示:

1
2
3
4
5
class Base { }
class Test<T, U>
where U : struct
where T : Base, new()
{ }

未绑定的类型参数

没有约束的类型参数(如公共类 SampleClass<T>{} 中的 T)称为未绑定的类型参数。 未绑定的类型参数具有以下规则:

  • 不能使用 != 和 == 运算符,因为无法保证具体的类型参数能支持这些运算符。
  • 可以在它们与 System.Object 之间来回转换,或将它们显式转换为任何接口类型。
  • 可以将它们与 null 进行比较。 将未绑定的参数与 null 进行比较时,如果类型参数为值类型,则该比较将始终返回 false。

类型参数作为约束

  在具有自己类型参数的成员函数必须将该参数约束为包含类型的类型参数时,将泛型类型参数用作约束非常有用,如下例所示:

1
2
3
4
public class List<T>
{
public void Add<U>(List<U> items) where U : T {/*...*/}
}

在上述示例中,T 在 Add 方法的上下文中是一个类型约束,而在 List 类的上下文中是一个未绑定的类型参数。

非托管约束

从 C# 7.3 开始,可使用 unmanaged 约束来指定类型参数必须为“非托管类型”。

参考:非托管约束

委托约束

  同样从 C# 7.3 开始,可将 System.Delegate 或 System.MulticastDelegate 用作基类约束。 CLR 始终允许此约束,但 C# 语言不允许。 使用 System.Delegate 约束,用户能够以类型安全的方式编写使用委托的代码。以下代码定义了合并两个同类型委托的扩展方法:

1
2
3
public static TDelegate TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)
where TDelegate : System.Delegate
=> Delegate.Combine(source, target) as TDelegate;

可使用上述方法来合并相同类型的委托:

1
2
3
4
5
6
7
8
9
10
Action first = () => Console.WriteLine("this");
Action second = () => Console.WriteLine("that");

var combined = first.TypeSafeCombine(second);
combined();

Func<bool> test = () => true;
// Combine signature ensures combined delegates must
// have the same type.
//var badCombined = first.TypeSafeCombine(test);

枚举约束

从 C# 7.3 开始,还可指定 System.Enum 类型作为基类约束。 CLR 始终允许此约束,但 C# 语言不允许。 使用 System.Enum 的泛型提供类型安全的编程,缓存使用 System.Enum 中静态方法的结果。

参考:枚举约束

泛型类

封闭式构造类型 (Node<int>):客户端代码指定类型参数;
创建开放式构造类型 (Node<T>):不指定类型参数(例如指定泛型基类时)

泛型类可继承自具体的封闭式构造或开放式构造基类:

1
2
3
4
5
6
7
8
9
10
11
class BaseNode { }
class BaseNodeGeneric<T> { }

// concrete type
class NodeConcrete<T> : BaseNode { }

//closed constructed type
class NodeClosed<T> : BaseNodeGeneric<int> { }

//open constructed type
class NodeOpen<T> : BaseNodeGeneric<T> { }

1,非泛型类(即,具体类)可继承自封闭式构造基类,但不可继承自开放式构造类或类型参数,因为运行时客户端代码无法提供实例化基类所需的类型参数。

1
2
3
4
5
6
7
8
//No error
class Node1 : BaseNodeGeneric<int> { }

//Generates an error
//class Node2 : BaseNodeGeneric<T> {}

//Generates an error
//class Node3 : T {}

2,继承自开放式构造类型的泛型类必须对非此继承类共享的任何基类类型参数提供类型参数,如下方代码所示:

1
2
3
4
5
6
7
8
9
10
class BaseNodeMultiple<T, U> { }

//No error
class Node4<T> : BaseNodeMultiple<T, int> { }

//No error
class Node5<T, U> : BaseNodeMultiple<T, U> { }

//Generates an error
//class Node6<T> : BaseNodeMultiple<T, U> {}

3,继承自开放式构造类型的泛型类必须指定作为基类型上约束超集或表示这些约束的约束:

1
2
class NodeItem<T> where T : System.IComparable<T>, new() { }
class SpecialNodeItem<T> : NodeItem<T> where T : System.IComparable<T>, new() { }

4,泛型类型可使用多个类型参数和约束,如下所示:

1
2
3
4
class SuperKeyType<K, V, U>
where U : System.IComparable<U>
where V : new()
{ }

如果一个泛型类实现一个接口,则该类的所有实例均可强制转换为该接口。

泛型接口

参考:泛型接口

泛型方法

参考:泛型方法

泛型和数组

参考:泛型和数组

泛型委托

参考:泛型委托

委托可以定义它自己的类型参数。 引用泛型委托的代码可以指定类型参数以创建封闭式构造类型,就像实例化泛型类或调用泛型方法一样,如以下示例中所示:

1
2
3
4
5
6
7
public delegate void Del<T>(T item);
public static void Notify(int i) { }

Del<int> m1 = new Del<int>(Notify);

// C# 2.0 版具有一种称为方法组转换的新功能,适用于具体委托类型和泛型委托类型,使你能够使用此简化语法编写上一行:
Del<int> m2 = Notify;

在泛型类中定义的委托可以用类方法使用的相同方式来使用泛型类类型参数。

1
2
3
4
5
6
7
class Stack<T>
{
T[] items;
int index;

public delegate void StackDelegate(T[] items);
}

引用委托的代码必须指定包含类的类型参数,如下所示:

1
2
3
4
5
6
7
private static void DoWork(float[] items) { }

public static void TestStack()
{
Stack<float> s = new Stack<float>();
Stack<float>.StackDelegate d = DoWork;
}

根据典型设计模式定义事件时,泛型委托特别有用,因为发件人参数可以为强类型,无需在它和 Object 之间强制转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
delegate void StackEventHandler<T, U>(T sender, U eventArgs);

class Stack<T>
{
public class StackEventArgs : System.EventArgs { }
public event StackEventHandler<Stack<T>, StackEventArgs> stackEvent;

protected virtual void OnStackChanged(StackEventArgs a)
{
stackEvent(this, a);
}
}

class SampleClass
{
public void HandleStackChange<T>(Stack<T> stack, Stack<T>.StackEventArgs args) { }
}

public static void Test()
{
Stack<double> s = new Stack<double>();
SampleClass o = new SampleClass();
s.stackEvent += o.HandleStackChange;
}

参考:

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/

任务并行主要处理任务,而数据并行是要从直观上移除任务,用一种更高级的抽象–并行循环,来替代任务。也就是说,并行的源不是算法的代码,而是算法所操作的数据。

Parallel

Parallel.For

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
for (int j = 1; j < 4; j++)
{
Console.WriteLine("\n第{0}次比较", j);

ConcurrentBag<int> bag = new ConcurrentBag<int>();

var watch = Stopwatch.StartNew();

watch.Start();

for (int i = 0; i < 20000000; i++)
{
bag.Add(i);
}

Console.WriteLine("串行计算:集合有:{0},总共耗时:{1}", bag.Count, watch.ElapsedMilliseconds);

GC.Collect();

bag = new ConcurrentBag<int>();

watch = Stopwatch.StartNew();

watch.Start();

Parallel.For(0, 20000000, i =>
{
bag.Add(i);
});

Console.WriteLine("并行计算:集合有:{0},总共耗时:{1}", bag.Count, watch.ElapsedMilliseconds);

GC.Collect();
}

比较结果:
微信截图_20181225092626.png

注意:

  • Parallel.For不支持浮点数的步进,使用的是Int32或Int64,每一次迭代的时候加1
  • 使用Parallel.For所迭代的顺序是无法保证的

Parallel.ForEach

forEach的独到之处就是可以将数据进行分区,每一个小区内实现串行计算,分区采用Partitioner.Create实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
for (int j = 1; j < 4; j++)
{
Console.WriteLine("\n第{0}次比较", j);

ConcurrentBag<int> bag = new ConcurrentBag<int>();

var watch = Stopwatch.StartNew();

watch.Start();

for (int i = 0; i < 30000000; i++)
{
bag.Add(i);
}

Console.WriteLine("串行计算:集合有:{0},总共耗时:{1}", bag.Count, watch.ElapsedMilliseconds);

GC.Collect();

bag = new ConcurrentBag<int>();

watch = Stopwatch.StartNew();

watch.Start();

// 创建分区的范围是0-3000000
// Partitioner.Create(0, 3000000, Environment.ProcessorCount)
// Environment.ProcessorCount能够获取到当前的硬件线程数
Parallel.ForEach(Partitioner.Create(0, 30000000), i =>
{
for (int m = i.Item1; m < i.Item2; m++)
{
bag.Add(m);
}
});

Console.WriteLine("并行计算:集合有:{0},总共耗时:{1}", bag.Count, watch.ElapsedMilliseconds);

GC.Collect();
}

结果:

微信截图_20181225093224.png

Parallel.Invoke

试图将很多方法并行运行,如果传入的是4个方法,则至少需要4个逻辑内核才能足以让这4个方法并发运行。

注意:

1.即使拥有4个逻辑内核,也不一定能够保证所需要运行的4个方法能够同时启动运行,如果其中的一个内核处于繁忙状态,那么底层的调度逻辑可能会延迟某些方法的初始化执行。

2.通过Parallel.Invoke编写的并发执行代码一定不能依赖与特定的执行顺序,因为它的并发执行顺序也是不定的。

3.使用Parallel.Invoke方法一定要测量运行结果、实现加速比以及逻辑内核的使用率,这点很重要。

4.使用Parallel.Invoke,在运行并行方法前都会产生一些额外的开销,如分配硬件线程等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private static void Main(string[] args)
{
var watch = Stopwatch.StartNew();

watch.Start();

Run1();

Run2();

Console.WriteLine("我是串行开发,总共耗时:{0}\n", watch.ElapsedMilliseconds);

watch.Restart();

Parallel.Invoke(Run1, Run2);

watch.Stop();

Console.WriteLine("我是并行开发,总共耗时:{0}", watch.ElapsedMilliseconds);

Console.ReadLine();
}

private static void Run1()
{
Console.WriteLine("我是任务一,我跑了3s");
Thread.Sleep(3000);
}

private static void Run2()
{
Console.WriteLine("我是任务二,我跑了5s");
Thread.Sleep(5000);
}

微信截图_20181225094750.png

中途退出并行循环

ParallelLoopState,该实例提供了Break和Stop方法来帮我们实现。

Break: 通知并行计算尽快的退出循环,比如并行计算正在迭代100,那么break后程序还会迭代所有小于100的。

Stop:并行循环应该尽快停止执行,如果调用Stop时迭代100正在被处理,那么循环无法保证处理完所有小于100的迭代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var watch = Stopwatch.StartNew();

watch.Start();

ConcurrentBag<int> bag = new ConcurrentBag<int>();

Parallel.For(0, 20000000, (i, state) =>
{
if (bag.Count == 1000)
{
state.Break();
return;
}
bag.Add(i);
});

Console.WriteLine("当前集合有{0}个元素。", bag.Count);

微信截图_20181225100015.png

异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private static void Main(string[] args)
{
try
{
Parallel.Invoke(Run1, Run2);
}
catch (AggregateException ex)
{
foreach (var single in ex.InnerExceptions)
{
Console.WriteLine(single.Message);
}
}

Console.Read();
}

private static void Run1()
{
Thread.Sleep(3000);
throw new Exception("我是任务1抛出的异常");
}

private static void Run2()
{
Thread.Sleep(5000);

throw new Exception("我是任务2抛出的异常");
}

微信截图_20181225100207.png

指定硬件线程数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var bag = new ConcurrentBag<int>();

ParallelOptions options = new ParallelOptions();

//指定使用的硬件线程数为1
options.MaxDegreeOfParallelism = 1;

Parallel.For(0, 300000, options, i =>
{
bag.Add(i);
});

Console.WriteLine("并行计算:集合有:{0}", bag.Count);

Console.Read();

并行Linq(PLINQ)

通过 AsParallel() 扩展方法,将集合从普通的 IEnumerable 改为ParallelQuery

PLINQ优于Parallel.ForEach的原因在于,PLINQ可以自动将执行查询的线程内部的临时处理结果聚合起来。

PLINQ使用3级处理管道来执行并行查询:

  • 1,首先,PLINQ决定需要多少线程来执行并行查询;
  • 2,其次,工作站线程从源集合中获取工作块,确保在有锁的情况下访问该工作块。每个线程独立的执行其工作项,并将结果压入本地队列。
  • 3,最终,所有本地结果会缓存到单个结果集合中。
1
2
3
4
5
6
7
8
9
10
var source = Enumerable.Range(1, 10000);

// Opt in to PLINQ with AsParallel.
var evenNums = from num in source.AsParallel()
where num % 2 == 0
select num;
Console.WriteLine("{0} even numbers out of {1} total",
evenNums.Count(), source.Count());
// The example displays the following output:
// 5000 even numbers out of 10000 total

参考:

8天玩转并行开发——第一天 Parallel的使用

C#并行编程-Parallel

C#并行编程-相关概念

https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.parallel?view=netframework-4.7.2

https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/how-to-specify-the-execution-mode-in-plinq

在目标方法要调用基于事件API,又要返回Task的时候使用。

比如下面的ApiWrapper方法,该方法要返回Task,又要调用EventClass对象的Do方法,并且等到Do方法触发Done事件后,Task才能得到结果并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void test()
{
var task = ApiWrapper();

Console.WriteLine("Test CurrentThread:" + Thread.CurrentThread.ManagedThreadId);

Console.WriteLine(task.Result);
}

private static Task<string> ApiWrapper()
{
var tcs = new TaskCompletionSource<string>();

var api = new EventClass();
api.Done += (args) => { tcs.TrySetResult(args); };
api.Do();

return tcs.Task;
}

public class EventClass
{
public Action<string> Done = (args) => {; };

public void Do()
{
Console.WriteLine("EventClass" + Thread.CurrentThread.ManagedThreadId);
Done("Done");
}
}

参考:

TaskCompletionSource Class

什么时候应该使用TaskCompletionSource

TaskCompletionSource的使用场景

.NET Framework 1.0 引进了 IAsyncResult 模式,也称为 Asynchronous Programming Model (APM)或 Begin/End 模式。 .NET Framework 2.0 增加了 Event-based Asynchronous Pattern (EAP)。 从.NET Framework 4 开始, Task-based Asynchronous Pattern (TAP) 取代了 APM 和 EAP,但能够轻松构建从早期模式中迁移的例程。

APM异步编程模式下,callback是执行在另一个线程中,不能随易的去更新UI。EAP这种异步编程模式下,事件绑定的方法也是在调用的那个线程中执行的。也就是说解决了异步编程的时候UI交互的问题,而且是在同一个线程中执行。

APM,EAP模式请参考:https://docs.microsoft.com/zh-cn/dotnet/standard/asynchronous-programming-patterns/asynchronous-programming-model-apm

异步编程准则

异步编程的准则是确定所需执行的操作是I/O-Bound 还是 CPU-Bound。因为这会极大影响代码性能,并可能导致某些构造的误用。

  • 如果代码会 等待 某些内容,例如数据库中的数据或web资源等,则你的工作是 I/O-Bound
  • 如果代码要执行开销巨大的计算,则你的工作是 CPU-Bound

如果你的工作为 I/O-Bound,请使用 asyncawait(而不使用 Task.Run)。 不应使用任务并行库。
如果你的工作为 CPU-Bound,并且你重视响应能力,请使用 async 和 await,并在另一个线程上使用 Task.Run 生成工作。 如果该工作同时适用于并发和并行,则应考虑使用任务并行库。

基于任务的异步模式 (TAP)

基于任务的异步模式 (TAP) 以 System.Threading.Tasks.Task 命名空间中的 System.Threading.Tasks.Task<TResult>System.Threading.Tasks 类型为基础,这些类型用于表示任意异步操作。 对于新的开发项目,建议采用 TAP 作为异步设计模式。

C# 5.0引入了2个新关键词:asyncawait。然而它大大简化了异步方法的编程。asyncawait关键字只是编译器功能。编译器会用Task类创建代码。

认识async和await

使用asyncawait关键词编写异步代码,具有与同步代码相当的结构和简单性,并且摒弃了异步编程的复杂结构。

await 不会开启新的线程,当前线程会一直往下走直到遇到真正的Async方法(比如说HttpClient.GetStringAsync),这个方法的内部会用Task.Run或者Task.Factory.StartNew 去开启线程。如果方法不是.NET为我们提供的Async方法,我们需要自己创建Task,才会真正的去创建线程。

如果另一个线程已经执行完毕(即name.IsCompleted=true),主线程仍然不用挂起,直接可以拿结果。
如果另一个线程还没有执行完毕(即name.IsCompleted=false),那么主线程会挂起等待,直到返回结果为止。

解析async和await

异步(async)

使用async修饰符标记的方法称为异步方法,异步方法只可以具有以下返回类型:

  • 1.Task
  • 2.Task<TResult>
  • 3.void
  • 4.从C# 7.0开始,任何具有可访问的GetAwaiter方法的类型。System.Threading.Tasks.ValueTask<TResult> 类型属于此类实现(需向项目添加System.Threading.Tasks.Extensions NuGet 包)。

  异步方法通常包含 await 运算符的一个或多个实例,但缺少 await 表达式也不会导致生成编译器错误。 如果异步方法未使用 await 运算符标记暂停点,那么异步方法会作为同步方法执行,即使有 async 修饰符也不例外,编译器将为此类方法发布一个警告。

等待(await)

await 表达式只能在由 async 修饰符标记的封闭方法体、lambda 表达式或异步方法中出现。在其他位置,它会解释为标识符。

使用await运算符的任务只可用于返回 TaskTask<TResult>System.Threading.Tasks.ValueType<TResult> 对象的方法。

异步方法同步运行,直至到达其第一个 await 表达式,此时 await 在方法的执行中插入挂起点,会将方法挂起直到所等待的任务完成,然后继续执行await后面的代码区域。
await 表达式并不阻止正在执行它的线程。 而是使编译器将剩下的异步方法注册为等待任务的延续任务。 控制权随后会返回给异步方法的调用方。 任务完成时,它会调用其延续任务,异步方法的执行会在暂停的位置处恢复。

注意:

  • 1.无法等待具有 void 返回类型的异步方法,并且无效返回方法的调用方捕获不到异步方法抛出的任何异常。

  • 2.异步方法无法声明 in、ref 或 out 参数,但可以调用包含此类参数的方法。 同样,异步方法无法通过引用返回值,但可以调用包含 ref 返回值的方法。

await并不是针对于async的方法,而是针对async方法所返回给我们的Task。

不用await关键字,确认Task执行完毕
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void Main(){
var task = Task.Run(() =>{
return GetName();
});

task.GetAwaiter().OnCompleted(() =>{
// 2 秒之后才会执行这里
var name = task.Result;
Console.WriteLine("My name is: " + name);
});

Console.WriteLine("主线程执行完毕");
Console.ReadLine();
}

static string GetName(){
Console.WriteLine("另外一个线程在获取名称");
Thread.Sleep(2000);
return "Hello World";
}
Task.GetAwaiter()和await Task 的区别
  • 加上await关键字之后,后面的代码会被挂起等待,直到task执行完毕有返回值的时候才会继续向下执行,这一段时间主线程会处于挂起状态。
  • GetAwaiter方法会返回一个awaitable的对象(继承了INotifyCompletion.OnCompleted方法)我们只是传递了一个委托进去,等task完成了就会执行这个委托,但是并不会影响主线程,下面的代码会立即执行。这也是为什么我们结果里面第一句话会是 主线程执行完毕
Task如何让主线程挂起等待
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void Main(){
var task = Task.Run(() =>{
return GetName();
});

var name = task.GetAwaiter().GetResult();
Console.WriteLine("My name is:{0}",name);

Console.WriteLine("主线程执行完毕");
Console.ReadLine();
}

static string GetName(){
Console.WriteLine("另外一个线程在获取名称");
Thread.Sleep(2000);
return "Hello World";
}

Task.GetAwait()方法会给我们返回一个awaitable的对象,通过调用这个对象的GetResult方法就会挂起主线程,当然也不是所有的情况都会挂起。在一开始的时候就启动了另一个线程去执行这个Task,当我们调用它的结果的时候,如果这个Task已经执行完毕,主线程是不用等待可以直接拿其结果的,如果没有执行完毕那主线程就得挂起等待了。

await的实质

await的实质是在调用awaitable对象的GetResult方法

1
2
3
4
5
6
7
8
9
10
11
12
13
static async Task Test(){
Task<string> task = Task.Run(() =>{
Console.WriteLine("另一个线程在运行!"); // 这句话只会被执行一次
Thread.Sleep(2000);
return "Hello World";
});

// 这里主线程会挂起等待,直到task执行完毕我们拿到返回结果
var result = task.GetAwaiter().GetResult();
// 这里不会挂起等待,因为task已经执行完了,我们可以直接拿到结果
var result2 = await task;
Console.WriteLine(str);
}

async和await使用建议

  • async方法需在其主体中具有await 关键字,否则它们将永不暂停。同时C# 编译器将生成一个警告,此代码将会以类似普通方法的方式进行编译和运行。 请注意这会导致效率低下,因为由 C# 编译器为异步方法生成的状态机将不会完成任何任务。
  • 应将 Async 作为后缀添加到所编写的每个异步方法名称中。这是 .NET 中的惯例,以便更轻松区分同步和异步方法。
  • async void 应仅用于事件处理程序。因为事件不具有返回类型(因此无法返回 TaskTask<T>)。 其他任何对 async void 的使用都不遵循 TAP 模型,且可能存在一定使用难度。
  • 避免上下文,调用 ConfigureAwait 并且传递false不要捕捉当前上下文。

例如:async void 方法中引发的异常无法在该方法外部被捕获或十分难以测试 async void 方法。

async和await总结

async/await 本质上只是一个语法糖,它并不产生线程,只是在编译时把语句的执行逻辑改了,相当于过去我们用callback,这里编译器自动实现了。

线程的转换是通过SynchronizationContext来实现,如果做了Task.ConfigureAwait(false)操作,运行MoveNext时就只是在线程池中拿个空闲线程出来执行;
如果 Task.ConfigureAwait(true)-(默认),则会在异步操作前Capture当前线程的SynchronizationContext,异步操作之后运行MoveNext时通过SynchronizationContext转到目标之前的线程。

一般是想更新UI则需要用到 SynchronizationContext,如果异步操作完成还需要做大量运算,则可以考虑Task.ConfigureAwait(false)把计算放到后台算,防止UI卡死。

另外还有在异步操作前做的ExecutionContext.FastCapture,获取当前线程的执行上下文,注意,如果Task.ConfigureAwait(false),会有个IgnoreSynctx的标记,表示在ExecutionContext.Capture里不做SynchronizationContext.Capture操作,Capture到的执行上下文用来在awaiter completed后给MoveNext用,使MoveNext可以有和前面线程同样的上下文。

通过SynchronizationContext.Post操作,可以使异步异常在最开始的try..catch块中轻松捕获。

调用异步方法

在一个异步方法里,可以调用一个或多个异步方法,如何编码取决于异步方法间结果是否相互依赖。

1.顺序调用异步方法

使用await关键词可以调用每个异步方法,如果一个异步方法需要使用另一个异步方法的结果,await关键词就非常必要。

2.使用组合器

如果异步方法间相互不依赖,则每个异步方法都不使用await,而是把每个异步方法的结果赋值给Task变量,就会运行得更快。

  • WhenAll是在所有传入的任务都完成时才返回Task。

  • WhenAny是在传入的任务其中一个完成就会返回Task。

异常处理

单个异步方法处理

异步方法的一个较好异常处理方式,是使用await关键字,将其放在try/catch中。

返回void的异步方法不会等待。这是因为从async void方法抛出的异常无法捕获。因此异步方法最好返回一个Task类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//异步方法错误处理
static async void HandleError()
{
try
{
await ThrowAfter(2000, "HandleError Error");
}
catch (Exception ex)
{

Console.WriteLine(ex.Message);
}
}
//在延迟后抛出异常
static async Task ThrowAfter(int ms, string message)
{
await Task.Delay(ms);
throw new Exception(message);
}

多个异步方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Task.WhenAll,在catch块内可以访问,再使用IsFaulted属性检查任务的状态,
// 以确认它们是否出现错误,然后再进行处理。
static async void HandleError()
{
Task t1 = null;
Task t2 = null;
try
{
t1 = ThrowAfter(1000, "HandleError-One-Error");
t2 = ThrowAfter(2000, "HandleError-Two-Error");
await Task.WhenAll(t1, t2);
}
catch (Exception)
{
if (t1.IsFaulted)
Console.WriteLine(t1.Exception.InnerException.Message);
if (t2.IsFaulted)
Console.WriteLine(t2.Exception.InnerException.Message);
}
}

// 使用AggregateException处理方式
static async void HandleError()
{
Task taskResult = null;
try
{
Task t1 = ThrowAfter(1000, "HandleError-One-Error");
Task t2 = ThrowAfter(2000, "HandleError-Two-Error");
await (taskResult = Task.WhenAll(t1, t2));
}
catch (Exception)
{
foreach (var ex in taskResult.Exception.InnerExceptions)
{
Console.WriteLine(ex.Message);
}

}
}
//在延迟后抛出异常
static async Task ThrowAfter(int ms, string message)
{
await Task.Delay(ms);
throw new Exception(message);
}

参考:

异步编程 In .NET

async/await IL翻译

async & await 的前世今生(Updated)

异步编程(async&await)

深入理解Async/Await

async 和 await 关键字

使用Nito.AsyncEx实现异步锁

使用 Async 和 Await 的异步编程 (C#)

https://docs.microsoft.com/zh-cn/dotnet/csharp/async

https://docs.microsoft.com/zh-cn/dotnet/standard/asynchronous-programming-patterns/asynchronous-programming-model-apm

  在.NET 4中的并行编程是依赖Task Parallel Library(后面简称为TPL) 实现的。在TPL中,最基本的执行单元是task(中文可以理解为”任务”),一个task就代表了你要执行的一个操作。你可以为你所要执行的每一个操作定义一个task,TPL就负责创建线程来执行你所定义的task,并且管理线程。TPL是面向task的,自动的;而传统的多线程是以人工为导向的。

  Task机制使得我们把注意力关注在我们要解决的问题上面。如果之前的多线程技术使得我们放弃了一些并行编程的使用,那么.NET 4中的新的并行编程技术可以让我们重新建立信心。虽然有了新的并行技术,但是传统的多线程的技术还是很有用的。任务并行(TPL)是一种范式,也是一组API,可以将大任务分割为多个小任务,并在多个线程中执行。当我们使用TPL中的并行技术的时候来执行多个task的时候,我们不用在关心底层创建线程,管理线程等。

线程基础

多核处理器带有一个以上的物理内核,每个物理内核都可能会提供多个硬件线程,也称之为逻辑内核或者逻辑处理器。

Windows将每一个硬件线程识别为一个可调度的逻辑处理器,每一个逻辑处理器可以运行软件线程代码,运行多个软件线程的进程可以充分发挥硬件线程和物理内核的优势,并行地运行指令。Windows会给每一个可用的硬件线程分配一块块的处理时间,并通过这种方式运行上百个千个软件线程。

硬件线程

物理内核:计算机实际内核数量;
硬件线程又叫做逻辑内核,可以在”任务管理器“中查看”性能“标签页

一般情况下,一个物理内核对应一个逻辑内核。当然如果你的cpu采用的是超线程技术,那么可能就会有4个物理内核对应。

软件线程

传统的代码都是串行的,就一个主线程,当我们为了实现加速而开了很多工作线程,这些工作线程也就是软件线程。

Task的基本使用

创建任务Task

开启task有两种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static void Main(string[] args)
{
//第一种方式开启
var task1 = new Task(() =>
{
Run1();
});

//第二种方式开启
var task2 = Task.Factory.StartNew(() =>
{
Run2();
});

Console.WriteLine("调用start之前****************************\n");

//调用start之前的“任务状态”
Console.WriteLine("task1的状态:{0}", task1.Status);

Console.WriteLine("task2的状态:{0}", task2.Status);

task1.Start();

Console.WriteLine("\n调用start之后****************************");

//调用start之前的“任务状态”
Console.WriteLine("\ntask1的状态:{0}", task1.Status);

Console.WriteLine("task2的状态:{0}", task2.Status);

//主线程等待任务执行完
Task.WaitAll(task1, task2);

Console.WriteLine("\n任务执行完后的状态****************************");

//调用start之前的“任务状态”
Console.WriteLine("\ntask1的状态:{0}", task1.Status);

Console.WriteLine("task2的状态:{0}", task2.Status);

Console.ReadKey();
}

private static void Run1()
{
Thread.Sleep(1000);
Console.WriteLine("\n我是任务1");
}

private static void Run2()
{
Thread.Sleep(2000);
Console.WriteLine("我是任务2");
}

** Task.Run 和 Task.Factory.StartNew 区别**

  可以认为 Task.Run 是简化的 Task.Factory.StartNew 的使用,除了需要指定一个线程是长时间占用的,否则就使用 Task.RunTask.Factory.StartNew 可以设置线程是长时间运行,这时线程池就不会等待这个线程回收。

为创建的Task传入参数

想向 Task 传入参数,只能用System.Action<object>;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void Main(string[] args)
{
string[] messages = { "First task", "Second task", "Third task", "Fourth task" };
foreach (string msg in messages)
{
Task myTask = new Task(obj => printMessage((string)obj), msg);
myTask.Start();
}
// wait for input before exiting
Console.WriteLine("Main method complete. Press enter to finish.");
Console.ReadLine();
}

static void printMessage(string message)
{
Console.WriteLine("Message: {0}", message);
}

在创建Task的时候,Task有很多的构造函数的重载,一个主要的重载就是传入TaskCreateOptions的枚举:

  • TaskCreateOptions.None:用默认的方式创建一个Task
  • TaskCreateOptions.PreferFairness:请求scheduler尽量公平的执行Task(Task和线程一样,有优先级的)
  • TaskCreateOptions.LongRunning:声明Task将会长时间的运行。
  • TaskCreateOptions.AttachToParent:因为Task是可以嵌套的,所以这个枚举就是把一个子task附加到一个父task中。

  最后要提到的一点就是,我们可以在Task的执行体中用Task.CurrentId来返回Task的唯一表示ID(int)。如果在Task执行体外使用这个属性就会得到null。

Task执行

等待Task执行完成

用Wait()方法来一直等待一个Task执行完成。当task执行完成,或者被cancel,或者抛出异常,这个方法才会返回。可以使用Wait()方法的不同重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Task task = createTask(token);
task.Start();

Console.WriteLine("Waiting for task to complete.");
task.Wait();
Console.WriteLine("Task Completed.");

// create and start another task
task = createTask(token);
task.Start();
Console.WriteLine("Waiting 2 secs for task to complete.");
bool completed = task.Wait(6000);
Console.WriteLine("Wait ended - task completed: {0}", completed);

// create and start another task
task = createTask(token);
task.Start();
Console.WriteLine("Waiting 2 secs for task to complete.");
completed = task.Wait(2000, token);
Console.WriteLine("Wait ended - task completed: {0} task cancelled {1}",
completed, task.IsCanceled);

wait方法子task执行完成之后会返回true。

等待多个task

1
Task.WaitAll(task1, task2);

等待多个task中的一个task执行完成

1
int taskIndex = Task.WaitAny(task1, task2);

懒加载的Task(Lazily Task)

延迟初始化,主要的好处就是避免不必要的系统开销。

Lazy变量只有在用到的时候才会被初始化。所以我们可以把Lazy变量和task的创建结合:只有这个task要被执行的时候才去初始化。现在如果用了Lazy的task,那么现在我们初始化的就是那个Lazy变量了,而没有初始化task,(初始化Lazy变量的开销小于初始化task),只有当调用了lazyData.Value时,Lazy变量中包含的那个task才会初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static void Main(string[] args)
{
// define the function
Func<string> taskBody = new Func<string>(() =>
{
Console.WriteLine("Task body working...");
return "Task Result";
});

// create the lazy variable
Lazy<Task<string>> lazyData = new Lazy<Task<string>>(() =>
Task<string>.Factory.StartNew(taskBody));

Console.WriteLine("Calling lazy variable");
Console.WriteLine("Result from task: {0}", lazyData.Value.Result);

// do the same thing in a single statement
Lazy<Task<string>> lazyData2 = new Lazy<Task<string>>(
() => Task<string>.Factory.StartNew(() =>
{
Console.WriteLine("Task body working...");
return "Task Result";
}));

Console.WriteLine("Calling second lazy variable");
Console.WriteLine("Result from task: {0}", lazyData2.Value.Result);

// wait for input before exiting
Console.WriteLine("Main method complete. Press enter to finish.");
Console.ReadLine();
}

Task 死锁

描述:如果有两个或者多个task(简称TaskA)等待其他的task(TaskB)执行完成才开始执行,但是TaskB也在等待TaskA执行完成才开始执行,这样死锁就产生了。

解决方案:避免这个问题最好的方法就是:不要使的task来依赖其他的task。也就是说,最好不要在你定义的task的执行体内包含其他的task。

Task异常处理

在执行 Task.Wait(),Task.WaitAll(),Task.WaitAny(),Task.Result.不管那里出现了异常,最后抛出的就是一个 System.AggregateException

处理基本的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// create the tasks
Task task1 = new Task(() =>
{
ArgumentOutOfRangeException exception = new ArgumentOutOfRangeException();
exception.Source = "task1";
throw exception;
});
Task task2 = new Task(() =>
{
throw new NullReferenceException();
});
Task task3 = new Task(() =>
{
Console.WriteLine("Hello from Task 3");
});
// start the tasks
task1.Start(); task2.Start(); task3.Start();
// wait for all of the tasks to complete
// and wrap the method in a try...catch block
try
{
Task.WaitAll(task1, task2, task3);
}
catch (AggregateException ex)
{
// enumerate the exceptions that have been aggregated
foreach (Exception inner in ex.InnerExceptions)
{
Console.WriteLine("Exception type {0} from {1}",
inner.GetType(), inner.Source);
}
}
// wait for input before exiting
Console.WriteLine("Main method complete. Press enter to finish.");
Console.ReadLine();

使用迭代的异常处理Handler

一般情况下,我们需要区分哪些异常需要处理,而哪些异常需要继续往上传递。AggregateException类提供了一个Handle()方法,我们可以用这个方法来处理

AggregateException中的每一个异常。在这个 Handle() 方法中,返回true就表明,这个异常我们已经处理了,不用抛出,反之。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// create the cancellation token source and the token
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
// create a task that waits on the cancellation token
Task task1 = new Task(() =>
{
// wait forever or until the token is cancelled
token.WaitHandle.WaitOne(-1);
// throw an exception to acknowledge the cancellation
throw new OperationCanceledException(token);
}, token);
// create a task that throws an exception
Task task2 = new Task(() =>
{
throw new NullReferenceException();
});
// start the tasks
task1.Start(); task2.Start();
// cancel the token
tokenSource.Cancel();
// wait on the tasks and catch any exceptions
try
{
Task.WaitAll(task1, task2);
}
catch (AggregateException ex)
{
// iterate through the inner exceptions using
// the handle method
ex.Handle((inner) =>
{
if (inner is OperationCanceledException)
{
// ...handle task cancellation...
return true;
}
else
{
// this is an exception we don't know how
// to handle, so return false
return false;
}
});
}
// wait for input before exiting
Console.WriteLine("Main method complete. Press enter to finish.");
Console.ReadKey();

获取Task的执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static void Main(string[] args)
{
// create the task
Task<int> task1 = new Task<int>(() =>
{
int sum = 0;
for (int i = 0; i < 100; i++)
{
sum += i;
}
return sum;
});

task1.Start();
// write out the result
Console.WriteLine("Result 1: {0}", task1.Result);

// create the task
Task<int> task2 = Task.Factory.StartNew<int>(() =>
{
int sum = 0;
for (int i = 0; i < 100; i++)
{
sum += i;
}
return sum;
});

// write out the result
Console.WriteLine("Result 1: {0}", task2.Result);

Console.ReadLine();
}

获取Task的状态

在.NET并行编程还有一个已经标准化的操作就是可以获取task的状态,通过 Task.Status 属性来得到的,这个属性返回一个 System.Threading.Tasks.TaskStatus 的枚举值。

如下:

  • Created:表明task已经被初始化了,但是还没有加入到Scheduler中。
  • WatingForActivation:task正在等待被加入到Scheduler中。
  • WaitingToRun:已经被加入到了Scheduler,等待执行。
  • Running:task正在运行
  • WaitingForChildrenToComplete:表明父task正在等待子task运行结束。
  • RanToCompletion:表明task已经执行完了,但是还没有被cancel,而且也这个task也没有抛出异常。
  • Canceled:表明task已经被cancel了。(大家可以参看之前讲述取消task的文章)
  • Faulted:表明task在运行的时候已经抛出了异常。

取消任务(Task)

通过轮询的方式检测Task是否被取消

  在很多Task内部都包含了循环,用来处理数据。我们可以在循环中通过 CancellationTokenIsCancellationRequest 属性来检测task是否被取消了。如果这个属性为true,那么我们就得跳出循环,并且释放task所占用的资源(如数据库资源,文件资源等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static void Main(string[] args)
{
// create the cancellation token source
CancellationTokenSource tokenSource = new CancellationTokenSource();

// create the cancellation token
CancellationToken token = tokenSource.Token;
// create the task

Task task = new Task(() =>
{
for (int i = 0; i < int.MaxValue; i++)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("Task cancel detected");
throw new OperationCanceledException(token);
}
else
{
Console.WriteLine("Int value {0}", i);
}
}
}, token);

// wait for input before we start the task
Console.WriteLine("Press enter to start task");
Console.WriteLine("Press enter again to cancel task");
Console.ReadLine();

// start the task
task.Start();

// read a line from the console.
Console.ReadLine();

// cancel the task
Console.WriteLine("Cancelling task");
tokenSource.Cancel();

// wait for input before exiting
Console.WriteLine("Main method complete. Press enter to finish.");
Console.ReadLine();
}

用委托delegate来检测Task是否被取消

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static void Main(string[] args)
{
// create the cancellation token source
CancellationTokenSource tokenSource = new CancellationTokenSource();

// create the cancellation token
CancellationToken token = tokenSource.Token;

// create the task
Task task = new Task(() =>
{
for (int i = 0; i < int.MaxValue; i++)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("Task cancel detected");
throw new OperationCanceledException(token);
}
else
{
Console.WriteLine("Int value {0}", i);
}
}
}, token);

// register a cancellation delegate
token.Register(() =>
{
Console.WriteLine(">>>>>> Delegate Invoked\n");
});

// wait for input before we start the task
Console.WriteLine("Press enter to start task");
Console.WriteLine("Press enter again to cancel task");
Console.ReadLine();

// start the task
task.Start();
// read a line from the console.
Console.ReadLine();

// cancel the task
Console.WriteLine("Cancelling task");
tokenSource.Cancel();

// wait for input before exiting
Console.WriteLine("Main method complete. Press enter to finish.");
Console.ReadLine();
}

用Wait Handle还检测Task是否被取消

  检测task是否被cancel就是调用CancellationToken.WaitHandle属性。对于这个属性的详细使用,在后续的文章中会深入的讲述,在这里主要知道一点就行了:CancellationToken的WaitOne()方法会阻止task的运行,只有CancellationToken的cancel()方法被调用后,这种阻止才会释放。

  在下面的例子中,创建了两个task,其中task2调用了WaitOne()方法,所以task2一直不会运行,除非调用了CancellationToken的Cancel()方法,所以WaitOne()方法也算是检测task是否被cancel的一种方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static void Main(string[] args)
{

// create the cancellation token source
CancellationTokenSource tokenSource = new CancellationTokenSource();

// create the cancellation token
CancellationToken token = tokenSource.Token;

// create the task
Task task1 = new Task(() =>
{
for (int i = 0; i < int.MaxValue; i++)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("Task cancel detected");
throw new OperationCanceledException(token);
}
else
{
Console.WriteLine("Int value {0}", i);
}
}
}, token);

// create a second task that will use the wait handle
Task task2 = new Task(() =>
{
// wait on the handle
token.WaitHandle.WaitOne();
// write out a message
Console.WriteLine(">>>>> Wait handle released");
});

// wait for input before we start the task
Console.WriteLine("Press enter to start task");
Console.WriteLine("Press enter again to cancel task");
Console.ReadLine();
// start the tasks
task1.Start();
task2.Start();

// read a line from the console.
Console.ReadLine();

// cancel the task
Console.WriteLine("Cancelling task");
tokenSource.Cancel();

// wait for input before exiting
Console.WriteLine("Main method complete. Press enter to finish.");
Console.ReadLine();
}

取消多个Task

  我们可以使用一个CancellationToken来创建多个不同的Tasks,当这个CancellationToken的Cancel()方法调用的时候,使用了这个token的多个task都会被取消。

创建组合的取消Task的Token

  我们可以用CancellationTokenSource.CreateLinkedTokenSource()方法来创建一个组合的token,这个组合的token有很多的CancellationToken组成。主要组合token中的任意一个token调用了Cancel()方法,那么使用这个组合token的所有task就会被取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static void Main(string[] args)
{
// create the cancellation token sources
CancellationTokenSource tokenSource1 = new CancellationTokenSource();
CancellationTokenSource tokenSource2 = new CancellationTokenSource();
CancellationTokenSource tokenSource3 = new CancellationTokenSource();

// create a composite token source using multiple tokens
CancellationTokenSource compositeSource =
CancellationTokenSource.CreateLinkedTokenSource(
tokenSource1.Token, tokenSource2.Token, tokenSource3.Token);

// create a cancellable task using the composite token
Task task = new Task(() =>
{
// wait until the token has been cancelled
compositeSource.Token.WaitHandle.WaitOne();
// throw a cancellation exception
throw new OperationCanceledException(compositeSource.Token);
}, compositeSource.Token);

// start the task
task.Start();

// cancel one of the original tokens
tokenSource2.Cancel();

// wait for input before exiting
Console.WriteLine("Main method complete. Press enter to finish.");
Console.ReadLine();
}

判断一个Task是否已经被取消了

可以使用Task的IsCancelled属性来判断task是否被取消了。

Task的休眠

使用CancellationToken的Wait Handle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static void Main(string[] args)
{
// create the cancellation token source
CancellationTokenSource tokenSource = new CancellationTokenSource();

// create the cancellation token
CancellationToken token = tokenSource.Token;

// create the first task, which we will let run fully
Task task1 = new Task(() =>
{
for (int i = 0; i < Int32.MaxValue; i++)
{
// put the task to sleep for 10 seconds
bool cancelled = token.WaitHandle.WaitOne(10000);
// print out a message
Console.WriteLine("Task 1 - Int value {0}. Cancelled? {1}",
i, cancelled);
// check to see if we have been cancelled
if (cancelled)
{
throw new OperationCanceledException(token);
}
}
}, token);
// start task
task1.Start();

// wait for input before exiting
Console.WriteLine("Press enter to cancel token.");
Console.ReadLine();

// cancel the token
tokenSource.Cancel();

// wait for input before exiting
Console.WriteLine("Main method complete. Press enter to finish.");
Console.ReadLine();
}

  有一点要注意:WaitOne()方法只有在设定的时间间隔到了,或者Cancel方法被调用,此时task才会被唤醒。如果如果cancel()方法被调用而导致task被唤醒,那么CancellationToken.WaitHandle.WaitOne()方法就会返回true,如果是因为设定的时间到了而导致task唤醒,那么CancellationToken.WaitHandle.WaitOne()方法返回false。

Task.Delay

Task.Delay在不阻塞当前线程的情况下使用逻辑延迟时使用。Task.Delay旨在异步运行。

使用传统的Sleep

Thread.Sleep时要阻止当前线程。

注意:Thread.Sleep在同步代码中使用,不能在异步代码中使用;

Thread.Sleep(10000);

  使用Thread.Sleep()之后,然后再调用token的cancel方法,task不会立即就被cancel,这主要是因为Thread.Sleep()将会一直阻塞线程,直到达到了设定的时间,这之后,再去check task时候被cancel了。举个例子,假设再task方法体内调用Thread.Sleep(100000)方法来休眠task,然后再后面的代码中调用token.Cancel()方法,此时处于并行编程内部机制不会去检测task是否已经发出了cancel请求,而是一直休眠,直到时间超过了100000微秒。如果采用的是之前的第一种休眠方法,那么不管WaitOne()中设置了多长的时间,只要token.Cancel()被调用,那么task就像内部的Scheduler发出了cancel的请求,而且task会被cancel。

自旋等待

推荐的方法。

之前的两种方法,当他们使得task休眠的时候,这些task已经从Scheduler的管理中退出来了,不被再内部的Scheduler管理(Scheduler是负责管理线程的),因为休眠的task已经不被Scheduler管理了,所以Scheduler必须做一些工作去决定下一步是哪个线程要运行,并且启动它。为了避免Scheduler做那些工作,我们可以采用自旋等待:此时这个休眠的task所对应的线程不会从Scheduler中退出,这个task会把自己和CPU的轮转关联起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void Main(string[] args)
{
// create the cancellation token source
CancellationTokenSource tokenSource = new CancellationTokenSource();

// create the cancellation token
CancellationToken token = tokenSource.Token;

// create the first task, which we will let run fully
Task task1 = new Task(() =>
{
for (int i = 0; i < Int32.MaxValue; i++)
{
// put the task to sleep for 10 seconds
Thread.SpinWait(10000);
// print out a message
Console.WriteLine("Task 1 - Int value {0}", i);
// check for task cancellation
token.ThrowIfCancellationRequested();
}
}, token);

// start task
task1.Start();

// wait for input before exiting
Console.WriteLine("Press enter to cancel token.");

Console.ReadLine();
// cancel the token
tokenSource.Cancel();

// wait for input before exiting
Console.WriteLine("Main method complete. Press enter to finish.");
Console.ReadLine();
}

  代码中我们在Thread.SpinWait()方法中传入一个整数,这个整数就表示CPU时间片轮转的次数,至于要等待多长的时间,这个就和计算机有关了,不同的计算机,CPU的轮转时间不一样。自旋等待的方法常常于获得同步锁,后续会讲解。使用自旋等待会一直占用CPU,而且也会消耗CPU的资源,更大的问题就是这个方法会影响Scheduler的运作。

参考:

.NET 4 并行(多核)编程系列

8天玩转并行开发——第二天 Task的使用

C# Task.Run 和 Task.Factory.StartNew 区别

线程基础

进程与线程

我们运行一个exe,就是一个进程实例,系统中有很多个进程。每一个进程都有自己的内存地址空间,每个进程相当于一个独立的边界,有自己的独占的资源,进程之间不能共享代码和数据空间。

151257-20160321141549448-1325816759.png

每一个进程有一个或多个线程,进程内多个线程可以共享所属进程的资源和数据,线程是操作系统调度的基本单元。线程是由操作系统来调度和执行的,她的基本状态如下图。

151257-20160321141550120-2131692214.png

线程的开销及调度

创建一个线程,主要包括线程内核对象、线程环境块、1M大小的用户模式栈、内核模式栈。其中用户模式栈对于普通的系统线程那1M是预留的,在需要的时候才会分配,但是对于CLR线程,那1M是一开始就分类了内存空间的。

151257-20160321141550589-1339297361.png

操作系统中那么多线程(一般都有上千个线程,大部分都处于休眠状态),对于单核CPU,一次只能有一个线程被调度执行,那么多线程怎么分配的呢?Windows系统采用时间轮询机制,CPU计算资源以时间片(大约30ms)的形式分配给执行线程。

计算机资源(CPU核心和CPU寄存器)一次只能调度一个线程,具体的调度流程:

  • 把CPU寄存器内的数据保存到当前线程内部(线程上下文等地方),给下一个线程腾地方;
  • 线程调度:在线程集合里取出一个需要执行的线程;
  • 加载新线程的上下文数据到CPU寄存器;
  • 新线程执行,享受她自己的CPU时间片(大约30ms),完了之后继续回到第一步,继续轮回;

上面线程调度的过程,就是一次线程切换,一次切换就涉及到线程上下文等数据的搬入搬出,性能开销是很大的。因此线程不可滥用,线程的创建和消费也是很昂贵的,这也是为什么建议尽量使用线程池的一个主要原因。

线程的主要几点性能影响:

  • 线程的创建、销毁都是很昂贵的;
  • 线程上下文切换有极大的性能开销,当然假如需要调度的新线程与当前是同一线程的话,就不需要线程上下文切换了,效率要快很多;
  • GC执行回收时,首先要(安全的)挂起所有线程,遍历所有线程栈(根),GC回收后更新所有线程的根地址,再恢复线程调用,线程越多,GC要干的活就越多;

多线程

线程池(ThreadPool)

将任务添加进线程池:

1
2
3
ThreadPool.QueueUserWorkItem(new WaitCallback(方法名));
//重载
ThreadPool.QueueUserWorkItem(new WaitCallback(方法名), 参数);

因为ThreadPool是静态类 所以不需要实例化.

每个CLR都有一个线程池,线程池在CLR内可以多个AppDomain共享,线程池是CLR内部管理的一个线程集合,初始是没有线程的,在需要的时候才会创建。

基本流程如下:

  • 线程池内部维护一个请求列队,用于缓存用户请求需要执行的代码任务,就是ThreadPool.QueueUserWorkItem提交的请求;
  • 有新任务后,线程池使用空闲线程或新线程来执行队列请求;
  • 任务执行完后线程不会销毁,留着重复使用;
  • 线程池自己负责维护线程的创建和销毁,当线程池中有大量闲置的线程时,线程池会自动结束一部分多余的线程来释放资源;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace 多线程池试验
{
class Program
{
public static void Main()
{
//新建ManualResetEvent对象并且初始化为无信号状态
ManualResetEvent eventX = new ManualResetEvent(false);
ThreadPool.SetMaxThreads(3, 3);
thr t = new thr(15, eventX);
for (int i = 0; i < 15; i++)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(t.ThreadProc), i);
}
//等待事件的完成,即线程调用ManualResetEvent.Set()方法
//eventX.WaitOne 阻止当前线程,直到当前 WaitHandle 收到信号为止。
eventX.WaitOne(Timeout.Infinite, true);
Console.WriteLine("断点测试");
Thread.Sleep(10000);
Console.WriteLine("运行结束");
}

public class thr
{
public thr(int count,ManualResetEvent mre)
{
iMaxCount = count;
eventX = mre;
}

public static int iCount = 0;
public static int iMaxCount = 0;
public ManualResetEvent eventX;
public void ThreadProc(object i)
{
Console.WriteLine("Thread[" + i.ToString() + "]");
Thread.Sleep(2000);
//Interlocked.Increment()操作是一个原子操作,作用是:iCount++ 具体请看下面说明
//原子操作,就是不能被更高等级中断抢夺优先的操作。你既然提这个问题,我就说深一点。
//由于操作系统大部分时间处于开中断状态,
//所以,一个程序在执行的时候可能被优先级更高的线程中断。
//而有些操作是不能被中断的,不然会出现无法还原的后果,这时候,这些操作就需要原子操作。
//就是不能被中断的操作。
Interlocked.Increment(ref iCount);
if (iCount == iMaxCount)
{
Console.WriteLine("发出结束信号!");
//将事件状态设置为终止状态,允许一个或多个等待线程继续。
eventX.Set();
}
}
}
}
}

AutoResetEvent和ManualResetEvent区别:
AutoResetEvent的WaitOne()方法执行后会自动又将信号置为不发送状态也就是阻塞状态,当再次遇到WaitOne()方法是又会被阻塞,而ManualResetEvent则不会,只要线程处于非阻塞状态则无论遇到多少次WaitOne()方法都不会被阻塞,除非调用ReSet()方法来手动阻塞线程。

线程池是有一个容量的,可以设置线程池的最大活跃线程数,调用方法ThreadPool.SetMaxThreads可以设置相关参数。但很多编程实践里都不建议程序猿们自己去设置这些参数,其实微软为了提高线程池性能,做了大量的优化,线程池可以很智能的确定是否要创建或是消费线程,大多数情况都可以满足需求了。

线程池使得线程可以充分有效地被利用,减少了任务启动的延迟,也不用大量的去创建线程,避免了大量线程的创建和销毁对性能的极大影响。

线程池的不足:

  • 线程池内的线程不支持线程的挂起、取消等操作,如想要取消线程里的任务,.NET支持一种协作式方式取消,使用起来也不很方便,而且有些场景并不满足需求;
  • 线程内的任务没有返回值,也不知道何时执行完成;
  • 不支持设置线程的优先级,还包括其他类似需要对线程有更多的控制的需求都不支持;

任务Task与并行Parallel

任务Task与并行Parallel本质上内部都是使用的线程池,提供了更丰富的并行编程的方式。任务Task基于线程池,可支持返回值,支持比较强大的任务执行计划定制等功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//创建一个任务
Task<int> t1 = new Task<int>(n =>
{
System.Threading.Thread.Sleep(1000);
return (int)n;
}, 1000);
//定制一个延续任务计划
t1.ContinueWith(task =>
{
Console.WriteLine("end" + t1.Result);
}, TaskContinuationOptions.AttachedToParent);
t1.Start();
//使用Task.Factory创建并启动一个任务
var t2 = System.Threading.Tasks.Task.Factory.StartNew(() =>
{
Console.WriteLine("t1:" + t1.Status);
});
Task.WaitAll();
Console.WriteLine(t1.Result);

并行Parallel内部其实使用的是Task对象(TPL会在内部创建System.Threading.Tasks.Task的实例),所有并行任务完成后才会返回。少量短时间任务建议就不要使用并行Parallel了,并行Parallel本身也是有性能开销的,而且还要进行并行任务调度、创建调用方法的委托等等。

221025576742743.png

GUI线程处理模型

这是很多开发C/S客户端应用程序会遇到的问题,GUI程序的界面控件不允许跨线程访问,如果在其他线程中访问了界面控件,运行时就会抛出一个异常,就像下面的图示,是不是很熟悉!这其中的罪魁祸首就是,就是“GUI的线程处理模型”。

151257-20160321141551714-1827445547.png

.NET支持多种不同应用程序模型,大多数的线程都是可以做任何事情(他们可能没有引入线程模型),但GUI应用程序(主要是Winform、WPF)引入了一个特殊线程处理模型,UI控件元素只能由创建它的线程访问或修改,微软这样处理是为了保证UI控件的线程安全。

为什么在UI线程中执行一个耗时的计算操作,会导致UI假死呢?这个问题要追溯到Windows的消息机制了。

因为Windows是基于消息机制的,我们在UI上所有的键盘、鼠标操作都是以消息的形式发送给各个应用程序的。GUI线程内部就有一个消息队列,GUI线程不断的循环处理这些消息,并根据消息更新UI的呈现。如果这个时候,你让GUI线程去处理一个耗时的操作(比如花10秒去下载一个文件),那GUI线程就没办法处理消息队列了,UI界面就处于假死的状态。

151257-20160321141552292-299214517.png

在线程里处理事件完成后,需要更新UI控件的状态:

(1)使用GUI控件提供的方法,Winform是控件的Invoke方法,WPF中是控件的Dispatcher.Invoke方法

1
2
3
4
5
//1.Winform:Invoke方法和BeginInvoke
this.label.Invoke(method, null);

//2.WPF:Dispatcher.Invoke
this.label.Dispatcher.Invoke(method, null);

(2)使用.NET中提供的BackgroundWorker执行耗时计算操作,在其任务完成事件RunWorkerCompleted 中更新UI控件

1
2
3
4
5
6
7
8
using (BackgroundWorker bw = new BackgroundWorker())
{
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler((ojb,arg) =>
{
this.label.Text = "test";
});
bw.RunWorkerAsync();
}

(3)使用GUI线程处理模型的同步上下文来送封UI控件修改操作,这样可以不需要调用UI控件元素

.NET中提供一个用于同步上下文的类SynchronizationContext,利用它可以把应用程序模型链接到他的线程处理模型,其实它的本质还是调用的第一步(1)中的方法。

实现代码分为三步,第一步定义一个静态类,用于GUI线程的UI元素访问封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static class GUIThreadHelper
{
public static System.Threading.SynchronizationContext GUISyncContext
{
get { return _GUISyncContext; }
set { _GUISyncContext = value; }
}

private static System.Threading.SynchronizationContext _GUISyncContext =
System.Threading.SynchronizationContext.Current;

/// <summary>
/// 主要用于GUI线程的同步回调
/// </summary>
/// <param name="callback"></param>
public static void SyncContextCallback(Action callback)
{
if (callback == null) return;
if (GUISyncContext == null)
{
callback();
return;
}
GUISyncContext.Post(result => callback(), null);
}

/// <summary>
/// 支持APM异步编程模型的GUI线程的同步回调
/// </summary>
public static AsyncCallback SyncContextCallback(AsyncCallback callback)
{
if (callback == null) return callback;
if (GUISyncContext == null) return callback;
return asynresult => GUISyncContext.Post(result => callback(result as IAsyncResult), asynresult);
}
}

第二步,在主窗口注册当前SynchronizationContext:

1
2
3
4
5
6
7
8
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
CLRTest.ConsoleTest.GUIThreadHelper.GUISyncContext = System.Threading.SynchronizationContext.Current;
}
}

第三步,就是使用了,可以在任何地方使用

1
2
3
4
5
6
GUIThreadHelper.SyncContextCallback(() =>
{
this.txtMessage.Text = res.ToString();
this.btnTest.Content = "DoTest";
this.btnTest.IsEnabled = true;
});

线程同步构造

基元线程同步构造分为:基元用户模式构造和基元内核模式构造,两种同步构造方式各有优缺点,而混合构造(如lock)就是综合两种构造模式的优点。

用户模式构造

基元用户模式比基元内核模式速度要快,她使用特殊的cpu指令来协调线程,在硬件中发生,速度很快。但也因此Windows操作系统永远检测不到一个线程在一个用户模式构造上阻塞了。举个例子来模拟一下用户模式构造的同步方式:

  • 线程1请求了临界资源,并在资源门口使用了用户模式构造的锁;
  • 线程2请求临界资源时,发现有锁,因此就在门口等待,并不停的去询问资源是否可用;
  • 线程1如果使用资源时间较长,则线程2会一直运行,并且占用CPU时间。占用CPU干什么呢?她会不停的轮询锁的状态,直到资源可用,这就是所谓的活锁;

缺点:线程2会一直使用CPU时间(假如当前系统只有这两个线程在运行),也就意味着不仅浪费了CPU时间,而且还会有频繁的线程上下文切换,对性能影响是很严重的。

当然她的优点是效率高,适合哪种对资源占用时间很短的线程同步。

.NET中为我们提供了两种原子性操作,利用原子操作可以实现一些简单的用户模式锁(如自旋锁)。

  • System.Threading.Interlocked:易失构造,它在包含一个简单数据类型的变量上执行原子性的读或写操作。
  • Thread.VolatileRead 和 Thread.VolatileWrite:互锁构造,它在包含一个简单数据类型的变量上执行原子性的读和写操作。

内核模式构造

这是针对用户模式的一个补充,先模拟一个内核模式构造的同步流程来理解她的工作方式:

  • 线程1请求了临界资源,并在资源门口使用了内核模式构造的锁;
  • 线程2请求临界资源时,发现有锁,就会被系统要求睡眠(阻塞),线程2就不会被执行了,也就不会浪费CPU和线程上下文切换了;
  • 等待线程1使用完资源后,解锁后会发送一个通知,然后操作系统会把线程2唤醒。假如有多个线程在临界资源门口等待,则会挑选一个唤醒;

看上去是不是非常棒!彻底解决了用户模式构造的缺点,但内核模式也有缺点的:将线程从用户模式切换到内核模式(或相反)导致巨大性能损失。调用线程将从托管代码转换为内核代码,再转回来,会浪费大量CPU时间,同时还伴随着线程上下文切换,因此尽量不要让线程从用户模式转到内核模式。

它的优点就是阻塞线程,不浪费CPU时间,适合那种需要长时间占用资源的线程同步。

内核模式构造的主要有两种方式,以及基于这两种方式的常见的锁:

  • 基于事件:如AutoResetEvent、ManualResetEvent
  • 基于信号量:如Semaphore

混合线程同步

Lock、SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim

lock的本质就是使用的Monitor,lock只是一种简化的语法形式,实质的语法形式如下:

1
2
3
4
5
6
7
8
9
10
bool lockTaken = false;
try
{
Monitor.Enter(obj, ref lockTaken);
//...
}
finally
{
if (lockTaken) Monitor.Exit(obj);
}

Semaphore 信号量

它可以控制对某一段代码或者对某个资源访问的线程的数量,超过这个数量之后,其它的线程就得等待,只有等现在有线程释放了之后,下面的线程才能访问。这个跟锁有相似的功能,只不过不是独占的,它允许一定数量的线程同时访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static SemaphoreSlim _sem = new SemaphoreSlim(3);    // 我们限制能同时访问的线程数量是3
static void Main(){
for (int i = 1; i <= 5; i++) new Thread(Enter).Start(i);
Console.ReadLine();
}

static void Enter(object id){
Console.WriteLine(id + " 开始排队...");
_sem.Wait();
Console.WriteLine(id + " 开始执行!");
Thread.Sleep(1000 * (int)id);
Console.WriteLine(id + " 执行完毕,离开!");
_sem.Release();
}

222030017324376.png

同步索引块是.NET中解决对象同步问题的基本机制,该机制为每个堆内的对象(即引用类型对象实例)分配一个同步索引,她其实是一个地址指针,初始值为-1不指向任何地址。

  • 创建一个锁对象Object obj,obj的同步索引块(地址)为-1,不指向任何地址;
  • Monitor.Enter(obj),创建或使用一个空闲的同步索引块(如下图中的同步块1),这个才是真正的同步索引块,其内部结构就是一个混合锁的结构,包含线程ID、递归计数、等待线程统计、内核对象等,类似一个混合锁AnotherHybridLock。obj对象(同步索引块AsynBlockIndex)指向该同步块1;
  • Exit时,重置为-1,那个同步索引块1可以被重复利用;

Lock关键字

lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。

通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock (“myLock”) 违反此准则:

  • 如果实例可以被公共访问,将出现 lock (this) 问题。
  • 如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。
  • 由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现lock(“myLock”) 问题。

最佳做法是定义 private 对象来锁定, 或 private static 对象变量来保护所有实例所共有的数据。

不要Lock值类型,不要Lock(this),不要Lock(null对象),推荐Lock只读静态对象。

151257-20160321141553854-448927161.jpg

因此,锁对象要求必须为一个引用对象(在堆上)。

多线程使用及线程同步总结

在使用Lock时,关键点就是锁对象了,需要注意以下几个方面:

  • 这个对象肯定要是引用类型,值类型可不可呢?值类型可以装箱啊!你觉得可不可以?但也不要用值类型,因为值类型多次装箱后的对象是不同的,会导致无法锁定;
  • 不要锁定this,尽量使用一个没有意义的Object对象来锁;
  • 不要锁定一个类型对象,因类型对象是全局的;
  • 不要锁定一个字符串,因为字符串可能被驻留,不同字符对象可能指向同一个字符串;
  • 不要使用[System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.Synchronized)],这个可以使用在方法上面,保证方法同一时刻只能被一个线程调用。她实质上是使用lock的,如果是实例方法,会锁定this,如果是静态方法,则会锁定类型对象

题目答案解析

1. 描述线程与进程的区别?

  • 一个应用程序实例是一个进程,一个进程内包含一个或多个线程,线程是进程的一部分;
  • 进程之间是相互独立的,他们有各自的私有内存空间和资源,进程内的线程可以共享其所属进程的所有资源;

2. 为什么GUI不支持跨线程访问控件?一般如何解决这个问题?

因为GUI应用程序引入了一个特殊的线程处理模型,为了保证UI控件的线程安全,这个线程处理模型不允许其他子线程跨线程访问UI元素。解决方法还是比较多的,如:

  • 利用UI控件提供的方法,Winform是控件的Invoke方法,WPF中是控件的Dispatcher.Invoke方法;
  • 使用BackgroundWorker;
  • 使用GUI线程处理模型的同步上下文SynchronizationContext来提交UI更新操作

上面几个方式在文中已详细给出。

3. 简述后台线程和前台线程的区别?

应用程序必须运行完所有的前台线程才可以退出,或者主动结束前台线程,不管后台线程是否还在运行,应用程序都会结束;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。

通过将 Thread.IsBackground 设置为 true,就可以将线程指定为后台线程,主线程就是一个前台线程。

4. 说说常用的锁,lock是一种什么样的锁?

常用的如如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,lock是一个混合锁,其实质是Monitor[‘mɒnɪtə]。

5. lock为什么要锁定一个参数,可不可锁定一个值类型?这个参数有什么要求?

lock的锁对象要求为一个引用类型。她可以锁定值类型,但值类型会被装箱,每次装箱后的对象都不一样,会导致锁定无效。

对于lock锁,锁定的这个对象参数才是关键,这个参数的同步索引块指针会指向一个真正的锁(同步块),这个锁(同步块)会被复用。

6. 多线程和异步有什么关系和区别?

多线程是实现异步的主要方式之一,异步并不等同于多线程。实现异步的方式还有很多,比如利用硬件的特性、使用进程或纤程等。在.NET中就有很多的异步编程支持,比如很多地方都有Begin、End的方法,就是一种异步编程支持,她内部有些是利用多线程,有些是利用硬件的特性来实现的异步编程。

7. 线程池的优点有哪些?又有哪些不足?

优点:减小线程创建和销毁的开销,可以复用线程;也从而减少了线程上下文切换的性能损失;在GC回收时,较少的线程更有利于GC的回收效率。

缺点:线程池无法对一个线程有更多的精确的控制,如了解其运行状态等;不能设置线程的优先级;加入到线程池的任务(方法)不能有返回值;对于需要长期运行的任务就不适合线程池。

8. Mutex和lock有何不同?一般用哪一个作为锁使用更好?

Mutex是一个基于内核模式的互斥锁,支持锁的递归调用,而Lock是一个混合锁,一般建议使用Lock更好,因为lock的性能更好。

9. 下面的代码,调用方法DeadLockTest(20),是否会引起死锁?并说明理由。

1
2
3
4
5
6
7
8
9
10
11
public void DeadLockTest(int i)
{
lock (this) //或者lock一个静态object变量
{
if (i > 10)
{
Console.WriteLine(i--);
DeadLockTest(i);
}
}
}

不会的,因为lock是一个混合锁,支持锁的递归调用,如果你使用一个ManualResetEvent或AutoResetEvent可能就会发生死锁。

10. 用双检锁实现一个单例模式Singleton。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static class Singleton<T> where T : class,new()
{
private static T _Instance;
private static object _lockObj = new object();

/// <summary>
/// 获取单例对象的实例
/// </summary>
public static T GetInstance()
{
if (_Instance != null) return _Instance;
lock (_lockObj)
{
if (_Instance == null)
{
var temp = Activator.CreateInstance<T>();
System.Threading.Interlocked.Exchange(ref _Instance, temp);
}
}
return _Instance;
}
}

11.下面代码输出结果是什么?为什么?如何改进她?

int a = 0;
System.Threading.Tasks.Parallel.For(0, 100000, (i) =>
{
a++;
});
Console.Write(a);

输出结果不稳定,小于等于100000。因为多线程访问,没有使用锁机制,会导致有更新丢失。

改进:

1
System.Threading.Interlocked.Add(ref a, 1);//正确

参考:

.NET面试题解析(07)-多线程编程与线程同步

https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.autoresetevent?view=netframework-4.7.2

C#多线程–线程池(ThreadPool)

C#多线程学习 之 线程池[ThreadPool]

C#深入学习笔记—Lock

面试题解析

1. 简述一下一个引用对象的生命周期?

  • new 创建对象并分配内存
  • 对象初始化
  • 对象操作、使用
  • 资源清理(非托管资源)
  • GC 垃圾回收

2. 创建下面对象实例,需要申请多少内存空间?

1
2
3
4
5
6
7
8
public class User
{
public int Age { get; set; }
public string Name { get; set; }

public string _Name = "123" + "abc";
public List<string> _Names;
}

44字节内存空间。

详细信息参考:DotNet基础-GC与内存管理

3. 什么是垃圾?

一个变量如果在其生存期内的某一时刻已经不再被引用,那么,这个对象就有可能成为垃圾

4. GC是什么,简述一下GC的工作方式?

GC 是垃圾回收(Garbage Collect)的缩写,是 .NET 核心机制的重要部分。她的基本工作原理就是遍历托管堆中的对象,标记哪些被使用对象(哪些没人使用的就是所谓的垃圾),然后把可达对象转移到一个连续的地址空间(也叫压缩),其余的所有没用的对象内存被回收掉。

5. GC进行垃圾回收时的主要流程是?

(1)标记:先假设所有对象都是垃圾,根据应用程序根Root遍历堆上的每一个引用对象,生成可达对象图,对于还在使用的对象(可达对象)进行标记(其实就是在对象同步索引块中开启一个标示位)。

(2)清除:针对所有不可达对象进行清除操作,针对普通对象直接回收内存,而对于实现了终结器的对象(实现了析构函数的对象)需要单独回收处理。清除之后,内存就会变得不连续了,就是步骤3的工作了。

(3)压缩:把剩下的对象转移到一个连续的内存,因为这些对象地址变了,还需要把那些 Root 跟指针的地址修改为移动后的新地址。

6. GC在哪些情况下回进行回收工作?

  • 内存不足溢出时(0代对象充满时)
  • Windwos 报告内存不足时,CLR 会强制执行垃圾回收
  • CLR 卸载 AppDomian,GC 回收所有
  • 调用 GC.Collect
  • 其他情况,如主机拒绝分配内存,物理内存不足,超出短期存活代的存段门限

7. using() 语法是如何确保对象资源被释放的?如果内部出现异常依然会释放资源吗?

using() 只是一种语法形式,其本质还是 try…finally 的结构,可以保证 Dispose 始终会被执行。

8. 解释一下C#里的析构函数?为什么有些编程建议里不推荐使用析构函数呢?

C# 里的析构函数其实就是终结器 Finalize,因为长得像 C++ 里的析构函数而已。

有些编程建议里不推荐使用析构函数要原因在于:第一是 Finalize 本身性能并不好;其次很多人搞不清楚 Finalize 的原理,可能会滥用,导致内存泄露,因此就干脆别用了

9. Finalize() 和 Dispose() 之间的区别?

Finalize() 和 Dispose() 都是 .NET 中提供释放非托管资源的方式,他们的主要区别在于执行者和执行时间不同:

  • finalize 由垃圾回收器调用;dispose 由对象调用。
  • finalize 无需担心因为没有调用 finalize 而使非托管资源得不到释放,而 dispose 必须手动调用。
  • finalize 不能保证立即释放非托管资源,Finalizer 被执行的时间是在对象不再被引用后的某个不确定的时间;而 dispose 一调用便释放非托管资源。
  • 只有 class 类型才能重写 finalize ,而结构不能;类和结构都能实现 IDispose 。

另外一个重点区别就是终结器会导致对象复活一次,也就说会被 GC 回收两次才最终完成回收工作,这也是不建议开发人员使用终结器的主要原因。

10. Dispose和Finalize方法在何时被调用?

  • Dispose 一调用便释放非托管资源;
  • Finalize 不能保证立即释放非托管资源,Finalizer 被执行的时间是在对象不再被引用后的某个不确定的时间;

11. .NET中的托管堆中是否可能出现内存泄露的现象?

是的,可能会。比如:

  • 不正确的使用静态字段,导致大量数据无法被GC释放;
  • 没有正确执行 Dispose(),非托管资源没有得到释放;
  • 不正确的使用终结器 Finalize(),导致无法正常释放资源;
  • 其他不正确的引用,导致大量托管对象无法被 GC 释放;

12. 在托管堆上创建新对象有哪几种常见方式?

  • new 一个对象;
  • 字符串赋值,如 string s1=”abc”;
  • 值类型装箱;

参考:

.NET面试题解析(06)-GC与内存管理

内存管理

GC的内存管理的目标主要都是引用类型对象。

对象创建及生命周期

一个对象的生命周期简单概括就是:创建 > 使用 > 释放,在.NET中一个对象的生命周期:

  • new创建对象并分配内存
  • 对象初始化
  • 对象操作、使用
  • 资源清理(非托管资源)
  • GC垃圾回收

大部分的对象创建都是开始于关键字 new,有个别引用类型是由专门 IL 指令的,比如 string 有 ldstr 指令。

引用对象都是分配在托管堆上的, 先来看看托管堆的基本结构,如下图,托管堆中的对象是顺序存放的,托管堆维护着一个指针 NextObjPtr,它指向下一个对象在堆中的分配位置。

82a2b4be6318aebc0dc9b7b29c2a7599.png

创建一个新对象的主要流程:

151257-20160309235623241-1001221060.png

模拟一个对象的创建过程:

1
2
3
4
5
6
7
8
public class User
{
public int Age { get; set; }
public string Name { get; set; }

public string _Name = "123" + "abc";
public List<string> _Names;
}
  • 1,对象大小估算,共计44个字节:
    (1) 属性 Age 值类型 Int,4字节;
    (2) 属性 Name,引用类型,初始为 NULL,4个字节,指向空地址;
    (3) 字段 _Name 初始赋值了,代码会被编译器优化为 _Name=”123abc” 。一个字符两个字节,字符串占用 2×6+8(附加成员:4字节 TypeHandle 地址,4字节 同步索引块 )= 20 字节,总共 内存大小 = 字符串对象20字节 + _Name 指向字符串的内存地址4字节=24字节;
    (4) 引用类型字段 List _Names 初始默认为 NULL,4个字节;
    (5) User对象的初始附加成员(4字节 TypeHandle 地址,4字节同步索引块)8个字节;
  • 2,内存申请: 申请 44 个字节的内存块,从指针 NextObjPtr 开始验证,空间是否足够,若不够则触发垃圾回收。
  • 3,内存分配: 从指针 NextObjPtr 处开始划分 44 个字节内存块。
  • 4,对象初始化: 首先初始化对象附加成员,再调用User对象的构造函数,对成员初始化,值类型默认初始为0,引用类型默认初始化为 NULL;
  • 5,托管堆指针后移: 指针 NextObjPtr 后移44个字节。
  • 6,返回内存地址: 返回对象的内存地址给引用变量。

GC垃圾回收

GC是垃圾回收( Garbage Collect)的缩写,是 .NET 核心机制的重要部分。她的基本工作原理就是遍历托管堆中的对象,标记哪些被使用对象(那些没人使用的就是所谓的垃圾),然后把可达对象转移到一个连续的地址空间(也叫压缩),其余的所有没用的对象内存被回收掉。

151257-20160309235624382-1396676769.png

垃圾回收基本流程

标记阶段

先假设所有对象都是垃圾,根据应用程序根指针 Root 遍历堆上的每一个引用对象,生成可达对象图,对于还在使用的对象(可达对象)进行标记(其实就是在对象同步索引块中开启一个标示位)。

其中 Root 根指针保存了当前所有需要使用的对象引用,他其实只是一个统称,意思就是这些对象当前还在使用,主要包含:静态对象/静态字段的引用;线程栈引用(局部变量、方法参数、栈帧);任何引用对象的 CPU 寄存器;根引用对象中引用的对象;GC Handle table;Freachable 队列等。

对性能的影响

标记阶段是一个 “几乎仅有只读操作” 的阶段。这个阶段中没有任何对象被移动,也没有任何内存被回收。

  • 1,当进行一次完整的标记时,垃圾回收器几乎遍历了每一个被引用的对象。若这部分数据并不存在于程序工作区(working set)中就会造成页面错误,从而导致重新加载对象时缓存丢失(cache miss)与缓存抖动(cache thrashing)。
  • 2,在一个多处理器系统中,当垃圾回收器在对象的头部进行位标记操作时,若相应的对象已被加载至其他处理器的缓存中,则会造成缓存失效。
  • 3,标记阶段的性能取决于引用图中对象的数目,但是和对象占用的内存大小并无关系。

垃圾回收器遍历对象图中所有被应用程序引用的对象。为了正确的遍历图中的对象,需要选定一系列起点保证引用对象的遍历,这些起点称为

1.局部根

局部变量是一种最显而易见的根。

2.静态根

另一种类型的根是 静态变量。静态成员类型在类型加载时被创建,并且可以在整个应用程序域( application domain )的生命周期内作为潜藏根。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Program
{
static event EventHandler ButtonClick;
static void Main(string[] args)
{
while (true)
{
Button button = new Button();
ButtonClick += button.OnClick;
}
}
}

class Button
{
public void OnClick(object sender, EventArgs e) { }
}

上面代码会造成内存泄漏。因为静态事件包含一个委托列表,而这个委托列表引用了我们创建的对象。
事实上,.NET 内存泄漏的最普遍原因是静态变量引用了对象。

可以使用 SOS.DLL 查看根,!gcroot 命令提供跟得类型和引用链的简明信息。

清理阶段与压缩阶段

清理

针对所有不可达对象进行清除操作,针对普通对象直接回收内存,而对于实现了终结器的对象(实现了析构函数的对象)需要单独回收处理。清除之后,内存就会变得不连续了,这时就需要压缩阶段了。

建议不要随意手动调用垃圾回收 GC.Collect(),GC 会选择合适的时机、合适的方式进行内存回收的。

压缩

把剩下的对象转移到一个连续的内存,因为这些对象地址变了,还需要把那些 Root 根指针的地址修改为移动后的新地址。

net-mem-02-mark-compact.png

性能影响

  • 1,移动对象意味着内存复制,这对于内存占用多的对象来说是昂贵的开销。
  • 2,对象被移动之后,所有的引用的值必须更新其地址。对于频繁引用的对象来说,这种分散的内存操作势必造成性能影响。

固定

该场景涉及到托管对象传递给非托管代码。

垃圾回收器的特性

垃圾回收器是如何和其他应用程序线程(通常称为赋值线程,mutator thread)进行交互的。

垃圾回收时暂停线程

垃圾回收器和其他应用程序线程并发执行可以产生的问题:

  • 假阴性(false negative):一个对象满足垃圾回收的条件,但被标记为活动的。
  • 假阳性(false positive):一个对象被认为是可回收对象,但它依然被应用程序所引用。尽量避免该情况发生。

在垃圾回收时挂起线程

垃圾回收时会在安全点(safe point)出挂起线程,JIT 编译器通过生成额外的信息确保只有安全的时候才挂起线程进行垃圾回收。而 CLR 也会尝试安全的挂起线程。

非托管线程并不会由于托管线程的挂起而受到影响,除非它已经切换回了托管线程,这一过程有平台调用转换器负责的。

在标记阶段挂起线程

出现假阳性
如果垃圾回收器在对象创建之前,已经完成了相关部分引用图更新,那么一个刚刚被创建的对象,即使已经被应用程序引用,也可能判断为未引用。

出现假阴性
对于一个已经被标记的对象,如果它的最后一个引用在标记阶段被移除,那么这个本该被回收的对象就会继续存活。
如果对象真的不可达,那就不可能重新变成可达状态,它将在下一轮垃圾回收周期中被回收。

在清理阶段挂起线程

CLR 禁止应用程序和垃圾回收过程并发执行。

工作站垃圾回收

工作站垃圾回收(workstation GC),分为并发工作站垃圾回收非并发工作站垃圾回收

并发工作站垃圾回收

并发工作站垃圾回收 是默认的特征。在并发工作站垃圾回收下,有一个独立的专门的垃圾回收线程。该线程在执行垃圾回收的过程时,始终具有 THRAD_PRIORITY_HIGHEST 优先级。CRL 可以决定是否允许某些垃圾回收阶段与应用程序线程并发执行。
图形界面应用程序应该尽量避免从 UI 线程触发垃圾回收,即在后台线程进行资源分配,且不要显示的从 UI 线程调用 GC.Collect 方法。因为UI线程被垃圾回收阻塞的同时,其它应用程序线程却在和垃圾回收过程争抢资源。

非并发工作站垃圾回收

非并发工作站垃圾回收特征在标记和清理阶段均会挂起应用程序线程。适用于从 UI 线程触发垃圾回收的情形。
由于在 UI 线程等待垃圾回收时,其它的后台线程不会和垃圾回收器争夺资源,一次 UI 线程可以即可恢复响应。

服务器垃圾回收

服务器垃圾回收专门针对服务器应用程序进行了优化。

使用服务器垃圾回收特征的唯一限制是物理处理器的数目。如果仅有一个处理器,则只能够选择工作站垃圾回收特征。

由于并发工作站垃圾回收是默认的垃圾回收特征,一般运行于默认的 CLR 宿主下的命令行,windonws 应用程序,windows 服务都使用默认的垃圾回收特征;不在默认的 CLR 宿主运行的应用程序则可以选择其他的垃圾回收特征。由于 IIS 大多安装在服务器上,在 IIS ASP.NET 宿主下运行的应用程序使用的就是服务器垃圾回收特征(可以通过 web.config 修改这个定义)

切换垃圾回收特征

在默认的宿主下,可以通过应用程序配置文件( app.config )修改垃圾回收特征及其子特征。

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>

<configuration>
<runtime>
<gcServer enabled="true"/>
<gcConcurrent enabled="false"/>
</runtime>
</configuration>

安全的使用低延迟垃圾回收

安全的使用低延迟垃圾回收的唯一途径是将其放在受限执行区域(constrained execution region,CER)内。

需要引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System.Runtime;
using System.Runtime.CompilerServices;

GCLatencyMode oldModel = GCSettings.LatencyMode;
RuntimeHelpers.PrepareConstrainedRegions();

try
{
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
// Perform time-sensitive work here (执行时间敏感操作)
}finally
{
GCSettings.LatencyMode = oldModel;
}

代龄(Generation)

.NET 垃圾回收器的“代”模型(generational model)使用局部垃圾回收进行性能优化。

和真实世界的人和动物不同,.NET 认为年轻的对象更容易死亡,而年老的对象更倾向于存货。
年轻和年老的定义取决于应用程序垃圾回收的频率。在大多数应用程序中,临时对象(在一个方法张中分配的对象)大多是年轻的,而随着应用程序初始化的对象大多是年老的。

分代(Generation)算法 是 CLR 垃圾回收器采用的一种机制,它唯一的目的就是提升应用程序的性能。分代回收,速度显然快于回收整个堆。分代( Generation )算法的假设前提条件:

  • 1、大量新创建的对象生命周期都比较短,而较老的对象生命周期会更长
  • 2、对部分内存进行回收比基于全部内存的回收操作要快
  • 3、新创建的对象之间关联程度通常较强。heap 分配的对象是连续的,关联度较强有利于提高 CPU cache 的命中率

在 “代”模型 中,托管堆被划分为三个区域:第0代,第1代和第2代。

  • 第0代,最新分配在堆上的对象,从来没有被垃圾收集过。任何一个新对象,当它第一次被分配在托管堆上时,就是第0代(大于85000的大对象除外)。
  • 第1代,0代满了会触发0代的垃圾回收,0代垃圾回收后,剩下的对象会搬到1代。
  • 第2代,当0代、1代满了,会触发0代、1代的垃圾回收,第0代升为第1代,第1代升为第2代。

net-mem-06-generation.png

大部分情况,GC 只需要回收0代即可,这样可以显著提高 GC 的效率,而且 GC 使用启发式内存优化算法,自动优化内存负载,自动调整各代的内存大小。

如果 Gen 0 heap 内存达到阀值,则触发0代GC,0代GC后Gen 0中幸存的对象进入Gen1。如果Gen 1的内存达到阀值,则进行1代GC,1代GC将Gen 0 heap和Gen 1 heap一起进行回收,幸存的对象进入Gen2。

  2代 GC 将Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收,Gen 0和Gen 1比较小,这两个代龄加起来总是保持在16M左右;Gen2的大小由应用程序确定,可能达到几G,因此0代和1代GC的成本非常低,2代GC称为full GC,通常成本很高。粗略的计算0代和1代GC应当能在几毫秒到几十毫秒之间完成,Gen 2 heap比较大时,full GC可能需要花费几秒时间。大致上来讲 .NET 应用运行期间,2代、1代和0代GC的频率应当大致为1:10:100。

第0代

第0代通常以 256KB-4MB 起步,根据使用情况缓慢增长。当第0代已满而不能完成一个新的内存分配请求时,第0代就会发起一次垃圾回收。
由于第0代的大小受高速缓存大小的影响,因此很可能在高速缓存中就可以找到第0代的所有对象。第0代的所有对象几乎都在一次垃圾回收结束之后清理殆尽。
但是,仍然有一些对象会出于各种原因得以存活。

  • 应用程序具有糟糕的行为,分配的临时对象在一次垃圾回收之后仍然存活。
  • 应用程序处于初始化阶段,这个阶段分配的对象多是长生命周期对象。
  • 应用程序创建的是短期临时对象,但是在垃圾回收触发时这些对象恰巧正在被使用。

这些撑过一次垃圾回收的对象不会被排列在第0代的起始位置。它们会被提升到第1代。

跨代移动固定对象

垃圾回收器无法移动固定的对象。但是 CLR 用一种非常巧妙的方法完成了固定对象的代升级:如果第0代由于固定对象的原因严重碎片化了,则 CLR 可以将整个第0代声明为更高代,并将一块新的内存声明为第0代,在这块内存中处理新的分配请求。

第1代

第1代是第0代和第2代之间的缓冲区域。第1代典型的初始长度为 512KB-4MB。当第1代被填满时,会在第1代触发垃圾回收。只有第1代中的对象会被垃圾回收器标记和清除。自然触发第1代垃圾回收的唯一时机是在第0代垃圾回收后,存活的对象被提升至第1级时(另一种是手动触发垃圾回收)。

第2代

第2代是在至少两次垃圾回收过程后存活对象的终极区域(还有大对象),在“代”模型中,这些对象属于老年对象,这些对象不太可能在短期内回收。第2代区域不会人为的进行大小限制,它可以扩展到整个系统进程的专用内存空间,在32位系统中为2GB,在64位系统中最多为8TB。第2代中设定了动态阈值(水印)以触发垃圾回收操作。

若垃圾回收发生在第二代,则这是一个完全的垃圾回收。这是昂贵的,需要消耗最多的时间。

大对象堆

大对象堆(large object heap,LOH)是一个专门容纳大对象的特殊区域。指内存占用大于85KB(85000字节)的对象。这个区域的主要特点就是:不会轻易被回收;就是回收了也不会被压缩(因为对象太大,移动复制的成本太高了)。

这个大小指对象本身的大小而非以该对象为根的整个对象树的大小。因此,包含1000个字符串(每个字符串含有100个字符)的数组不是一个大对象,但是一个长度为50000的整数数组是一个大对象。

大对象从 LOH 中直接分配而不会放在第0代,第1代或者第2代中。

垃圾回收在 LOH 上进行回收时,并不会清理大对象并进行数据复制。LOH 维护了一个未使用内存的链表,新的内存分配请求可以从链表中进行。

当第2代对象占用的内存达到阈值时,则 LOH 就会进行垃圾回收。类似的,若 LOH 占用的内存达到阈值,也会触发第2代垃圾回收。因此,创建太多大型的临时对象也会造成类似 “中年危机” –必须进行完全回收以释放这些对象。LOH 中的碎片是另一个潜在问题,因为 LOH 中对象之间的空洞无法在清理阶段被移除并达到对堆进行碎片整理的效果。一个有效的策略是缓存并重用大对象,或者是(如果数组中对象的类型是一致的)分配一个大对象,然后在需要时手动将其分成小块。

GC.Collect() 方法

作用:强制进行垃圾回收。

Collect():强制对所有代进行即时垃圾回收。

Collect(Int32):强制对零代到指定代进行即时垃圾回收。

Collect(Int32, GCCollectionMode):强制在 GCCollectionMode 值所指定的时间对零代到指定代进行垃圾回收

GC注意事项:

  • 1、只管理内存,非托管资源,如文件句柄,GDI 资源,数据库连接等还需要用户去管理。
  • 2、循环引用,网状结构等的实现会变得简单。GC 的标志-压缩算法能有效的检测这些关系,并将不再被引用的网状结构整体删除。
  • 3、GC 通过从程序的根对象开始遍历来检测一个对象是否可被其他对象访问,而不是用类似于 COM 中的引用计数方法。
  • 4、GC 在一个独立的线程中运行来删除不再被引用的内存。
  • 5、GC 每次运行时会压缩托管堆。
  • 6、你必须对非托管资源的释放负责。可以通过在类型中定义Finalizer来保证资源得到释放。
  • 7、对象的 Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间。注意并非和C++中一样在对象超出声明周期时立即执行析构函数
  • 8、Finalizer的使用有性能上的代价。需要Finalization的对象不会立即被清除,而需要先执行Finalizer.Finalizer,不是在GC执行的线程被调用。GC把每一个需要执行Finalizer的对象放到一个队列中去,然后启动另一个线程来执行所有这些Finalizer,而GC线程继续去删除其他待回收的对象。在下一个GC周期,这些执行完Finalizer的对象的内存才会被回收。
  • 9、.NET GC使用”代”(generations)的概念来优化性能。代帮助GC更迅速的识别那些最可能成为垃圾的对象。在上次执行完垃圾回收后新创建的对象为第0代对象。经历了一次GC周期的对象为第1代对象。经历了两次或更多的GC周期的对象为第2代对象。代的作用是为了区分局部变量和需要在应用程序生存周期中一直存活的对象。大部分第0代对象是局部变量。成员变量和全局变量很快变成第1代对象并最终成为第2代对象。
  • 10、GC 对不同代的对象执行不同的检查策略以优化性能。每个 GC 周期都会检查第0代对象。大约1/10的 GC 周期检查第0代和第1代对象。大约1/100的GC周期检查所有的对象。重新思考Finalization的代价:需要Finalization的对象可能比不需要 Finalization 在内存中停留额外9个GC周期。如果此时它还没有被Finalize,就变成第2代对象,从而在内存中停留更长时间。

跨代引用

一个高代对象引用一个低代对象的情形只会在一种类型的语句中出现:将一个非空引用类型对象赋值给一个引用类型的实例的成员变量(或者复制给一个数组的元素)。

非托管资源回收

  非托管资源不受 CLR 或者垃圾回收器的管理(如内核对象句柄,数据库连接和非托管内存等)。释放非托管资源需要使用 终结化特性,即一个对象与一段特定的代码关联起来。这段代码必须在该对象(代表一个非托管资源)不再需要时执行。

常见的有:ApplicationContext, Brush, Component, ComponentDesigner, Container, Context, Cursor, FileStream, Font, Icon, Image, Matrix, Object, OdbcDataReader, OleDBDataReader, Pen, Regex, Socket, StreamWriter, Timer, Tooltip, 文件句柄, GDI资源, 数据库连接等等资源。

.NET 中提供释放非托管资源的方式主要是:Finalize() 和 Dispose()。

.NET的GC机制有这样两个问题:

  • GC并不是能释放所有的资源。它不能自动释放非托管资源。
  • GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。

  GC 并不是实时性的,这会造成系统性能上的瓶颈和不确定性。所以有了 IDisposable 接口,IDisposable 接口定义了 Dispose 方法,这个方法用来供程序员显式调用以释放非托管资源。使用 using 语句可以简化资源管理。

当你用 Dispose 方法释放未托管对象的时候,应该调用 GC.SuppressFinalize。如果对象正在终结队列( finalization queue ), GC.SuppressFinalize 会阻止 GC 调用 Finalize 方法。因为 Finalize 方法的调用会牺牲部分性能。如果你的 Dispose 方法已经对委托管资源作了清理,就没必要让 GC 再调用对象的 Finalize 方法。

终结器

所有实现了终结器(析构函数)的对象,会被 GC 特殊照顾,GC 的终止化队列跟踪所有实现了 Finalize 方法(析构函数)的对象。

  • 当 CLR 在托管堆上分配对象时,GC 检查该对象是否实现了自定义的 Finalize 方法(析构函数)。如果是,对象会被标记为可终结的,同时这个对象的指针被保存在名为终结队列的内部队列中。终结队列是一个由垃圾回收器维护的表,它指向每一个在从堆上删除之前必须被终结的对象。
  • 当 GC 执行并且检测到一个不被使用的对象时,需要进一步检查“终结队列”来查询该对象类型是否含有 Finalize 方法,如果没有则将该对象视为垃圾,如果存在则将该对象的引用移动到另外一张 Freachable 列表,此时对象会被复活一次。
  • CLR 将有一个单独的高优先级线程负责处理 Freachable 列表,就是依次调用其中每个对象的 Finalize 方法,然后删除引用,这时对象实例就被视为不再被使用,对象再次变成垃圾。
  • 下一个 GC 执行时,将释放已经被调用 Finalize 方法的那些对象实例。

简单总结:Finalize() 可以确保非托管资源会被释放,但需要很多额外的工作(比如终结对象特殊管理),而且 GC 需要执行两次才会真正释放资源。唯一的优点就是不需要显示调用。

Finalization Queue 和 Freachable Queue

  这两个队列和 .NET 对象所提供的 Finalize 方法有关。这两个队列并不用于存储真正的对象,而是存储一组指向对象的指针。当程序中使用了 new 操作符在 Managed Heap 上分配空间时,GC 会对其进行分析,如果该对象含有 Finalize 方法则在 Finalization Queue 中添加一个指向该对象的指针。

  在 GC 被启动以后,经过 Mark 阶段分辨出哪些是垃圾。再在垃圾中搜索,如果发现垃圾中有被 Finalization Queue 中的指针所指向的对象,则将这个对象从垃圾中分离出来,并将指向它的指针移动到 Freachable Queue 中。这个过程被称为是对象的复生(Resurrection),本来死去的对象就这样被救活了。为什么要救活它呢?因为这个对象的 Finalize 方法还没有被执行,所以不能让它死去。Freachable Queue 平时不做什么事,但是一旦里面被添加了指针之后,它就会去触发所指对象的 Finalize 方法执行,之后将这个指针从队列中剔除,这是对象就可以安静的死去了。

  .NET Framework 的 System.GC 类提供了控制Finalize的两个方法,ReRegisterForFinalize 和 SuppressFinalize。前者是请求系统完成对象的 Finalize 方法,后者是请求系统不要完成对象的 Finalize 方法。ReRegisterForFinalize 方法其实就是将指向对象的指针重新添加到 Finalization Queue 中。这就出现了一个很有趣的现象,因为在 Finalization Queue 中的对象可以复生,如果在对象的 Finalize 方法中调用 ReRegisterForFinalize 方法,这样就形成了一个在堆上永远不会死去的对象,像凤凰涅槃一样每次死的时候都可以复生。

手动确定性终结化

手动确定性终结化,需要客户端释放资源。

自动非确定性终结化

Finalize 来自 System.Object 中受保护的虚方法 Finalize,无法被子类显示重写,也无法显示调用。她的作用就是用来释放非托管资源,由 GC 来执行回收,因此可以保证非托管资源可以被释放。

  • 无法被子类显示重写:.NET 提供类似 C++ 析构函数的形式来实现重写,因此也有称之为析构函数,但其实她只是外表和C++ 里的析构函数像而已。
  • 无法显示调用:由 GC 来管理和执行释放,不需要手动执行了,不用担心忘记调用Dispose。

  任何一种类型都可以通过重写 System.Object 定义的(protected)Finalize 方法表明该类型需要进行自动化终结。例如在 File 类中自动终结化方法 ~File(终结器,析构函数)。在对象将被销毁时,该方法必须得到执行。值类型只有在装箱的情况下才有垃圾回收的必要。

  当一个拥有终结器的对象被创建时,他的一个引用将被添加到一个特殊的运行时队列上,成为终结队列(finalization queue)。这个队列被垃圾回收器界定为根。这意味着即使应用程序没有针对这个引用对象的引用,它仍然会子啊终结队列上保持活动状态。

  当这个对象不再被应用程序引用,并开始垃圾回收时,若垃圾回收发现唯一一个针对该对象的引用来自于终结队列,则它会将这个对象的引用移动到另一个运行时管理的队列上,成为终结可达队列(freacheable queue)。该队列仍然被界定为根,因而,该对象仍然在被引用并保持存活。

  对象的终结器不会在垃圾回收的过程中执行。在 CLR 初始化的过程会创建一个特殊的线程,称为 终结器线程(finalizaer thread)(每一个进程只会有一个终结器线程,这和垃圾回收器特征无关,该线程运行在 THREAD_PRIORITY_HIGHEST 优先级上)。这个线程会反复等待终结化事件(finalization event)的触发。在垃圾回收器完成垃圾回收并触发该事件后,如果终结可达队列中有对象放入,则终结器线程就会被激活。终结器线程将对象的引用从终结可达队列中移除,同时同步执行对象上的终结器方法。而当下一次垃圾回收开始时,由于该对象再无引用,因此垃圾回收可以将其内存回收。

非确定性终结的缺点

  • 有终结器的对象将至少位于第1代,更容易经历“中年危机”,从而更容易进行多次完整回收。
  • 终结器线程的压力(有很多对象需要进行终结化)可能导致内存泄漏。如果应用程序线程分配对象的频率比终结器线程终结化对象的频率更高,那么应用程序将从等待终结化的对象中持续的泄漏内存。
  • 自动非确定性终结化还是很多难以发现或调试错误的来源。因为终结化是异步的,因此多个对象的终结顺序是难以保证的。

终结器也许永远不会被调用

  终结器无法在进程被野蛮的关闭时执行。如用户通过任务管理器或者 TerminateProcess API 终止线程的执行,则终结器将无法进行资源回收。

对象的复活

终结化为对象提供了一个在其不被应用程序引用的情况下执行任意代码的机会。这种机会可以用于创建一个应用程序到该对象的引用,在对象即将失效时,另其起死回生,这种能力称为复活(resurrection)。主要的风险是,该对象引用的其它对象有可能已经被终结化而处于无效状态,此时,只能重新初始化该对象所有引用的对象;另一个问题是该对象的终结器不会被执行,因此你需要将该对象的引用作为参数传递个 GC.ReRegisterForFinnalize 方法。

适用于复活机制的场景之一就是对象池(object pooling)。

Dispose模式

.Net 规定所有需要进行确定终结化的对象都必须实现 IDisposable 接口。而该接口仅有一个方法,即 Dispose 方法。这个方法会释放非托管资源,进行确定终结化。

Dispose 需要手动调用:

1
2
3
4
5
6
7
8
9
10
//方式1:显示接口调用
SomeType st1=new SomeType();
//do sth
st1.Dispose();

//方式2:using()语法调用,自动执行Dispose接口
using (var st2 = new SomeType())
{
//do sth
}
  • 第一种方式,显示调用,缺点显而易见,如果程序猿忘了调用接口,则会造成资源得不到释放。或者调用前出现异常,当然这一点可以使用 try…finally 避免。

  • 建议使用第二种实现方式,他可以保证无论如何 Dispose 接口都可以得到调用,原理其实很简单,using() 的 IL 代码如下图,因为 using 只是一种语法形式,本质上还是 try…finally 的结构。

151257-20160309235625475-414934067.png

微软是推荐同时实现 IDisposable 接口和 Finalize (析构函数),其实 FCL 中很多类库都是这样实现的,这样可以兼具两者的优点:

  • 如果调用了 Dispose,则可以忽略对象的终结器,对象一次就回收了;
  • 如果忘了调用 Dispose,则还有一层保障,终结器会负责对象资源的释放;

性能优化建议

尽量不要手动执行垃圾回收的方法:GC.Collect()

垃圾回收的运行成本较高(涉及到了对象块的移动、遍历找到不再被使用的对象、很多状态变量的设置以及 Finalize 方法的调用等等),对性能影响也较大,因此我们在编写程序时,应该避免不必要的内存分配,也尽量减少或避免使用 GC.Collect() 来执行垃圾回收,一般 GC 会在最适合的时间进行垃圾回收。

需要注意,在执行垃圾回收的时候,所有线程都是要被挂起的(如果回收的时候,代码还在执行,那对象状态就不稳定了,也没办法回收了)。

推荐 Dispose 代替 Finalize

如果你了解 GC 内存管理以及 Finalize 的原理,可以同时使用 Dispose 和 Finalize 双保险,否则尽量使用 Dispose。

选择合适的垃圾回收机制:工作站模式、服务器模式。

弱引用

弱引用(weak reference)是用于管理托管对象引用的附加机制。典型的对象引用(通常为强引用(strong reference))是明确的:只要还拥有一个对象的引用,那么这个对象就会存活。这种行为的正确性是有垃圾回收器保证的。

但在某些情况下,我们还希望有一种隐形的 “绳索”,即能够绑在对象上,又不影响垃圾回收器回收它占用的内存。如果垃圾回收器回收了这个对象,那么我们可以探测到绳索的一端和对象断开了。如果垃圾回收器还没有处理这个对象,我们可以牵动“绳索”来获得这个对象的一个强引用并在此使用这个对象。

常见的场景:

  • 在不保持对象的存活的情况下提供外部服务。例如,定时器和事件服务可以为对象所用,但并不需要维持对象的引用。这可以避免很多典型的内存泄漏问题。
  • 可以自动管理缓存或池策略。一个缓存可以保有最近使用对象的弱引用而不妨碍他们被回收。一个池可以划分为一个非常小的部分用以维持一小部分对象的强引用,以及另外一个可选部分对象的弱引用。
  • 用以保存一个大对象的引用,并寄希望于它不会被回收。应用程序可以持有一个需要长时间才能初始化的大对象的弱引用。若这个对象被回收,则重新初始化该对象,否则可以在需要时直接复用该对象。

引用程序代码可以通过 System.WeakReference 类来使用弱引用。可以基于 System.WeakReference 实现事件,这样可以规避.NET内存泄漏的元凶–忘记反注册事件。

弱引用默认不会跟踪对象的复活。若想允许复活追踪功能,就使用重载的构造函数并将第二个参数传参为true。

允许追踪对象复活状态的弱引用称为长弱引用(long weak reference);不追踪对象复活状态的弱引用称为短弱引用(short weak reference)。

垃圾回收句柄

弱引用是一类特殊的垃圾回收句柄( GC handle )。一个垃圾回收句柄是一个特殊的底层值类型,它为对象引用提供了如下的功能。

  • 维持一个对象的标准的(强)引用,防止对象被回收。以 GCHandleType.Normal 表示。
  • 维持一个对象的短弱引用。以 GCHandleType.Weak 表示。
  • 维持一个对象的长弱引用。以 GCHandleType.WeakTrackResurrection 表示。
  • 维持一个对象的引用,并进行固定,以防止它在内存中被移动。如果需要还可以获得该对象的地址。以 GCHandleType.Pinned 表示。

实际开发中,我们几乎没有直接使用垃圾回收句柄的需要。但是它们,作为另一种可以保持对象的根,经常出现在诊断结果中。

参考:

.NET面试题解析(06)-GC与内存管理

C#技术漫谈之垃圾回收机制(GC)

[.Net性能优化]

CSharp转换关键字

explicit(显示转换)

explicit:必须通过转换来调用的用户定义的类型转换运算符。

  此转换运算符从源类型转换为目标类型。 源类型提供转换运算符。 不同于隐式转换,显式转换运算符必须通过转换的方式来调用。 如果转换操作会导致异常或丢失信息,则应将其标记为 explicit。 这可阻止编译器静默调用可能产生意外后果的转换操作。
省略转换将导致编译时错误 CS0266。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct Digit
{
byte value;
public Digit(byte value)
{
if (value > 9)
{
throw new ArgumentException();
}
this.value = value;
}

// Define explicit byte-to-Digit conversion operator:
public static explicit operator Digit(byte b)
{
Digit d = new Digit(b);
Console.WriteLine("conversion occurred");
return d;
}
}

class ExplicitTest
{
static void Main()
{
try
{
byte b = 3;
Digit d = (Digit)b; // explicit conversion
}
catch (Exception e)
{
Console.WriteLine("{0} Exception caught.", e);
}
}
}
/*
Output:
conversion occurred
*/

implicit(隐式转换)

implicit 关键字用于声明隐式的用户定义类型转换运算符。 如果可以确保转换过程不会造成数据丢失,则可使用该关键字在用户定义类型和其他类型之间进行隐式转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Digit
{
public Digit(double d) { val = d; }
public double val;
// ...other members

// User-defined conversion from Digit to double
public static implicit operator double(Digit d)
{
return d.val;
}
// User-defined conversion from double to Digit
public static implicit operator Digit(double d)
{
return new Digit(d);
}
}

class Program
{
static void Main(string[] args)
{
Digit dig = new Digit(7);
//This call invokes the implicit "double" operator
double num = dig;
//This call invokes the implicit "Digit" operator
Digit dig2 = 12;
Console.WriteLine("num = {0} dig2 = {1}", num, dig2.val);
Console.ReadLine();
}
}
/*
Output:
num = 7 dig2 = 12
*/

operator(运算符)

operator作用:

  • 1, 重载内置运算符
  • 2, 在类或结构声明中提供用户定义的转换

若要在自定义类或结构上重载运算符,可以在相应的类型中创建运算符声明。 重载内置 C# 运算符的运算符声明必须满足以下规则:

  • 同时包含 public 和 static 修饰符。
  • 包含 operator X,其中 X 是被重载运算符的名称或符号。
  • 一元运算符具有一个参数,二元运算符具有两个参数。 在每种情况下,都必须至少有一个参数与声明运算符的类或结构的类型相同。

注:
1,一元运算符:++,–,!;
2,二元运算符:+,-,* /;
3,三元运算符:a=3>4?3:4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Fraction
{
int num, den;
public Fraction(int num, int den)
{
this.num = num;
this.den = den;
}

// overload operator +
public static Fraction operator +(Fraction a, Fraction b)
{
return new Fraction(a.num * b.den + b.num * a.den,
a.den * b.den);
}

// overload operator *
public static Fraction operator *(Fraction a, Fraction b)
{
return new Fraction(a.num * b.num, a.den * b.den);
}

// user-defined conversion from Fraction to double
public static implicit operator double(Fraction f)
{
return (double)f.num / f.den;
}
}

class Test
{
static void Main()
{
Fraction a = new Fraction(1, 2);
Fraction b = new Fraction(3, 7);
Fraction c = new Fraction(2, 3);
Console.WriteLine((double)(a * b + c));
}
}
/*
Output
0.880952380952381
*/

参考:

https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/explicit

DotNet解决事件(Event)内存泄漏通用方法

  一个生命周期较短的对象(对象A)注册到一个生命周期较长(对象B)的某个事件(Event)上,两者便无形之间建立一个引用关系(B引用A)。这种引用关系导致GC在进行垃圾回收的时候不会将A是为垃圾对象,最终使其常驻内存(或者说将A捆绑到B上,具有了和B一样的生命周期)。这种让无用的对象不能被GC垃圾回收的现象,在托管环境下就是一种典型的内存泄漏问题。

代码下载
提取码:xazz

事实上,.NET内存泄漏的最普遍原因是静态变量引用了对象。

造成事件(Event)内存泄漏的原因

事件本质上就是一个System.Delegate对象。
Delegate分解成两个部分:委托的事情和委托的对象。与之相似地,.NET的Delegate对象同样可以分解成两个部分:委托的功能(Method)和目标对象(Target),这可以直接从Delegate的定义就可以看出来:

1
2
3
4
5
6
public abstract class Delegate : ICloneable, ISerializable
{
// Others
public MethodInfo Method { get; }
public object Target { get; }
}

常用的事件处理类型EventHandlerEventHandler<TEventArgs>本质上就是一个Delegate。

他们继承自System.MulticastDelegateMulticastDelegate派生于Delegate

1
2
3
4
5
[Serializable, ComVisible(true)]
public delegate void EventHandler(object sender, EventArgs e);

[Serializable]
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs: EventArgs;

  经过简单一句事件注册代码就通过一个EventHandler(EventHandler)事件的源(TodoListManager)和事件的监听者(TodoListForm)两着关联起来,三者之间的关系如下图所示。从这张图中我们可以看到:TodoListForm实际上是通过注册的EventHandler的Target属性被TodoListManager间接引用着的。所以才会导致TodoListForm在关闭之后,永远不能拿成为垃圾对象,因为TodoListManager是一个基于static属性定义的Singleton对象,永远是GC的根。

image_thumb_2.png

解决方法

  当对象A注册到B的某个事件上,A并不受到B的“强制引用”。既然不能“强引用(Strong Reference)”,那就只能是“弱引用(Weak Reference)”。通过System.WeakReference来解决这个问题。

方法:采取某种机制,让事件源(Event Source)的EventHandler通过WeakReference的方式与事件监听者建立关系。只有在这种情况下,事件监听者没有了事件源的强制引用,在我们不用的时候才能及时成为垃圾对象,等待GC对它的清理。

image_thumb_3.png

  通过传入EventHandler<TEventArgs>对象构造WeakReferenceHandler,在EventHandler的Target属性基础上建立WeakReference对象,在执行处理事件的时候通过该WeakReference找到真正的目标对象,如果找得到则通过反射在其基础上调用相应的方法;反之,如果通过不能得到Target,那么表明该事件的监听对象已经被GC当作垃圾对象回收掉了。为了在注册事件的时候方遍,特定义了一个隐式的类型转换:WeakReferenceHandler转换成EventHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System;
using System.Reflection;
namespace Artech.MemLeakByEvents
{
public class WeakEventHandler<TEventArgs> where TEventArgs : EventArgs
{
public WeakReference Reference
{ get; private set; }

public MethodInfo Method
{ get; private set; }

public EventHandler<TEventArgs> Handler
{ get; private set; }

public WeakEventHandler(EventHandler<TEventArgs> eventHandler)
{
Reference = new WeakReference(eventHandler.Target);
Method = eventHandler.Method;
Handler = Invoke;
}

public void Invoke(object sender, TEventArgs e)
{
object target = Reference.Target;
if (null != target)
{
Method.Invoke(target, new object[] { sender, e });
}
}

/// <summary>
/// 隐式转换
/// </summary>
/// <param name="weakHandler"></param>
public static implicit operator EventHandler<TEventArgs>(WeakEventHandler<TEventArgs> weakHandler)
{
return weakHandler.Handler;
}
}
}

实际进行事件注册:

1
2
3
4
5
private void TodoListForm_Load(object sender, EventArgs e)
{
SynchronizationContext = SynchronizationContext.Current;
TodoListManager.Instance.TodoListChanged += new WeakEventHandler<TodoListEventArgs>(TodoListManager_TodoListChanged);
}

参考:

事件(Event),绝大多数内存泄漏(Memory Leak)的元凶[下篇] (提供Source Code下载)