C#进阶
未完成,不定时更新
零、预备节
using关键字,using块保证系统在代码退出该块时释放资源。
一、线程
平时写简单程序的时候都是单线程,编译器只给程序分配一个主线程。
但是后续随着难度增加,程序功能的复杂,主线程一拳难敌四手,容易造成程序假死。
于是我们需要创建线程,来执行程序的部分功能。
1.1 前台线程与后台线程
前台线程:
由 Thread 类创建的线程默认是前台线程。所有前台线程关闭后,程序才能关闭。
后台线程:
只要所有前台线程结束,后台线程自动强制结束。
1.2 线程定义
Thread th = new Thread(方法名);//线程定义
th.IsBackground = true; //设置为后台线程(默认为前台线程)
th.Start(); //线程待命(方法如果是带参数的,在start里面填参数
// 而且必须是object类型)
1.3 处理线程间异常
//程序加载时加上:
Control.CheckForIllegalCrossThreadCalls = false;
1.4 线程同步
是指某一时刻只有一个线程可以访问资源。
C#中提供了lock方式,语法:
lock (locker)
{
dosomething...
}
举一个售票系统的简单例子:
e.g.
class Program
{
static void Main(string[] args)
{
TicketSeller ticketSeller = new TicketSeller();
//创建两个线程同时访问Sale方法
Thread t1 = new Thread(ticketSeller.Sale);
Thread t2 = new Thread(ticketSeller.Sale);
//启动线程
t1.Start();
t2.Start();
Console.ReadKey();
}
}
class TicketSeller
{
//剩余票数
public int num = 1;
private static readonly object locker = new object(); //资源锁,尽可能用private
public void Sale()
{
lock (locker)
{
int tmp = num;
if (tmp > 0)//判断是否有书,如果有就可以卖
{
Thread.Sleep(1000);
num -= 1;
Console.WriteLine("售出一张票,还剩余{0}张", num);
}
else
{
Console.WriteLine("没有了");
}
}
}
}
如果没有进行锁定,则会发生剩余票数为负的情况。
1.5 线程池(ThreadPool)简介
Task是在ThreadPool的基础上推出的。
ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少线程的创建,节省开销。
e.g.
for (int i = 1; i <= 10; i++)
{
//ThreadPool执行任务
ThreadPool.QueueUserWorkItem(
new WaitCallback((obj) =>
{
Console.WriteLine($"第{obj}个执行任务");
}), i);
}
Console.ReadKey();
但是线程池也有不足,它不能控制thread的执行顺序;也不能获取内部thread取消、异常、完成的通知,不利于监管。
net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的优点,同时也解决了使用线程池不易控制的弊端。
1.6 Task 的创建与运行
Task的创建和执行方式有如下三种:
//1.new方式实例化一个Task,需要通过Start方法启动
Task task = new Task(() =>
{
Thread.Sleep(100);
Console.WriteLine("1");
});
task.Start();
//2.Task.Factory.StartNew(Action action)创建和启动一个Task
Task task2 = Task.Factory.StartNew(() =>
{
Thread.Sleep(100);
Console.WriteLine("2");
});
//3.Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
Task task3 = Task.Run(() =>
{
Thread.Sleep(100);
Console.WriteLine($"3");
});
Console.WriteLine("执行主线程!");
余下的转到3.3
二、多线程与异步编程
2.1 异步引入背景
启动程序时,系统会在内存中创建一个新的进程 (Process),进程是构建运行程序的资源集合。在进程的内部系统创建了一个称为线程 (Thread) 的内核对象,它代表了真正执行的线程,系统会在Main方法的第一行开始线程执行。
- 默认情况下一个进程仅包含一个线程(主线程);
- 线程可以派生其他线程;
- 如果一个进程拥有多个线程,它们将共享进程资源;
- CPU执行调度的单元是线程;
如果程序都同步单线程地运行,那么在一些C/S多对多通讯上、GUI交互上、大文件传输时就会出现“卡死”,“等待”等一些弊端。因此引入了异步,使得程序代码不必按照顺序严格执行。
1、同步(sync):
发出一个功能调用时,在没有得到结果之前,该调用就不返回。
2、异步(async):
与同步相对,调用在发出之后,这个调用就直接返回了,所以没有返回结果。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。
如果一个操作的部分是异步的,那么这个操作整体就是异步的。==》异步封装。
2.2 异步编程模式
.NET 提供了执行异步操作的三种模式:
基于任务的异步编程模式 (TAP) ,该模式使用单一方法表示异步操作的开始和完成。 TAP 是在 .NET Framework 4 中引入的。 这是在 .NET 中进行异步编程的推荐方法。 C# 中的 async 和 await 关键词以及 Visual Basic 中的 Async 和 Await 运算符为 TAP 添加了语言支持。
基于事件的异步模式 (EAP),是提供异步行为的基于事件的旧模型。 这种模式需要后缀为 Async 的方法,以及一个或多个事件、事件处理程序委托类型和 EventArg 派生类型。 EAP 是在 .NET Framework 2.0 中引入的。 建议新开发中不再使用这种模式
异步编程模型 (APM) 模式(也称为 IAsyncResult 模式),这是使用 IAsyncResult 接口提供异步行为的旧模型。 在这种模式下,同步操作需要 Begin 和 End 方法(例如,BeginWrite 和 EndWrite以实现异步写入操作)。 不建议新的开发使用此模式。
2.3 async/await 特性异步
C#5.0 引入了这个特性,方便实现异步。
async是上下文关键字,代表下文提到的await将作为异步方法,而非函数名称。
await 关键字不会创建新线程,但执行到关键字的时候由后台线程接管函数运行,原线程保存现场立刻返回。运用到了.Net程序创建后的10个线程池。
2.3.1 结构
该特性由3部分组成:
- async 异步方法
- await 表达式
- 调用方法
e.g.
class Program
{
static void Main()
{
...
Task<int> value = AsyncClass.XXXAsync(X,X); //调用异步方法
...
}
}
static class AsyncClass
{
public static async Task<int> XXXAsync(X,X) //async 异步方法
{
//Task<int> getIntTask = 异步方法; //此时异步方法就已经开始执行了,为了避免阻塞,控制权上交给XXXAsync。
//DoOtherWork(); //代表做其他工作的代码块,因为getIntTask没被await修饰,这部分代码可以不受阻塞地继续进行。
//int sum = await getIntTask;
int sum = await 异步方法; //await 表达式
return sum;
}
}
当Main(或其他)调用异步方法时,方法将立即返回一个Task
类型的占位符对象 ,这个Task对象代表着用于调用异步方法的正在运行的那个进程,然后才开始执行异步方法。此时由于已经返回,主线程不会阻塞,将会继续执行。当异步方法执行完毕后,会返回一个int给占位符。
XXXAsync方法 在await后的方法完成之前被阻塞;XXXAsync方法先返回给调用者,控制权移交;- 任务完成后控制权回到
XXXAsync方法,await操作符从被其修饰的方法中检索int结果。
2.3.2 返回值类型
共有4种:
Task:不需要返回某个值,但是需要检查异步方法的状态,返回一个Task类型对象即可;Task<T>:使用await调用任务将获得这个T类型的值;void:仅仅想用异步,在写异步事件处理函数。ValueTask<T>:这是一个值类型对象
虽然异步方法的返回值如上,但是方法体中不包含任何返回如上类型的return语句。
这与异步控制流有关:
到达await时出现2个流:1、异步方法内;2、调用方法内
- 异步方法内:
- 异步执行await空闲任务;
- await完成后,执行后续部分;
- 遇到return、末尾时:
- 返回类型void:控制流退出
- 返回类型Task;设置Task状态属性后退出
- 返回值为另外两个:Task基础上还设置Task的Result属性。
- 调用方法内:
- 从异步方法获取Task对象,需要值的时候就访问Result属性。
2.3.3 取消一个异步
Thread怎么取消任务呢?一般流程是:设置一个变量来控制任务是否停止,如设置一个变量isStop,然后线程轮询查看isStop,如果isStop为true就停止。
Task中有一个专门的类 CancellationTokenSource 来取消任务执行
e.g.
CancellationTokenSource source = new CancellationTokenSource();
int index = 0;
//开启一个task执行任务
Task task1 = new Task(() =>
{
while (!source.IsCancellationRequested)
{
Thread.Sleep(1000);
Console.WriteLine($"第{++index}次执行,线程运行中...");
}
});
task1.Start();
//五秒后取消任务执行
Thread.Sleep(5000);
//source.Cancel()方法请求取消任务,IsCancellationRequested会变成true
source.Cancel();
2.3.4 调用方法中同步地等待(阻塞方法)
Wait & WaitAll & WaitAny
没有返回值,同步方法。
使用Thread时,我们知道用thread.Join()方法即可阻塞主线程。
- Wait:
类似于thread.Join(),用于单一Task对象。 - WaitAll:
该方法会同步地等待括号内所有的Task全部完成,阻塞主进程; - WaitAny:
该方法会同步地等待括号内任意一个的Task完成,阻塞主进程;
2.3.5 调用方法中异步地等待(延续操作)
WhenAll & WhenAny
在task执行完毕后开始执行后续操作
- WhenAll:
该方法会异步地等待括号内所有的Task全部完成,不会占用主进程,返回一个Task; - WhenAny:
该方法会异步地等待括号内任意一个的Task完成,不会占用主进程,返回一个Task< T >;
2.3.6 Task.Delay
实质是创建了一个Task对象,该对象将暂停其在线程中的处理。异步中的等待请使用delay。
- 单独
Task.Delay():线程创建了个新的任务去执行延时,线程将继续进行。 await Task.Delay():线程将等待这个新任务延时完再继续执行代码。"推荐"Thread.Sleep()只是把CPU时间片分出去了,实际上还是占有资源;而await Task.Delay()是用的完成端口,延时过程不会占用资源,开始延时和延时完成后的线程可能是两个不同的线程。
由于机器的性能大不相同,有时数据访问量也不尽相同,仅由一个delay无法做到程序的普适性。
s
2.3.7 异步Lambda
2.3 Task.Run 多线程
2.3.2 Task.Run 内存泄漏陷阱
三、Socket编程
3.1 概念入门
3.1.1 Socket
类似两个人打电话
程序通过 Socket 通信
人通过 电话 通信
电脑与电脑联系需要规定 协议
人与人联系需要规定 语言
- Server:负责监听的 socket(
Socket()返回),负责跟客户端进行通信的socket(Accept()返回)。 - Client:负责跟服务器进行通信的socket。
3.1.2 同步、异步、阻塞、非阻塞
| 术语 | 解释 |
|---|---|
| 同步 | Client发送请求后,只有得到Server回应才可以发下一个 |
| 异步 | Client发送请求后,无需等待回应即可发送下一个请求 |
| 阻塞 | 执行socket的调用函数只有得到结果后才返回,否则当前thread将挂起,此socket一直阻塞在线程调用上 |
| 非阻塞 | 执行socket调用函数时,无论是否受到结果,立即返回,不会阻塞thread |
3.2 C/S通信流程

3.3 Socket的两种类型
3.3.1 流式Socket (STREAM):
一种面向连接的Socket,针对TCP服务应用,安全,效率低。
3.3.2 数据报式Socket (DATAGRAM):
一种无连接的Socket,针对无连接的UDP服务应用,顺序混乱,在接收端要分析重排及要求重发,但是效率高。
3.4 Socket例子
网上有很多例子,可自主查找或访问MSDN。
TCP案例1
TCP案例2
UDP案例
四、单元测试
4.1 基本测试分类
- MSTest:微软自己的
- NUnit:
- xUnit:(推荐)
4.2 MAUI无法被xUnit测试程序引用
把不能测试的部分从项目中剥离出来
一些不能剥离出来的部分,需要使用到“依赖注入”
一些使用依赖注入但无法实例化的部分,需要在测试等地方使用Mock来 “伪造” 实例

浙公网安备 33010602011771号