首页 » Web前端 » php多线程异步技巧_多线程和异步有什么接洽关系和差异若何实现异步

php多线程异步技巧_多线程和异步有什么接洽关系和差异若何实现异步

访客 2024-12-19 0

扫一扫用手机浏览

文章目录 [+]

很多很多年前,有个叫 DOS 的操作系统。

DOS 通过一行一行命令运行程序。
在同一时候里,你只可能运行一个程序,这便是 单进程系统。

php多线程异步技巧_多线程和异步有什么接洽关系和差异若何实现异步

后来涌现了 Windows,用户可以在系统中打开多个程序并利用它们。
这便是 多进程系统。

php多线程异步技巧_多线程和异步有什么接洽关系和差异若何实现异步
(图片来自网络侵删)

线程 与 进程 的关系,就犹如 进程 与 系统 的关系。
一个 系统 可以存在多个 进程 ,一个 进程 也可以存在多个 线程 。

本日的主题与 多线程 的事理关系不大,因此就不在其 事理 上进行更多的解释和解释了。

什么是单线程,什么是多线程

还得记大约五、六年前,我去 KFC 和 McDonald's 就创造了一个有趣的差异。

在 KFC 中,收银 与 配餐 是同一人。

顾客在点餐后,连续站在原地等待西餐,造成了 KFC 中常常能见到长长的军队在排队。

在 McDonald's ,这两件事是由两个不同的人卖力的。

顾客在点餐完成后,带着号离开点餐的军队,直接去等待配餐叫号。
点餐的军队人数比 KFC 明显少了许多。

比拟这两种模式,你会创造

KFC 的模式很随意马虎积压一长排的顾客,让他们烦于等待而终极离开。
McDonald's 的模式不随意马虎产生排长队的顾客,并且可以让顾客早早的进入叫号等餐环节。

我们把 线程 视作 员工 ,把 顾客 视作 任务,于是这两个例子就可以非常形象的阐明什么 单线程 ,什么是 多线程 。

KFC 这种模式模式便是 单线程 , 一个任务从头至尾由一 线程 完成,在一个任务完成之前,不接管第二个任务。
McDonald's 这种模式便是 多线程 , 将一个任务拆分身分歧的阶段(部分),并交给不同的 线程 分别处理。
什么是同步,什么是异步

当你明白了 KFC 和 McDonald's 后,统统就很大略了。

线程 是 员工,同步 / 异步 便是顾客点餐的流程了。

顾客不能去一下洗手间,只能呆呆地站在那里等待配置的模式便是 同步 。
顾客支付往后,利用等待配置的韶光去一下洗手间,找个座位的模式便是 异步 。

显而易见,异步 可以供应更高的效率,它可以利用 等待 的韶光去完成一些事情。

在我们的代码中,一个 顾客 一边等待配置、一边做些别的事情,便是 多线程 了。

因此,(单/多线程) 与 (同/异)步 是密不可分的两个观点。

实现异步

在正常情形下,我们写出来的代码都是 同步 的,上一行代码没做完就肯定不会实行第二行。

以是 如何实现同步 这个问题的答案与 如何写一段代码 是一样的。

那么,我们自然而然的就把目光放在了 如何实现异步,这一话题上了。

在 .Net 中,我们有几种 异步 的实现方案 :

ThreadBeginInvokeTaskasync / await

下面,我会先容每种方案是如何实现的

Thread

首先,如上面所提到的,异步 的目标便是,先开始 某个任务,然后利用等待的韶光去做点 别的事情。

很明显,这里有两个线程

一个卖力 某个任务。
另一个卖力 别的事情,并在完成 别的事情 后开始等待 某个任务 的完成。

利用这个思想,我们可以自己做一个异步的小例子了。

// 某个任务的结果int resultOfSomeTask = 0;Thread someTask = new Thread(new ThreadStart(() =>{ Thread.Sleep(1000); resultOfSomeTask = 100;}));someTask.Start();DoSomething(1); // 做一些别的事情DoSomething(2); // 做一些别的事情DoSomething(3); // 做一些别的事情// 每隔一下子去看看 【某个任务】 是否完成了while (true){ if (someTask.ThreadState == ThreadState.Stopped) break; Thread.Sleep(1);}Assert.AreEqual(100, resultOfSomeTask);

代码解释

我们利用 Thread 创建一个线程,让它去完成 某个任务,我们仿照该任务须要 1秒钟,并会产生一个 100 的结果利用 Thread.Start 开始这个任务在 someTask 实行过程中,我们造作一些 别的事情利用一个轮询查看 someTask 的状态,当完成后,我们创造已经得到了 100 这个结果。

上面例子中 while(true) 部分,我们可以利用 Thread.Join() 方法来代替以达到同样的效果。

// 某个任务的结果int resultOfSomeTask = 0;Thread someTask = new Thread(new ThreadStart(() =>{ Thread.Sleep(1000); resultOfSomeTask = 100;}));someTask.Start();DoSomeThine(2); // 做一些别的事情DoSomeThine(3); // 做一些别的事情DoSomeThine(1); // 做一些别的事情// 产生与 while(true) 同样的效果// 当 someTask 完成后,才会连续进行someTask.Join();Assert.AreEqual(100, resultOfSomeTask);

这种异步的实现办法让开发者无法只关注在 逻辑 本身,代码中混入了大量的与线程有关的代码。

而且最不人性化的是,Thread 要么没有参数,要么只给一个 object 类型的参数,最草稿的是,它 无法返回结果,我们必须写一些额外的代码、逻辑去要主线程中得到子线程中的结果。

题外话

在实际生产环境中,我们每每利用 ThreadPool 来开启线程。

毕竟每开一个线程,系统都会产生一相应的花费来支持它。

ThreadPool 可以开启有限个的线程,并对任务排队,仅当有线程空闲时,才会连续处理任务。

BeginInvoke

BeginInvoke 是 Delegate 上的一个成员,它与 EndInvoke 一起利用可以实现异步操作。

BeginInvoke 相称于上面例子中 Thread.Start() 的功能EndInvoke 相称于上面例子中 Thread.Join() 的功能

由于 BeginInvoke 是 Delegate 上的成员,以是我们先声明一个 Delegate

/// <summary>/// 这是一个描述了一个利用整形并返回整形的委托类型。
/// 你可以利用直策应用 Func<int,int> 来作为委托的类型。
/// </summary>/// <param name="i"></param>/// <returns></returns>public delegate int TaskGetIntByInt(int i);

BeginInvoke 的入参比较特殊,它分为两个分部。

前面的几个参数,便是委托中定义的参数后面两个参数,一个是异步任务完成时的回调,一个是可以向回调函数传入的额外参数,你可以通报任何你须要的内容至回调里,而避免了在进程内访问进程外成员的情形

下面是一个 BeginInvoke 的例子

// 这是一个耗时1秒的任务,会返回入参的平方数TaskGetIntByInt someTask = new TaskGetIntByInt(i =>{ Thread.Sleep(1000); return i i;});// 定义一个函数,用于 someTask 完成时的回调AsyncCallback callback = new AsyncCallback(ar =>{ string state = ar.AsyncState as string; Assert.AreEqual("Hello", state);});// 开始平方数运算的任务// callback, "HelloWorld" 根据需求传入,你也可以传 nullIAsyncResult ar = someTask.BeginInvoke(10, callback, "HelloWorld");// 开始一些别的任务DoSomeThing(1);DoSomeThing(2);DoSomeThing(3);// 等待 someTask 的运算结果,形如 Thread.Join()int result = someTask.EndInvoke(ar);Assert.AreEqual(100, result);

代码解释

首先 创建委托的实例,你可以利用其它类型上的成员来布局,也可以像示例中那样直接写一个内部方法。

接下来 利用 BeginInvoke 开始异步调用。
把稳 这里返回了一个 IAsyncResult 类型。

你可以把这个 IAsyncResult 理解为你在 McDonald's 点好餐后的 号 , 每个人的 号 都是不同的,每个顾客都可以用这个 号 领取你的美食。

在代码中,每次调用 BeginInvoke 都会产生不同的 IAsyncResult,你可以用不同的 IAsyncResult 去获取它们对应的结果。

BeginInvoke 的时候,你还可以指定一个 回调函数 ,还可以指定一个变量,供 回调函数 利用。

此时,someTask 已经在子线程中运行了。
同时,主线程连续实行了 3 个 DoSomething() 方法。

当你须要 someTask 的运行结果时,你只须要调用 someTask.EndInvoke(IAsyncResult) 。

当子线程已经完成后,调用 EndInvoke 你可以立即得到结果。
当子线程尚未完成时,调用 EndInvoke 会一贯等待,等到子线程实行完成后,才可以得到结果。

题外话

若你的异步任务是一个耗时极长的任务,在主线程利用 EndInvoke 会 傻等 良久。

此时,你可以将 EndInvoke 方法在 Callback 内实行。

将 someTask 作为 回调函数 的参数传入,就可以在 Callback 内利用 EndInvoke 得到结果。

TaskGetIntByInt someTask = new TaskGetIntByInt(i =>{ Thread.Sleep(3000); return i i;});DoSomeThing(1);DoSomeThing(2);DoSomeThing(3);AsyncCallback callback = new AsyncCallback(_ar =>{ // BeginInvoke 的末了一位参数可以通过 AsyncState 取得 TaskGetIntByInt task = (TaskGetIntByInt)_ar.AsyncState; int result = task.EndInvoke(_ar); Assert.AreEqual(100, result);});IAsyncResult ar = someTask.BeginInvoke(10, callback, someTask);

对付一个 异步 任务的结果,我们每每有两种方法处理 :

在主线程中等待结果在子线程中处理结果

对付一个耗时较短的任务,我们可以先利用 异步 将该任务放在子线程中实行。
再连续在主线程中处理其它任务,末了等待 异步 任务的完成。
这种办法便是 在主线程中等待结果 。

对付一个耗时较长的任务,如果在主线程中 等待 会有可能对终端用户带来不好的运用体验。

因此,我们不会在主线程中等待 异步 的完成。

我们在 异步 任务开启后,就可以早早关照用户 你的任务正在处理中。
同时在子线程中,当任务完成后,你可以利用数据库等手段,将 正在处理中的任务 标为 已完成,并关照他们。

Task

从 .Net4.0 开始,Task 成为了实现 异步 的紧张利器。

Task 的用法与 JavaScript 中的 Promise 非常靠近。

Task 表示一个 异步 任务。
废话不多说,我们先写一个返回 Task 的方法。

// Task<int> 表示这是一个返回 int 类型的 Taskprivate Task<int> AsyncPower(int i){ // 返回一个任务 // 利用 Task.Run 相称于先创建了一个 Task // 再 Start return Task<int>.Run(() => { // 该任务会耗时 1 秒钟 Thread.Sleep(1000); // 1 秒钟后会返回参数的平方 return i i; });}

与之条件到的相同,我们有两种方法处理这个 Task 的结果 :

在主线程中等待结果在子线程中处理结果

我们看看两种模式分别是如何实现的

在主线程中等待结果

直接访问 Task.Result 属性,就可以等待并得到 异步 任务的结果。

var task = AsyncPower(10);// 这里会等 1 秒int result = task.Result; // result = 100

怎么样 ? 是不是超级大略 ?

在子线程中处理结果

利用方法 ContinueWith 可以添加一个方法,在 Task 完成后被实行。

这个 ContinueWith 和 JavaScript 里的 Promise 的 then 方法有着异曲同工的效果。

var task = AsyncPower(10);task.ContinueWith(t => { int result = t.Result; // result = 100});

怎么样 ? 是不是依然超级大略 ?

就像之前说的,Task 用起来就像 Promise,Promise 最大的特点便是可以用一步一步 then 下去。

$.get("someurl") .then(rlt => foo(rlt)) .then(rlt => bar(rlt));

Task 的 ContinueWith 也支持这样的编写方法 :

var task = AsyncPowe(10);task.ContinueWith(t => { // some code here}).ContinueWith(t => { // some code here}).CondinueWith(t => { // some code here});async / await

这个 .Net 4.5 加入的关键字,让 异步代码 写起来和 同步代码 没什么差异了。

我们先看看下面的同步代码

int Power(int i){ return i i;}void Main(){ int result = Power(10); Console.WriteLine(result); // 100 Console.ReadLine();}

把上面的代码改成异步代码,只须要几个小小的改动 :

将 Power 的返回值改为 Task<T>修正返回结果,利用 Task.Run 包装返回的结果为调用 Power 的代码前加上 await 关键字为有 await 关键字的方法加上 async 关键字

新的代码如下

Task<int> Power(int i){ return Task<int>.Run(()=> { Thread.Sleep(100); // 仿照耗时 return i i; });}async void Main(){ Console.WriteLine("Hello"); int result = await Power(10); Console.WriteLine(result); // 100 Console.ReadLine();}

运行一下,创造没什么差异。

如果你向掌握台输出线程ID的话,你会创造 Console.WriteLine("Hello") 和 Console.WriteLine(result) 并不事情在同一个线程同。

为什么会有这样的效果 ?

由于 编译器,你会创造,和之前的异步实现不同,async 和 await 不是某个封装了繁芜逻辑的类型,而是两个关键字。

关键字的意义便是编译过程。

在编译时,碰着 async 就会知道这是一个存在着异步的方法,编译器向这个类型添加一些额外的成员来支持 await 操作。

当碰着 await 关键字时,编译器会从当前行截断,并向后面的代码编译到 Task.ContinueWith 中去。

这样一来,看似同步的代码,经编译后,就会一拆为二。

前部分运行在主线程中,后部分运行在子线程中,分割点便是 await 所在的代码行。

慎用异步

几种在 .Net 平台中利用 异步 的方法都先容完了,希望大家能够对 异步 编程有了一定的理解和认识。

但是,在实际生产中,依赖要慎用异步。

异步 在带来性能提高的同时,还会带来一些更繁芜的问题:

线程安全

线程间的切换并不是有着类似 事务 的特色,它无法担保两个线程对同一资源的读写的完全性。

而且大部分情形下,一个线程操作完,就会被挂机实行另一个线程,以是对付多个线程访问同一资源,须要考虑线程安全的问题。

换句话说,便是担保一个线程在实行一个最小操作时,另一个线程不许可操作该工具。

调试难

异步 的实质便是 多线程 ,当你考试测验用断点调试代码时,由于两个线程都在你的代码中运行,因此常常涌现从这个线程的断点进入另一个线程的断点的情景。

须要依赖 IDE 中更多的工具和设置,才能办理上述的问题。

分歧一的高下文

异步 代码每每在子线程中运行。

子线程 很可能会利用在 主线程 中已经施放的资源。

比如

using(var conn = new SqlConnection("........")){ conn.Open(); // 假定一个根据用户名查询用户ID的方法 Tast<int> task = UserService.AsyncGetUserId(conn, "Admin"); task.ContinueWith(t => { // 此时的 conn 已经被主线程开释了 UserService.DoSomethingWithConn(conn); });}

你须要利用一些额外的代码来办理这些问题。
并且这些代码不一定具备通用性,每每要详细问题详细剖析。

因此在实际任务中,到底选择 同步 还是 异步 要视详细情形而定。

本日本文先容了几种实现 异步 的方法,不能说它们之间谁比谁更好一点,各有利害。

篇幅缘故原由,将不再对几种方案进行比拟,会在往后的文章中详细地先容各自利害。

标签:

相关文章