异步Async
并行,一般指并行计算,是说同一时刻有多条指令同时被执行,这些指令可能执行于同一CPU的多核上,或者多个CPU上,或者多个物理主机甚至多个网络中。
多线程,一般指同一进程中多个线程(包含其数据结构、上下文与代码片段)协作运行。在多核计算机中多个线程将有机会同时运行于多个核上,如果线程中进行的是计算,则行成并行计算。
异步,与同步相对应,是指呼叫另一操作后,不等待其结果,继续执行之后的操作,若之后没有其他操作,当前线程将进入睡眠状态,而CPU时间将有机会切至其他线程。在异步操作完成后通过回调函数的方式获取通知与结果。异步的实现方式有多种,如多线程与完成端口。多线程将异步操作放入另一线程中运行,通过轮询或回调方法得到完成通知;完成端口,由操作系统接管异步操作的调度,通过硬件中断,在完成时触发回调方法,此方式不需要占用额外线程。 阻塞
异步特点:
1异步不会造成阻塞
2异步可以启用额外的线程去执行任务。
3异步并不会缩短任务的时间
Stopwatch时间统计类
Stopwatch watch = new Stopwatch();
watch.Start();
watch.Stop();
watch.ElapsedMilliseconds//执行事件
异步方法调用
Console.WriteLine("****************异步调用开始**********************");
for (int i = 0; i < 5; i++)
{
string name = string.Format("异步调用{0}", i);
DoHandler method = DoSomething;
method.BeginInvoke("异步调用", null, null);
}
Console.WriteLine("****************异步调用结束**********************");
异步方法(含返回值)调用
//子线程中使用返回值
Console.WriteLine("****************异步待返回值进阶调用开始**********************");
Func<string, long> method = DoSomethingReturn;
//主线程中使用返回值
IAsyncResult result = method.BeginInvoke("异步带返回值调用", t =>
{
long iresult = method.EndInvoke(t);
Console.WriteLine("这里是异步返回值,当前线程={0},计算结果是{1}", Thread.CurrentThread.ManagedThreadId, iresult);
}, null);
异步回调函数
Console.WriteLine("****************异步进阶调用开始**********************");
for (int i = 0; i < 5; i++)
{
string name = string.Format("异步调用{0}", i);
DoHandler method = DoSomething;
AsyncCallback callback = t => Console.WriteLine("当前完成的线程是ID={0},状态参数:{1}", Thread.CurrentThread.ManagedThreadId, t.AsyncState);
//关注参数
//参数2:回调函数
//参数3:回调获得
IAsyncResult result = method.BeginInvoke(name, callback, "Admin");
}
Console.WriteLine("****************异步进阶调用结束**********************");
主线程等待异步结果
方式一:
result.AsyncWaitHandle.WaitOne(-1);//线程一直等待
方式二:
while (!result.IsCompleted)//判断是否结束
{
Thread.Sleep(100);
Console.WriteLine("请继续等待...");//类似进度条
}
方式三:
method.EndInvoke(result)
主线程等待就相当于程序的阻塞。
异步的优缺点:
因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些 初入,而且难以调试。
适用范围:
1.在执行I/O操作时
2.网络读写 调用接口
3.数据库的操作
4.WebService
对于需要长时间CPU运算的场合,图形处理,算法执行不适合使用异步。
但是往往由于使用多线程编程的简单和符合习惯,所以很多人往往会使用线程来执行耗时较长的I/O操作,也导致很多人就误认为异步==多线程
总结:需要耗时操作的尽量都使用异步执行。
多线程
操作系统的进程和线程,线程就是围绕进程的一个子单元。
理解多线程
例 1
界面上点击某个按钮后,需要执行一个非常耗时的操作,如果不使用多线程,就只能傻等操作返回。
使用多线程,点击按钮之后,开辟1个新线程后台去执行这个耗时操作,前台界面继续执行其他菜单目录,录入数据等。
例 2
有个操作,1个线程需要20分钟完成,现在的多核cpu,可以真正同一时刻运行多个线程。
假设是双核cpu,同一时刻运行2个线程,操作就只需要10分钟可以完成。
多线程优点:
1提高应用程序执行效率【例1】
2提高CPU利用率【例2】
线程的数量
理想的线程数量<=CPU核心数量
Thread类
Thread thread = new Thread(SayHi);
thread.Start();
线程的挂起
Thread.Sleep(1000);
线程的阻塞
Thread.Join();
示例:
for (var i = 0; i < 5; i++)
{
Thread th1 = new Thread(DoSomething);
th1.Start();
th1.Join();
}
阻塞后,各线程会依次执行。
线程终止,不推荐使用,不安全不可控。
Thread.Abort();
前台线程、后台线程
Thread thread = new Thread(() =>
{
// 默认为 False
Console.WriteLine(Thread.CurrentThread.IsBackground);
});
// 可以设置为后台线程
//thread.IsBackground = true;
thread.Start();
thread.Join();
[
](javascript:void(0)😉
.Net的公用语言运行时(Common Language Runtime,CLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序,或是定时对某些系统资源进行扫描的程序。
重要性
(1)有前台,不关闭
如果线程为前台线程,可能导致UI线程已关闭,但实际还有前台线程暗地里运行,所以程序并没有真正关闭。
(2)无前台,全关闭
当然也要注意,如果所有前台线程都关闭,后台线程会自动关闭,后台线程的代码逻辑可能没执行完就终止了。
线程传递参数
1使用ParameterizedThreadStart委托
注意:此处只能接受object类型参数,因为Thread中只接受ThreadStart委托,而该委托是一个无参无返回值的。所以此处使用ParameterizedThreadStart。
ParameterizedThreadStart pts1 = DoSomething;
Thread thread = new Thread(pts1);
object name = "线程参数";
thread.Start(name);
在线程执行过程中,该参数需要使用需要这是一个只读属性的对象
static readonly object obj = new object();
public static void DoSomething(string name)
{
Console.WriteLine("线程{0}启动", Thread.CurrentThread.ManagedThreadId);
Stopwatch sw = new Stopwatch();
sw.Start();
long result = 0;
for (int i = 0; i < 1000000000; i++)
{
result += i;
}
Thread.Sleep(2000);
sw.Stop();
Console.WriteLine("任务名称={3},当前线程={0},计算结果是{1},总耗时:{2}", Thread.CurrentThread.ManagedThreadId, result, sw.ElapsedMilliseconds, obj.ToString());
}
2使用lambda传递参数(推荐)
string hi = "张三";
Thread thread = new Thread(() =>
{
DoSomething(hi);
});
thread.Start();
线程异常
注意:只要有线程抛出异常,整个程序就会停止。所以,异常必须处理。
对创建线程try catch没用,需要对线程中调用的函数添加异常处理代码
static void Main(string[] args)
{
try // 无用的 try catch
{
Thread thread = new Thread(SayHi);
thread.Start();
}
catch (Exception ex)
{
// 不会打印错误
Console.WriteLine(ex.ToString());
}
}
//正确
static void Main(string[] args)
{
Thread thread = new Thread(SayHi);
thread.Start();
thread.Join();
}
static void SayHi()
{
try
{
Console.WriteLine("hi");
throw new Exception("error");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
线程安全
试试下列代码:
static void Main(string[] args)
{
Thread thread = new Thread(
() => { Check("张三"); });
Thread thread2 = new Thread(
() => { Check("李四"); });
thread.Start();
thread2.Start();
thread.Join();
thread2.Join();
}
static void Check(string name)
{
User.Name = name;
string format = @"{0} = {1}";
for (int i = 0; i < 10; i++)
{
string s = string.Format(format, name, User.Name);
Console.WriteLine(s);
Thread.Sleep(10);
}
}
public class User
{
public static string Name { get; set; }
}
张三等于了李四
原因:User.Name是静态属性,也就是共享资源,多个线程访问共享资源,需要对共享资源做同步处理。
如何解决:
创建一个只读对象对对象处理使用lock(锁操作)
基本含义: lock就是把一段代码定义为临界区,所谓临界区就是同一时刻只能有一个线程来操作临界区的代码,当一个线程位于代码的临界区时,另一个线程不能进入临界区,如果 试图进入临界区,则只能一直等待(即被阻止),直到已经进入临界区的线程访问完毕,并释放锁旗标
static readonly object obj = new object();
// -------------------------
lock (obj)
{
User.Name = name;
string format = @"{0} = {1}";
for (int i = 0; i < 10; i++)
{
string s = string.Format(format, name, User.Name);
Console.WriteLine(s);
Thread.Sleep(10);
}
}
多线程的缺点
1线程占用一定资源,比如内存,CPU;创建和销毁操作都比较昂贵
2线程调度器要管理线程
3大量的创建线程,导致内存不够用,线程调度器繁忙。
4当多个线程竞争同一个静态资源时,会造成死锁。
线程池
由于线程的创建和销毁时比较昂贵的操作,所以在ASP.NET中自己设定了一个线程池,由底层进行统一调配。
线程池思想:事先创建好几个对象,需要就从池中分配,用完就返回池中
注意:
(1)线程池,只适合短时操作,不要阻塞线程池线程
(2)线程池,线程是后台线程
(3)线程池的线程数量有上限
(4)ASP.NET 使用自己的线程池
优缺点
(1)电脑的内存是有限的,每个线程,都会占用内存,如果并发数量很多,内存会爆掉。
(2)使用线程池,当并发超过一定数量,则会排队,所以,并行的请求处理时间会被延长。
用法:
ThreadPool.QueueUserWorkItem((obj) =>
{
DoSomething();
Console.WriteLine(obj);
},"admin");
多线程和异步的区别
多线程是对cpu剩余劳动力的压榨,是一种技术,强调的是并发。
异步强调的是非阻塞,是一种编程模式(pattern),主要解决了UI响应被阻塞的问题,可借助线程技术或者硬件本身的计算能力解决。
背景:作为一个北漂,准备结束北漂生涯,谋划如何搬家。其余家当都变买了,就剩下有一辆小轿车,和一辆摩托车需要带回家。
- 阻塞式编程:先开其中一辆回去,再回来开另一辆车回去。
- 传统异步式编程:摩托车办理快递,我开汽车回去。注意,快递公司派件(回调)时我不一定已经开车到家,如果必须本人签收,就比较麻烦了。----此种通过回调进行异步编程的方式,没法编写符合思维顺序的代码。
- 基于多线程的异步编程:我获得了瞬间移动的超能力(cpu计算速度提升),以毫秒级的速度在汽车与摩托车之间切换驾驶。汽车(主线程)上有车载电话,可以使用处理其它事情。----期间频繁的上下文切换,会造成额外的损耗,造成反应能力比较差,只能开到60迈。
4.并行编程:我获得了分身的超能力(多核cpu的出现),两个我同时开两辆车回家。----充分发挥了cpu的能力,没有额外切换上下文的损耗,精力充沛,在120的时速狂飙。
Task任务
任务是什么
System.Threading.Tasks.Task,任务代表了一个异步操作。
任务非常强大,极大的简化了异步编程,并且能带来显著的性能提升。
举个简单例子,执行一个操作,我们要查询两张表的数据,我们创建两个任务分别各自查询一张表,是不是快些呢?
任务的线程是后台线程
本质上讲,任务就是一个线程池的增强版API
线程池的缺点
1线程池获取结果并不方便
2异步处理不方便
3连续的异步操作不方便
目的:
场景1:3个并发请求,通过3个接口获取数据,我们希望得到所有接口的数据进行下一步业务处理。
场景2:3个并发请求,只要其中某一个完成了,就触发一个事件结果。
任务的创建
1.构造函数创建
static void Main(string[] args)
{
Task task = new Task(() =>
{
// True 线程池线程
Console.WriteLine(Thread.CurrentThread.IsThreadPoolThread);
});
task.Start();
Console.ReadLine();
}
2.Task.Factory.StartNew创建
static void Main(string[] args)
{
Task.Factory.StartNew(() =>
{
// True 线程池线程
Console.WriteLine(Thread.CurrentThread.IsThreadPoolThread);
});
Console.ReadLine();
}
注意:任务工厂模式下,任务会立即调用执行。
3.不使用线程池设置
static void Main(string[] args)
{
Task task = new Task(() =>
{
// False 不使用线程池
Console.WriteLine(Thread.CurrentThread.IsThreadPoolThread);
}, TaskCreationOptions.LongRunning);
task.Start();
Console.ReadLine();
}
获取任务返回值
Task<string> task = new Task<string>(() =>
{
DoSomething();
return "此处为执行完成的结果";
});
task.Start();
Console.WriteLine(task.Result);
任务的回调
task.ContinueWith
(1)支持1个 task 接多个ContinueWith
(2)ContinueWith返回下一代 task,下一代可以继续ContinueWith
Task<string> task = new Task<string>(() =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
return "boy,";
});
task.ContinueWith((t) =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(t.Result + "come on.");
});
task.Start();
Console.ReadLine();
Task的线程等待和延续
Task下的线程等待和延续主要有以下几类:
Wait
针对单个Task的实例,可以让某一个线程进入等待。
WaitAny
执行的所有线程中只要有一个线程结束,主线程就就执行,否则就卡主
Stopwatch sw = new Stopwatch();
sw.Start();
Console.WriteLine("开始了,当前主线程是:{0}", Thread.CurrentThread.ManagedThreadId);
List<Task> taskList = new List<Task>();
Task t1 = new Task(() =>
{
Console.WriteLine("我是第一个任务");
Console.WriteLine("1号开始了,当前1号线程是:{0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000);
Console.WriteLine("1号任务执行完成");
});
taskList.Add(t1);
Task t2 = new Task(() =>
{
Console.WriteLine("我是第二个任务");
Console.WriteLine("2号开始了,当前2号线程是:{0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(6000);
Console.WriteLine("2号任务执行完成");
});
taskList.Add(t2);
Task t3 = new Task(() =>
{
Console.WriteLine("我是第三个任务");
Console.WriteLine("3号开始了,当前3号线程是:{0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(8000);
Console.WriteLine("3号任务执行完成");
});
taskList.Add(t3);
t1.Start();
t2.Start();
t3.Start();
Task.WaitAny(taskList.ToArray());
Console.WriteLine("全部执行完成了,执行时间{0},当前ID是:{1}",sw.ElapsedMilliseconds,Thread.CurrentThread.ManagedThreadId);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wSJTfDct-1606188603611)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20201116161014828.png)]
WatiAll
表示任务列表中所有的任务都已经结束后,主线程恢复。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bNtZGowy-1606188603613)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20201116161212151.png)]
WhenAny
表示任务列表中任何一个任务结束后,配合ContinueWith进行回调触发。
WhenAll
表示任务列表中所有任务都结束后,配合ContinueWith进行回调触发。
注意:WhenAny和WhenAll,都是开启一个新线程执行,不卡顿主线程。
TaskFactory
TaskFactory可以开启线程,当然也对应的线程的等待和延续。
ContinueWhenAny:等价于Task的WhenAny+ContinueWith
ContinueWhenAll:等价于Task的WhenAll+ContinueWith
1.ContinueWhenAny
Task.Factory.ContinueWhenAny(taskList.ToArray(),m=>{
Console.WriteLine("")
})
2.ContinueWhenAll
Task.Factory.ContinueWhenAll(taskList.ToArray(),m=>{
Console.WriteLine("")
})
异步编程Async/Await
async/await是C#5.0引入的一个关键词,其目的是让异步编程边的更加简洁,也是为了顺应异步编程的大趋势。
功能:
1使用async/await能简单的创建异步方法,异步方法可以防止耗时操作阻塞当前线程。
2使用async/await来构建异步方法,更加简洁。
语法
① 方法使用async作为修饰符
② 方法内部包含一个或者多个await表达式,表示可以异步完成的任务
③ 必须具备以下三种返回类型 void 、Task 、Task ,其中后两种的返回对象标识未来完成的工作,调用方法和异步方法可以继续执行。
④异步方法的参数可以任意类型,但是不能为out和ref参数
⑤约定俗成,一般异步方法都是以 Async作为后缀的。
⑥ 除了方法之外,Lambda表达式和匿名函数也可以作为异步对象。
异步方法的控制流
1首先第一个await之前的部分,这部分应该是少量且无需长时间等待的代码。
2await表达式,表示需要被异步执行的任务,await可以有多个。
3在await表达式之后出现的方法的其余代码。
例子1:
public static async Task DoSomethingAsync()
{
Console.WriteLine("异步方法同步部分1");
long sum = 0;
sum = await Task.Run(() =>
{
Console.WriteLine("异步执行部分1,线程ID:{0}",Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
long result = 0;
for (var i = 0; i < 1000000000; i++)
{
result += i;
}
Console.WriteLine("异步执行部分结束");
return result;
});
Console.WriteLine("计算结果{0}", sum);
}
例子2:
public static async void DoSomethingAsync()
{
Task t1 = new Task(()=> {
Console.WriteLine("异步开始了,线程:{0}", Thread.CurrentThread.ManagedThreadId);
long result = 0;
for (int i = 0; i < 1000000000; i++)
{
result += i;
}
Console.WriteLine("异步结束,计算结果{0},线程{1}", result, Thread.CurrentThread.ManagedThreadId);
});
t1.Start();
await t1;
}
async/await和普通异步的对比
思考:实现一个程序,每次任务休眠5秒后,返回100,打印100
public static void DoSomething()
{
Console.WriteLine("异步方法开始1");
var aaa= await Task.Run(() =>
{
Thread.Sleep(5000);
return 100;
});
Console.WriteLine("计算结果{0}", aaa.result);
}
调用:
static void Main(string[] args)
{
DoSomething();
DoSomething();
DoSomething();
}
解释:上述代码运行后,是同步执行的,虽然Task会异步产生一个线程运行,但是由于Task中的返回值需要使用aaa.result获取,阻塞了线程,所以运行结果是同步的。
将上述代码改装为异步:
public static void DoSomething111()
{
Console.WriteLine("异步方法开始1");
//通过第一个异步方法得到结果
var aaa= Task.Run(() =>
{
Thread.Sleep(5000);
return 100;
});
aaa.ContinueWith(x =>
{
Console.WriteLine(x.Result);
});
}
解释:由于Task.Run()返回的是一个Task对象,所以如果不希望阻塞线程,那么可以调用ContinueWith进行回调处理,在回调中获取Task的返回值,ContinueWith不会阻塞主线程。
优雅的写法async/await
public async static void DoSomething111()
{
Console.WriteLine("异步方法开始1");
//通过第一个异步方法得到结果
var aaa= await Task.Run(() =>
{
Thread.Sleep(5000);
return 100;
});
Console.WriteLine("结果是{0}", aaa);
}
解释:Task.Run()执行的是一个耗时任务,await标记表示等待任务结果,将结果赋值给aaa,没错,就是这么优雅。
Socket
概念
socket的英文原义是“孔”或“插座”。作为进程通信机制,取后一种意思。通常也称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄(其实就是两个程序通信用的)。
socket非常类似于电话插座。以一个电话网为例:电话的通话双方相当于相互通信的2个程序,电话号码就是ip地址。任何用户在通话之前,首先要占有一部电话机,相当于申请一个socket;同时要知道对方的号码,相当于对方有一个固定的socket。然后向对方拨号呼叫,相当于发出连接请求。对方假如在场并空闲,拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是一方向电话机发出信号和对方从电话机接收信号的过程,相当于向socket发送数据和从socket接收数据。通话结束后,一方挂起电话机相当于关闭socket,撤销连接。
Socket使用步骤
服务器端:
第一步:创建一个用于监听连接的Socket对像;
第二步:用指定的端口号和服务器的ip建立一个EndPoint对像;
第三步:用socket对像的Bind()方法绑定EndPoint;
第四步:用socket对像的Listen()方法开始监听;
第五步:接收到客户端的连接,用socket对像的Accept()方法创建一个新的用于和客户端进行通信的socket对像;
第六步:通信结束后一定记得关闭socket;
客户端:
第一步:建立一个Socket对像;
第二步:用指定的端口号和服务器的ip建立一个EndPoint对像;
第三步:用socket对像的Connect()方法以上面建立的EndPoint对像做为参数,向服务器发出连接请求;
第四步:如果连接成功,就用socket对像的Send()方法向服务器发送信息;
第五步:用socket对像的Receive()方法接受服务器发来的信息 ;
第六步:通信结束后一定记得关闭socket;