@TOC


前言

搞编程的,基础不牢,地动山摇,多多复习哦,与诸君共勉。

一、特征【ObsoleteAttribute】、程序

1.[Obsolete(“提示信息”, ture/false)]:false/true代表是否停止编译,true代表。编译器发出错误提示并停止遍历,否则只发出提示信息不停止编译。提醒旧函数过时,使用新函数:

1
2
3
4
5
[Obsolete("该函数已经过时,请使用新函数NewDoSomething()", false)]
public void DoSomething()
{
//...
}

2..NET中预定义了200多个特性,细致信息在System.Reflection命名空间中的类

  • 也可以自定义特性类,咱们用特征修饰哪个程序就用[]括起来放在程序前面。自定义的特性中的构造函数中的参数是必选参数,特性中的公有变量是可选参数
  • 自定义特性时可以被其他特性修饰:
    1
    2
    3
    4
    5
    6
    7
    /// <summary>
    /// AttributeUsageAttribute类时专门修饰特性的特性,有AttributeTargets枚举,
    用来规定特性可作用于哪些程序元素,可以用多个,中间用|连接;
    有Inherited能否被继承;AllowMultiple表示能够对同一元素多次应用特性
    /// </summary>
    [AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
    internal class Program{//...}

3.程序集:我们写好的代码最终都被编译器编译为很多程序集一种逻辑上的划分

  • 两种最常见的程序集.exe和.dll
    • .exe:可执行文件。有一个主程序入口,可运行
    • .dll:动态链接库文件。无主程序入口,不可运行,智能让其他程序调用

二、反射:获取类型和程序集的信息.NET在System.Reflection命名空间中定义了一系列反射类,和System.Type类一起提供了反射功能

  • System.Reflection.Assembly类是为控制程序集【获取程序集元数据信息】而设计。在获取程序集元数据之前需要先用Assembly对象的Load()方法把程序集加载到内存
  • 动态加载、动态调用、实现插件、System.Activator.CreateInstance(XxxType, null);XxxType代表咱们用.GetType获取的类,null代表构造函数的参数、xxx as xxx,强制类型转换,前面大后面小【强制类型转换是为了得到对象的方法,咱们也可以用Type.GetMethod(“欲调用方法名”)】

三、多线程

1.创建一个线程:

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
System.Threading.Thread:
//创建一个线程要三步骤:
/// <summary>
/// 创建线程三步骤的第一步:编写入口函数。线程需入口函数,可以通过委托机制编写入口函数传递给线程。C#中与工作线程的入口函数相关的委托是ThreadStart
/// </summary>
private static void EntryPointMethod()
{

}

/// <summary>
/// 创建线程三步骤的第二步:创建入口委托。传递给线程的入口函数,必须是没有参数和返回值的函数
/// </summary>
public delegate void ThreadStart();
ThreadStart entryPoint = new ThreadStart(EntryPointMethod);

//线程需入口函数,可以通过委托机制编写入口函数传递给线程。C#中与工作线程的入口函数相关的委托是ThreadStart
/// <summary>
/// 创建线程三步骤的第三步:创建线程
/// </summary>
Thread WorkThread = new Thread(entryPoint);

注意事项:除了用委托传递线程的入口函数之外,还可以通过匿名函数创建线程,将上面三个步骤合并为一步。并且解决了咱不能使用线程前面定义的变量,也就是入口函数没有参数和返回值的缺点
Thread DrawGraphThread = new Thread(delegate()
{
//入口函数代码
}
)
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
例子,namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
#region Form1构造函数
public Form1()
{
InitializeComponent();
}
#endregion

#region 图形绘制函数
/// <summary>
/// 第一步:创建线程的入口函数
/// </summary>
private void DrawGraph()
{
int loop = 0;
int sect = 0;
float[] x = new float[31];
float[] y = new float[31];

//绘制图形10000遍,每隔200毫秒绘制一遍
while (loop <= 10000)
{
sect = (sect + 1) % 25 + 1;

//绘制图形
Graphics graphics = this.CreateGraphics();
for (int i = 0; i < sect; i++)
{
x[i] = ((float)(150 * Math.Cos(i * 2 * Math.PI / sect) + 150));
y[i] = (float)(150 * Math.Sin(i * 2 * Math.PI / sect) + 150);
}
for (int m = 0; m < sect - 1; m++)
{
for (int n = 0; n < sect; n++)
{
graphics.DrawLine(Pens.Blue, x[m], y[m], x[n], y[n]);
}
}

//线程暂停200ms
Thread.Sleep(200);

//清除图形,以便重新绘制
graphics.Clear(this.BackColor);
loop++;
}
}
#endregion

#region 声明工作线程,并创建四个按钮的事件
/// <summary>
/// 声明工作线程
/// </summary>
Thread DrawGraphThread;

/// <summary>
/// 开始按钮
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void startThreadButton_Click(object sender, EventArgs e)
{
//第二步:创建入口委托
ThreadStart entryPoint = new ThreadStart(DrawGraph);

////第三步:创建线程,并启动线程
//new Thread(entryPoint).Start();

//第三步:创建线程
DrawGraphThread = new Thread(entryPoint);

//启动线程
DrawGraphThread.Start();

//设置按钮的有效性
startThreadButton.Enabled = false;
suspendThreadButton.Enabled = true;
resumeThreadButton.Enabled = false;
abortThreadButton.Enabled = true;
}

/// <summary>
/// 暂停按钮
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void suspendThreadButton_Click(object sender, EventArgs e)
{
//暂停线程。Suspend()和Abort()方法并不是立即停止线程,对于Suspend()方法,.NET会让线程再继续执行几个指令以确保线程在安全状态下挂起
DrawGraphThread.Suspend();

//设置按钮的有效性
suspendThreadButton.Enabled = false;
resumeThreadButton.Enabled = true;
abortThreadButton.Enabled = false;

}

/// <summary>
/// 恢复按钮
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void resumeThreadButton_Click(object sender, EventArgs e)
{
//恢复线程
DrawGraphThread.Resume();

//设置按钮的有效性
resumeThreadButton.Enabled = false;
suspendThreadButton.Enabled = true;
abortThreadButton.Enabled = true;
}

/// <summary>
/// 中止按钮
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void abortThreadButton_Click(object sender, EventArgs e)
{
// 中止线程.Suspend()和Abort()方法并不是立即停止线程,对于Suspend()方法,.NET会让线程再继续执行几个指令以确保线程在安全状态下挂起
//中止线程时Abort()方法会抛出一个ThreadAbortException异常,可以保证线程中止时如果正在执行try语句的代码,可以确保对应finally块被执行,然后确保相应资源被释放
DrawGraphThread.Abort();

//设置按钮的有效性
startThreadButton.Enabled = true;
suspendThreadButton.Enabled = false;
resumeThreadButton.Enabled = false;
abortThreadButton.Enabled = false;
}
#endregion
}

2.线程的优先级可以通过Thread.Priority设置设置,Priority属性是一个ThreadPriority型枚举,5个优先级

  • 没明确指定则两个线程优先级相同均为默认值Normal,所以交替执行【被执行的几率大概相等】。而且,在默认情况下,主线程Main和工作线程的优先级相同,也是交替进行
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
//线程A
Thread ThreadA = new Thread(delegate()
{
for (int i = 0; i <= 100000000; i++)
{
if (i % 1000000 == 0)
{
Console.WriteLine('A');
}
}
});

//线程B
Thread ThreadB = new Thread(delegate ()
{
for (int i = 0; i <= 100000000; i++)
{
if (i % 1000000 == 0)
{
Console.WriteLine('B');
}
}
});

//启动线程
ThreadA.Start();
ThreadB.Start();
  • 调整优先级后,只是说明优先级高的线程占有更多的CPU事件,cpu会执行哪个看CPU的心情

3.线程的同步:需要两个线程协同工作才能完成一项任务的情况,不协同最终结果就会出错

  • 多个线程共享的资源时临界资源;访问临界资源的代码叫做临界区;
  • 一个例子:结果出错,用互锁、管程等修好
    • 运行结果有对有错,其实就是错的
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
internal class Program
{
//缓冲区
private static char buffer;



private static void Main(string[] args)
{
//写线程
Thread Writer = new Thread(delegate ()
{
string sentence = "无可奈何花落去,似曾相识燕归来,i love min.";

for (int i = 0; i < 24; i++)
{
buffer = sentence[i]; //像缓冲区写入数据
Thread.Sleep(26);
}
});

//读线程
Thread Reader = new Thread(delegate ()
{
for (int i = 0; i < 24; i++)
{
char ch = buffer; //从缓冲区读数据
Console.Write(ch);
Thread.Sleep(20);
}
});

//启动线程
Writer.Start();
Reader.Start();
}
  • .NET框架保证线程同步的同步类【看下面例子】
    • Interlocked互锁
      • 用Interlocked互锁的解决思路:为了实现同步,线程Writer在写入字符前先检查缓冲区满否?,满则等待,直到缓冲区数据被进程Reader读取;缓冲区空就写入,写入后就把缓冲区标记为满
      • numberOfUsedSpace计数器。通过System.Threading.Interlocked类控制计数器,实现进程的同步
      • 运行结果完全正确
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
internal class Program
{
//缓冲区
private static char buffer;

//标识量(表示缓冲区已使用的空间,初始值为0)
private static long numberOfUsedSpace = 0;

private static void Main(string[] args)
{
//写线程
Thread Writer = new Thread(delegate ()
{
string sentence = "无可奈何花落去,似曾相识燕归来,i love min.";

for (int i = 0; i < 24; i++)
{
while (Interlocked.Read(ref numberOfUsedSpace) == 1)
{
Thread.Sleep(10);
}


buffer = sentence[i]; //向缓冲区写入数据
//写入数据后把缓冲区标记为满
Interlocked.Increment(ref numberOfUsedSpace);
}
});

//读线程
Thread Reader = new Thread(delegate ()
{
for (int i = 0; i < 24; i++)
{
while (Interlocked.Read(ref numberOfUsedSpace) == 0)
{
Thread.Sleep(10);
}

char ch = buffer; //从缓冲区读数据
Console.Write(ch);

//读取数据后把缓冲区标记为空
Interlocked.Decrement(ref numberOfUsedSpace);
}
});

//启动线程
Writer.Start();
Reader.Start();
}
}
- Monitor管程
  - 用Monitor的解决思路:用独占锁方式控制线程同步,只有获得独占锁的线程才能访问临界资源。一线程进入临界区时先调用Monitor类的Enter()方法尝试获取临界资源的独占锁若独占锁已被其他线程占用这个线程就进入等待状态。前面线程在执行后面线程只能等待,睡在临界资源上。Monitor会记录所有睡在临界资源上的线程,执行完了退出临界区时需要调用Monitor.Pulse()方法唤醒睡眠在临界资源的线程
  - 运行结果完全正确:
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
77
78
79
80
internal class Program
{
//缓冲区
private static char buffer;

//用于同步的对象
private static object lockForBuffer = new object();

private static void Main(string[] args)
{
//写线程
Thread Writer = new Thread(delegate ()
{
string sentence = "无可奈何花落去,似曾相识燕归来,i love min.";

for (int i = 0; i < 30; i++)
{
try
{
//进入临界区
Monitor.Enter(lockForBuffer);

buffer = sentence[i]; //向缓冲区写入数据

//唤醒睡眠在临界资源上等待的线程
Monitor.Pulse(lockForBuffer);
//让当前线程睡眠再临界资源上
Monitor.Wait(lockForBuffer);
}
catch (System.Threading.ThreadInterruptedException)
{

Console.WriteLine("Writer写线程被中止"); ;
}
finally
{
//退出临界区
Monitor.Exit(lockForBuffer);
}
}
});

//读线程
Thread Reader = new Thread(delegate ()
{

for (int i = 0; i < 30; i++)
{
try
{
//进入临界区
Monitor.Enter(lockForBuffer);

//从缓冲区读取数据
char ch = buffer;
Console.Write(ch);

//唤醒睡眠在临界资源上等待的线程
Monitor.Pulse(lockForBuffer);
//让当前线程睡眠再临界资源上
Monitor.Wait(lockForBuffer);
}
catch (System.Threading.ThreadInterruptedException)
{

Console.WriteLine("Reader读线程被中止"); ;
}
finally
{
//退出临界区
Monitor.Exit(lockForBuffer);
}
}
});

//启动线程
Writer.Start();
Reader.Start();
}
}
  - 注意事项:
    - Monitor类只能锁定引用类型对象而不能锁定值类型变量。这是因为当参数为值类型变量时,每调用依次Monitor.Enter()方法时值类型要进行一次装箱操作得到一个新Object对象,Monitor类不同操作用于不同对象是不能保证原子性的
    - 为确临界区时临界资源得到释放,应该在try中用Monitor类的代码并在finally中调用Monitor.Exit()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try
{
Monitor.Enter(要锁定的对象);
//临界区代码...
}
finally
{
Monitor.Exit(要锁定的对象);
}

等价写法
lock(要锁定的对象)
{
//临界区代码...
}
    - 一个线程在某资源上放了一把锁,其他访问该资的线程只能暂停,程序效率降低,所以加锁之前想清楚
- Mutex互斥体
  - 排他性的使用共享资源,也就是一个资源一次只能被一个线程服务的过程或行为叫做线程间的互斥。互斥是一种特殊的线程同步。
  - 注意事项:
    - 和Monitor类似,只有获取Mutex对象所属权的线程才能进入临界区
    - 虽然用Mutex类要比使用Monitor类消耗更多的系统资源,但是用Mutex类可以跨越应用程序边界,在多个应用程序之间进行同步【系统互斥体】。还有局部互斥体,只能在创建Mutex对象的程序中用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在线程中创建互斥体:
Thread ThreadA = new Thread(delegate()
{
//创建互斥体
Mutex fileMutex = new Mutex(false, "MutexForTimeRecordFile");
});
第一个参数代表创建者是否具有初始所属权,第二个参数代表互斥体的系统名称,
OS根据互斥体的系统名称辨别互斥体,只要有相同系统名称,就是同一个系统互斥体

创建完了后就是在trycatch语句中用了
...
try
{
//尝试请求互斥体的所属权,成则进入临界区,不成功则等待
fileMutex.WaitOne();
...
}
...
finally
{
//释放互斥体的所属权
fileMutex.ReleaseMutex();
}
...

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
44
解决方法:让两个或多个线程以相同顺序访问
临界资源即可
internal class Program
{
//两个临界资源
private static object A = new object();
private static object B = new object();

private static void Main(string[] args)
{
//线程WriterThread
Thread WriterThread = new Thread(delegate()
{
//以相同顺序访问临界资源,先拿A,再拿B
lock (A)
{
lock (B)
{
//....
}
}
});

//线程ReaderThread
Thread ReaderThread = new Thread(delegate()
{
//以相同顺序访问临界资源,先拿A,再拿B
lock (A)
{
lock (B)
{
//....
}
}
});

//启动线程
Writer.Start();
Reader.Start();
}



}

5.线程池

  • 线程池:System.Threading.ThreadPool类,如果程序中包含很多简单不需要特殊控制的线程就可用ThreadPool类
    • 一个应用程序最多一个线程池,
    • 如果向对线程进行更多特殊控制,就别用线程池。这三种情况不宜用ThreadPool类而用单独的Thread类
      • 线程执行很多时间
      • 需要为线程指定详细的优先级
      • 执行过程中需要对线程进行睡眠、挂起等操作
    • .NET中线程池中线程数目默认最少1最多25可以用GetMaxThreads()、GetMinThreadsSetMaxThreads()、SetMinThreads()获取线程数目上限和下限

四、集合

1..NET集合类【CL类库中】

  • System.Array类:优点是可按序找数,缺点是大小固定
  • System.Collections

2.IEnumerable接口,直接或间接实现了IEnumerable接口的类,在遍历时foreach会自动调用IEnumerable接口中的GetEnumerator()方法去获取元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
IEnumerable接口,直接或间接实现了IEnumerable接口的
类,在遍历时foreach会自动调用IEnumerable接口中的
GetEnumerator()方法去获取元素。
public interface IEnumerable
{
IEnumerator GetIEnumerator();//返回的实现了IEnumerator接口的对象就是迭代器
}

public interface IEnumerator
{
bool MoveNext(); // 获取下一个元素
object Currect{ get ;} // 获取当前元素
void Reset(); // 将枚举数设置为其初始位置
}

3.IEnumerable 接口并不是我们看到的只有一个方法,它还有 4 个扩展方法。其中 Cast() 和 OfType() 这 2 个方法非常实用。而 OfType 比 Cast 更加强大,它除了进行强制转换外还可以实现类型的过滤。从结果中可以看到第一个 foreach 遇到 int 类型后由于无法转换而报出 InvalidCastException 异常,而使用 OfType 进行转换时则会自动进行类型筛选,遇到 int 类型的数据将不会转换,所以进行转换时 OfType 是首选


巨人的肩膀