委托、事件、多线程

一 委托

定义:

A delegate is a type that represents references to methods with a particular parameter list and return type.

When you instantiate a delegate,you can associate with any method with a compatible signature and return type.

You can invoke (or call) the method though the delegate instance.

一个委托是一种类,可以指向一个或者多个方法,(这个委托有参数列表和返回值类型)。当你实例化一个委托后,即创建委托的实例,这个委托类型的实例,可以和任何其他方法相关联起来,即可以存储这些方法的引用,【只要】类型兼容(任何方法他们的签名和返回值与委托类型的签名和返回值保持一致,即类型兼容)。你可以直接调用这些方法,通过委托的实例。

【委托就是将方法给委托,用委托来调用方法,通过+=,-=给委托添加或者减少方法,匿名函数只使用一次。】

委托的由来:

委托是一个类,继承自system.multicastdelegate,这是一个特殊类,不能声明类型来继承,只能声明委托来自动继承,编译器限制,所以委托虽然是一种类,但是不能用声明类的方式去声明。委托封装一个方法叫单播委托,多个方法叫多播委托。【多播委托一般用来调用无返回值的方法,不用来调用有返回值的方法,因为有返回值的多播委托中间的结果都会被丢弃掉,只剩最后一个。】

反编译工具IL:IL中间语言(C#--IL---计算机二进制)C#是等价于IL的,IL是一个很好的工具,没有编辑器没有编译器的方便。

 

系统自带的两种委托:

通过委托调用方法,是间接调用,有两种存在的两种委托:Action泛型委托可以指向一个没有返回值,但是可以有参数(16个)也可以没有参数的方法。Func委托是指向一个有返回值,参数(16个)可以有也可以没有的方法。

 

调用时,相当于执行了方法,为啥用委托?

委托可以把方法包裹成变量,Invoke时会调用这个方法

三 多线程:

啥是进程:

进程(Process)是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。一个正在运行的应用程序在操作系统中被视为一个进程,进程可以包括一个或多个线程。线程是操作系统分配处理器时间的基本单元,在进程中可以有多个线程同时执行代码。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。是应用程序的一个运行例程,是应用程序的一次动态执行过程。

【程序在服务器上运行时,占据的计算资源合集,称之为进程。】

 啥是线程:

线程(Thread)是进程中的基本执行单元,是操作系统分配CPU时间的基本单位,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。

【程序执行的最小单位,最小的执行流。(向操作系统请求所得)】

 

 句柄:其实是个long数字,是操作系统标识应用程序

多线程:(一个进程有多个线程同时执行)

多线程的优点:可以同时完成多个任务;可以使程序的响应速度更快;可以让占用大量处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务;可以随时停止任务;可以设置每个任务的优先级以优化程序性能。

为什么可以多线程执行呢?

1、CPU运行速度太快,硬件处理速度跟不上,所以操作系统进行分时间片管理。这样,从宏观角度来说是多线程并发的,因为CPU速度太快,察觉不到,看起来是同一时刻执行了不同的操作。但是从微观角度来讲,同一时刻只能有一个线程在处理。【并发:cpu分片的并发】

2、目前电脑都是多核多CPU的,一个CPU在同一时刻只能运行一个线程,但是多个CPU在同一时刻就可以运行多个线程。【多核之间叫并行】

4核8线程----核是指物理的核  线程是指虚拟核,也就是8个虚拟核,可以同时执行8个线程,通过分片,那么可以执行N多个线程。

然而,多线程虽然有很多优点,但是也必须认识到多线程可能存在影响系统性能的不利方面,才能正确使用线程。不利方面主要有如下几点:

1)线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多。

2)多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程。

3)线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。

4)线程太多会导致控制太复杂,最终可能造成很多程序缺陷。

创建多线程的步骤:
1、编写线程所要执行的方法
2、实例化Thread类,并传入一个指向线程所要执行方法的委托。(这时线程已经产生,但还没有运行)
3、调用Thread实例的Start方法,标记该线程可以被CPU执行了,但具体执行时间由CPU决定。

CPU的几核几线程具体是啥意思,后面的Ghz是不是数字越高越好?

举个通俗的例子吧。CPU就是个银行,大家排队存钱取钱办手续。有几个窗口就是几个线程,也就是可以“同时”接待的顾客数量。核心数相当于职员数量,也就是真正干活的人。4核心4线程,四个职员四个窗口,一个职员负责一个窗口,这没啥好说的。4核心8线程,四个职员八个窗口,一个职员负责两个窗口,不过职员没有分身术,职员A在1号窗口干活的时候他就没法同时处理2号窗口。这叫超线程(Hyperthread)。那4核心8线程跟4核心4线程比有什么区别呢?或者说超线程有什么好处呢?这要分情况说。如果来了4个储户,一直在4个窗口办手续,那4C4T(4核心4线程,下面都这么简写)和4C8T倒是没什么区别。但实际情况是银行大厅里常年坐着100多号人,都要办手续。而手续往往特别的复杂,需要储户填写10000000张单据的那种。如果一直给前四个储户办手续,后面的就等到黄瓜菜都凉了,银行一定会被投诉到死。所以就这么办:X储户填单据的时候,把Y储户叫过来办手续,X填完了这张再让X过来继续办。不过叫Y过来的时候就得暂时把跟X储户相关的单子和资料先收起来,等X下次过来再把Y相关的东西收好,把X的东西再拿出来。这个时候更多的窗口也就是更多的线程的优势就显示出来了。X在1号窗口填单据的时候职员A可以转到2号窗口给Y办手续,然后再转到1好窗口继续给X办手续。这样就省下把东西收拾来收拾去的时间,这可以大幅度提高效率。因为这样的切换其实非常的频繁,所以综合来说还是差不少事的。而且是任务越多超线程相对来说效果越好。再细致一点说呢,职员虽然同时只能处理一个单子,但他有两只眼睛,两只手和一张嘴还有一个脑子。处理一个客户的单子要先用眼睛看一下,然后脑子想一下怎么处理,然后动手动嘴。几乎所有单子都是这个流程,这三项都做完算是处理好一个单子。但在职员看完第一个单子开始动脑子想的时候,其实他的眼睛是闲着的。开始做第一个单子的时候眼睛和脑子都闲着。不能让丫闲着啊!于是我们要求,职员在看完第一个单子开始动脑子想的时候要开始用眼睛看这个客户的下一个单子,而不是等第一个单子完全处理好再看第二个单子。这样第一个单子想好了,开始动手动嘴了,就可以立刻开始想第二个单子,而同时眼睛就可以看第三个单子了。也就是说,这个职员可以手上做着第一个单子,脑子里想着第二个单子,眼睛同时看第三个单子。某种程度上来说,虽然职员没有分身术,但他在同一时间处理着三个单子,效率高多了吧?这个叫流水线。但有的时候对于一个储户来说,第一个单子不办完下一个就没法继续。比如一个储户要先查余额再汇款,余额不知道汇款的单子根本无从处理。加上单子和单子不一样,有的单子需要想很久,有的单子根本不用想。所以一个职员负责一个窗口有的时候,手脑眼有的时候还是有闲下来的时候。这个时候超线程则可以更好的榨干这个职员的剩余价值。比如虽然他没有分身术,但比如他在第一个窗口给X办手续,因为种种原因眼睛闲下来了,那他可以先看看第二个窗口里Y的单子嘛。

GHz代表每个职员每秒可以做多少个动作。一般来说,每秒职员可以做的动作也多,他干活速度就越快。不过这个事不绝对,有的职员是老手,10个动作就可以办完一件事。有的职员效率低,同样一件事要30个动作才能完成。

Thread类是是控制线程的基础类,位于System.Threading命名空间下,具有4个重载的构造函数:

1

 

初始化 Thread 类的新实例,指定允许对象在线程启动时传递给线程的委托。要执行的方法是有参的。

2

 

初始化 Thread 类的新实例,指定允许对象在线程启动时传递给线程的委托,并指定线程的最大堆栈大小

3

 

初始化 Thread 类的新实例。要执行的方法是无参的。

4

 

初始化 Thread 类的新实例,指定线程的最大堆栈大小。

线程的属性,优先级,方法

 

 

 

前台线程和后台线程

前台线程:只有所有的前台线程都结束,应用程序才能结束。默认情况下创建的线程              都是前台线程
后台线程:只要所有的前台线程结束,后台线程自动结束。通过Thread.IsBackground设置后台线程。必须在调用Start方法之前设置线程的类型,否则一旦线程运行,将无法改变其类型。

线程同步

所谓同步:是指在某一时刻只有一个线程可以访问变量。
如果不能确保对变量的访问是同步的,就会产生错误。
c#为同步访问变量提供了一个非常简单的方式,即使用c#语言的关键字Lock,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在c#中,关键字Lock定义如下:
Lock(expression)
{
   statement_block
}

expression代表你希望跟踪的对象:
如果你想保护一个类的实例,一般地,你可以使用this;
如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了
statement_block就算互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。

以售卖图书为例:

 

 

加了lock锁之后结果正确

 

跨线程访问:

点击“测试”,创建一个线程,从0循环到10000给文本框赋值

 

 

结果1

 

产生错误的原因:textBox1是由主线程创建的,thread线程是另外创建的一个线程,在.NET上执行的是托管代码,C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件。

解决方案1

在窗体的加载事件中,将C#内置控件(Control)类的CheckForIllegalCrossThreadCalls属性设置为false,屏蔽掉C#编译器对跨线程调用的检查。

 

使用上述的方法虽然可以保证程序正常运行并实现应用的功能,但是在实际的软件开发中,做如此设置是不安全的(不符合.NET的安全规范),在产品软件的开发中,此类情况是不允许的。如果要在遵守.NET安全标准的前提下,实现从一个线程成功地访问另一个线程创建的空间,要使用C#的方法回调机制。

解决方案2:使用回调函数

啥是回调函数:回调函数是指使用者自己定义一个函数,实现这个函数的程序内容,然后把这个函数(入口地址)作为参数传入别人(或系统)的函数中,由别人(或系统)的函数在运行时来调用的函数。函数是你实现的,但由别人(或系统)的函数在运行时通过参数传递的方式调用,这就是所谓的回调函数。简单来说,就是由别人的函数运行期间来回调你实现的函数。

回调函数意义:因为可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需要知道存在一个具有特定原型和限制条件的被调用函数。简而言之,回调函数就是允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。

 

实际开发中,此类会封装,只提供函数接口。

 

结果:

 

解决方案2.1

 

也就是说,回调其实就是个委托

 

 

其中需要注意Test方法中的invoke

 

疑问?为啥使用回调函数之后就可以跨线程访问

如果有两个线程,本来两个线程是互相不访问的,但是委托就有那么一个作用,当一个线程想去访问另一个线程的东西的时候,就可以去借助于委托去访问。

 private void button1_Click(object sender, EventArgs e)
        {
            Console.WriteLine($"同步 Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
            int j = 3;
            int k = 5;
            int m = j + k;
            for (int i = 0; i < 5; i++)
            {
                string name = string.Format($"同步{i}");
                this.DoSomethingLong(name);
            }
            Console.WriteLine($"同步 End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
        }

  

 

posted @ 2021-12-10 14:51  C#工控菜鸟  阅读(524)  评论(0编辑  收藏  举报