C#进阶

未完成,不定时更新

零、预备节

  1. using关键字,using块保证系统在代码退出该块时释放资源

一、线程

平时写简单程序的时候都是单线程,编译器只给程序分配一个主线程。
但是后续随着难度增加,程序功能的复杂,主线程一拳难敌四手,容易造成程序假死。
于是我们需要创建线程,来执行程序的部分功能。

1.1 前台线程与后台线程

前台线程:

由 Thread 类创建的线程默认是前台线程。所有前台线程关闭后,程序才能关闭。

后台线程:

只要所有前台线程结束,后台线程自动强制结束。

1.2 线程定义

Thread th = new Thread(方法名);//线程定义
th.IsBackground = true;		//设置为后台线程(默认为前台线程)
th.Start();					//线程待命(方法如果是带参数的,在start里面填参数
							//	而且必须是object类型)

详细的属性、方法查看MSDN、此样例属于第二构造函数

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的基础上推出了TaskTask拥有线程池的优点,同时也解决了使用线程池不易控制的弊端。

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# 中的 asyncawait 关键词以及 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、调用方法内

  1. 异步方法内
    • 异步执行await空闲任务;
    • await完成后,执行后续部分;
    • 遇到return、末尾时:
      1. 返回类型void:控制流退出
      2. 返回类型Task;设置Task状态属性后退出
      3. 返回值为另外两个:Task基础上还设置Task的Result属性。
  2. 调用方法内
    • 从异步方法获取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()方法即可阻塞主线程。

  1. Wait:
    类似于thread.Join(),用于单一Task对象。
  2. WaitAll:
    该方法会同步地等待括号内所有的Task全部完成,阻塞主进程;
  3. WaitAny:
    该方法会同步地等待括号内任意一个的Task完成,阻塞主进程;

2.3.5 调用方法中异步地等待(延续操作)

WhenAll & WhenAny
在task执行完毕后开始执行后续操作

  1. WhenAll:
    该方法会异步地等待括号内所有的Task全部完成,不会占用主进程,返回一个Task;
  2. 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来 “伪造” 实例

posted @ 2022-10-23 11:32  LASER_06  阅读(201)  评论(0)    收藏  举报