关于协程:漫谈协程coroutine

一 什么是协程协程当初曾经不是一个新的技术了,然而因为之前始终在用较低版本的c++,没什么机会应用协程。最近写了不少go的代码,接触到了协程,所以想从零开始学习一下协程。 1. 到底什么是协程之前据说协程的时候,大家都讲协程就是执行在用户态的微线程,加上go中协程的应用和线程差不多,我也就始终这样了解了。然而真正定义协程的性能是:能够随时的挂起和复原,它容许多个入口点在不同的执行点挂起和复原,而不是像线程那样在任何时候都可能被零碎强制切换。那么能够随时挂起和复原到底能解决什么问题呢?上面咱们来谈谈协程的劣势。 2. 协程的劣势协程领有轻量,高效,简略等劣势。 轻量:协程个别都是在各个语言的层面上做实现,线程依然是操作系统运算调度的最小单位,比起线程来,创立协程更加轻量。协程有多种实现形式,当咱们在一个线程上调配多个协程时,协程之间就不须要思考锁机制。高效:当咱们的线程在执行IO密集型操作时,往往须要期待IO后果,此时操作系统要么做线程的切换,而频繁的切换线程是一个和高额的操作,当应用协程的时候,咱们在线程内应用协程将操作挂起,期待IO实现时再继续执行,这样不会产生线程切换等操作。简化异步编程:在咱们应用rpc框架时,框架往往会提供同步,异步等调用形式,当同步调用其余接口时,以后线程会被阻塞,当异步调用其余接口时,就须要你提供一个回调函数,当有后果返回时,由框架将后果回吐给你。这种编程形式是不不便的,协程能够简化这个操作,前面咱们会举例说明。上面我会介绍协程是如何产生上述劣势的,行文逻辑如下,在第二个章节,我会介绍,当已知了协程的性能,应用协程的时候,咱们如何简化了异步编程;第三个章节咱们会介绍协程是如何实现咱们心愿的那些性能的。 二 应用协程异步编程应用异步做网络编程(实现业务逻辑)时,咱们的业务代码是有严格的执行程序的,然而异步的返回是无序的,就使得咱们,代码往往须要一些状态码来判断前置调用是否曾经实现,如果再叠加了异样解决这些逻辑的话,代码逻辑会十分艰涩难懂,而且容易经常性的造成回调天堂。举个例子,如果咱们应用异步回调的形式对一个整型数字做加3的操作,咱们有一个加1的函数,加3时须要调用三次: void AsyncAddOne(int val, std::function<void (int)> callback) { std::thread t([value, callback = std::move(callback)] { callback(val + 1); }); t.detach();}AsyncAddOne(1, [] (int result) { AsyncAddOne(result, [] (int result) { AsyncAddOne(result, [] (int result) { cout << "result is: " << result << endl; }); }); });看起来非常的艰涩难懂,当初大部门的服务框架其实曾经做了一些优化,比方应用Promise/Future个性。上面只是简略示意一下: AddOne.then({return AddOne.then({return AddOnde})})咱们拿一个在日常生产过程中的一段实例来示范Promise/Future个性,示例如下:这段代码的逻辑是应用了两个异步线程别离调用了redis和mysql,拿到后果后做本身的业务解决申请 // 第一个串行工作,CommonTasktrpc::Future<Result> CommonHandler() { // 1. do something in common handler return MakeReadyFuture<Result1>(res);}void HttpHandler() { // 1. 解决公共逻辑 auto http_task = CommonHandler(); // 2. 工作1实现后,创立并执行并行任务 auto data_task = http_task.Then([](Future<Result1>&& result1) { // 2.1 创立redis工作,通过redis_proxy发动调用, 并返回相干后果,cmd为申请redis的命令 trpc::Future<Result2> fut_redis_task = redis_proxy->AsyncRedis(cmd); // 2.2 创立mysql工作, 通过mysql_proxy发动调用, 并返回相干后果,cmd为申请mysql的命令 trpc::Future<Result2> fut_mysql_task = mysql_proxy->AsyncMysql(cmd); // 将单个工作退出parallel_futs parallel_futs.push_back(fut_redis_task); parallel_futs.push_back(fut_mysql_task); // 若并行任务2.1和2.2都实现了则完结该回调,并进入下一个回调 auto fut = WhenAll(parallel_futs).Then([](std::vector<Future<Result2>>&& result2) { // 别离取得redis和mysql的result, 进而实现相干工作 // result[0].GetValue(); // result[1].GetValue(); // 3. do something calc handler... return trpc::MakeReadyFuture<Resul3t>(res); }); return fut; }); // 回包 data_task.Then([](Future<Result3>&& result3){ if (result3.IsReady()) { // 4. do something and response to client // full succ in reply } else { // full exception in reply } SendUnaryResponse(reply); // 链式调用最初的then能够返回void });}尽管Future这种模式曾经简化了之前本人写代码判断各个异步工作的实现状态(实际上是封装在了Future本身的逻辑中),然而也有肯定的编程复杂度,尤其在波及到错误处理的时候。应用协程能够让咱们像应用一个线程做同步调用一样,来写咱们的一部调用代码。具体是如何做到的,能够参照下文的实现。 ...

February 19, 2024 · 1 min · jiezi

关于协程:react17源码浅析

前言 React17自去年十月公布以来,呈现了几个比拟重要的变动。首先,17作为一个过渡版本,其明确了在react中的定位,即:承前启后,作为渐进式框架的首版本,在后续的18、19等版本中会进行渐进降级而不是强制进行硬切换;其次,17联合最新的浏览器的个性做了一些更改和优化,比方对合成事件零碎的优化;最初,自16以来的基于Fiber架构的模式对整个react性能优化在每个小版本中也会一直的进行逐渐的微调,每次的微调都走漏着react大佬们的一些思路与思考。本文以react17.0.0版本的源码动手,着重从react-reconciler和scheduler这两个模块中的局部源码进行拆解和浅析,心愿可能窥一斑而见全豹,揣度各位大佬的一些架构思路和想法,从而拓宽一些集体的眼界,然集体程度无限,不免洞若观火,对react的了解也可能有所偏颇,本着想将这样一个宏大的架构简洁剖析进去的想法,心愿能对各位同学有所启发,对于更为经典的局部,仍须要各位去品读源码,咱们依然应该对源码放弃着一颗敬畏之心,随着技术的晋升,每每品读都会有不同的感触! 目录构造 react目录比拟大,涉猎的也比拟多,这里只显示可能会波及局部的目录 packages react src jsx ReactJSX.jsReactJSXElement.js (定义了jsx)ReactJSXElementValidator.jsReact.jsReactBaseClasses.js (setState及forceState)ReactContext.js (context上下文及Provider和Consumer)ReactElement.js (定义了ReactElement的格局)ReactForwardRef.js (Ref的定义)ReactHooks.js (ReactHooks相干,不是本文重点,能够参看之前的文章)ReactLazy.jsReactMemo.jsReactMutableSource.jsReactStartTransition.js (批量更新的事务的概念)react-dom src clients ReactDOMHostConfig.jsReactDOMLegacy.jsReactDOMRoot.js (三种模式:legacy模式、blocking模式、concurrent模式)events EventListeners.jsReactDOMEventListeners.jsSyntheticEvent.jsserver (React Sever Component相干,不开展讲了)react-reconciler src ReactChildFiber.jsReactFiber.jsReactFiberBeginWork.jsReactFiberCommitWork.jsReactFiberCompleteWork.jsReactFiberLane.jsReactFiberReconciler.jsReactFiberRoot.jsReactFiberWorkLoop.jsscheduler src Scheduler.jsSchedulerMinHeap.jsSchedulerPostTask.jsSchedulerProfiling.js源码解析整个React的Fiber架构的外围在于对浏览器进行了工夫分片解决(ps:Firefox新版本本身也实现了工夫分片),抹掉了平台的差别,从而使得浏览器解决时候能够将控制权交出去,防止了js线程过多占用而阻塞渲染线程,实现了更细粒度的调度,即为:协程或纤程的调度 React文件名作用备注jsx-runtime.jsjsx解释器编译jsxReactElement.jsReact元素的格局React的结点格局信息jsx-runtime.jsreact17之后不再须要对每个react组件进行react的import,其内置了一个jsx-runtime的运行时,感兴趣的同学能够看一下这个react-jsx-dev-runtime.development.js,简略来说就是利用正则对jsx进行了一层浅的转化,实质jsx是对js的一种扩大 ReactElement.jsconst ReactElement = function( type, key, ref, self, source, owner, props) { const element = { $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, ref: ref, props: props }}React-DOM这里次要对合成事件及DOM的一些解决进行了论述 <h2>DOM解决</h2> 文件名作用备注ReactDOMHostConfig.jsappendChildToContainer等原生dom操作ReactDOMLegacy.jslegacyRenderSubtreeIntoContainer未启用异步渲染的dom操作ReactDOMRoot.jscreateRoot、createLegacyRoot、createBlockingRoot三种模式的根组件React17的进行了模式的设置,别离为:Legacy模式、Concurrent模式、Blocking模式,其中Concurrent模式是启用fiber分片的异步渲染形式,而Legacy模式则仍是15的同步渲染模式,Blocking则是介于二者之间的模式,React无意依照这样一种渐进的形式进行适度 <h2>合成事件</h2> 文件名作用备注EventListeners.jsaddEventCaptureListener、addEventBubbleListener原生事件监听ReactDOMEventListeners.jsdispatchEventReact的事件SyntheticEvent.jscreateSyntheticEvent合成事件ReactDOMEventListeners.js外围是dispatchEvent进行事件的散发,17之后不再将事件全副冒泡到document去代理,这和浏览器的改良无关,不再须要代理绑定,浏览器能够对更细粒度的区域进行监听 function dispatchDiscreateEvent() {}function dispatchBlockingEvent() {}export function dispatchEvent( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent,): void { if ( allowReplay && hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(domEventName) ) { queueDiscreteEvent( null, // Flags that we're not actually blocked on anything as far as we know. domEventName, eventSystemFlags, targetContainer, nativeEvent, ); return; } const blockedOn = attemptToDispatchEvent( domEventName, eventSystemFlags, targetContainer, nativeEvent, ); if (blockedOn === null) { // We successfully dispatched this event. if (allowReplay) { clearIfContinuousEvent(domEventName, nativeEvent); } return; } if (allowReplay) { if (isReplayableDiscreteEvent(domEventName)) { queueDiscreteEvent( blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent, ); return; } if ( queueIfContinuousEvent( blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent, ) ) { return; } clearIfContinuousEvent(domEventName, nativeEvent); } dispatchEventForPluginEventSystem( domEventName, eventSystemFlags, nativeEvent, null, targetContainer, );}Scheduler实质是依据工作开始工夫和过期工夫利用小顶堆的优先队列而进行的工夫分片解决及调度 ...

March 9, 2021 · 31 min · jiezi

关于协程:CC协程学习笔记丨CC实现协程及原理分析视频

协程,又称微线程,纤程。英文名Coroutine。 协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中失去广泛应用。 子程序,或者称为函数,在所有语言中都是层级调用,比方A调用B,B在执行过程中又调用了C,C执行结束返回,B执行结束返回,最初是A执行结束。 所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。 子程序调用总是一个入口,一次返回,调用程序是明确的。而协程的调用和子程序不同。 协程看上去也是子程序,但执行过程中,在子程序外部可中断,而后转而执行别的子程序,在适当的时候再返回来接着执行。 留神,在一个子程序中中断,去执行其余子程序,不是函数调用,有点相似CPU的中断。比方子程序A、B:def A(): print '1'print '2'print '3'def B():print 'x'print 'y'print 'z'假如由协程执行,在执行A的过程中,能够随时中断,去执行B,B也可能在执行过程中中断再去执行A,后果可能是: 12xy3z然而在A中是没有调用B的,所以协程的调用比函数调用了解起来要难一些。 看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何劣势? 最大的劣势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序本身管制,因而,没有线程切换的开销,和多线程比,线程数量越多,协程的性能劣势就越显著。 第二大劣势就是不须要多线程的锁机制,因为只有一个线程,也不存在同时写变量抵触,在协程中管制共享资源不加锁,只须要判断状态就好了,所以执行效率比多线程高很多。 因为协程是一个线程执行,那怎么利用多核CPU呢?最简略的办法是多过程+协程,既充分利用多核,又充分发挥协程的高效率,可取得极高的性能。 Python对协程的反对还十分无限,用在generator中的yield能够肯定水平上实现协程。尽管反对不齐全,但曾经能够施展相当大的威力了。 来看例子: 传统的生产者-消费者模型是一个线程写音讯,一个线程取音讯,通过锁机制管制队列和期待,但一不小心就可能死锁。 如果改用协程,生产者生产音讯后,间接通过yield跳转到消费者开始执行,待消费者执行结束后,切换回生产者持续生产,效率极高:import time def consumer():r = ''while True:n = yield rif not n:returnprint('[CONSUMER] Consuming %s...' % n)time.sleep(1)r = '200 OK'def produce(c):c.next()n = 0while n < 5:n = n + 1print('[PRODUCER] Producing %s...' % n)r = c.send(n)print('[PRODUCER] Consumer return: %s' % r)c.close()if __name__=='__main__':c = consumer()produce(c)执行后果: ...

October 26, 2020 · 6 min · jiezi

iOS知识梳理-异步编程-coobjc学习

这几年异步编程是个比较热门的话题。 今天我们在iOS平台下简单聊聊异步编程和coobjc。 首先要回答一个问题,我们为什么需要异步编程? 早年的时候,大家都很习惯于开一个线程去执行耗时任务,即使这个耗时任务并非CPU密集型任务,比如一个同步的IO或网络调用。但发展到今日,大家对这种场景应该使用异步而非子线程的结论应当没什么疑问。开线程本身开销相对比较大,并且多线程编程动不动要加锁,很容易出现crash或更严重的性能问题。而iOS平台,系统API有不少就是这种不科学的同步耗时调用,并且GCD的API算是很好用的线程封装,这导致iOS平台下很容易滥用多线程引发各种问题。 总而言之,原则上,网络、IO等很多不耗CPU的耗时操作都应该优先使用异步来解决。 再来看异步编程的方案,iOS平台下常用的就是delegate和block回调。delegate导致逻辑的割裂,并且使用场景比较注重于UI层,对大多数异步场景算不上好用。 而block回调语法同样有一些缺陷。最大的问题就是回调地狱: [NSURLConnection sendAsynchronousRequest:rq queue:nil completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) { if (connectionError) { if (callback) { callback(nil, nil,connectionError); } } else{ dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; NSString *imageUrl = dict[@"image"]; [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:imageUrl]] queue:nil completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ if (connectionError) { callback(nil, dict,connectionError); } else{ UIImage *image = [[UIImage alloc] initWithData:data]; if (callback) { (image, dict, nil); } } }); }]; }); } }]不过iOS开发中好像并没有觉得很痛,至少没有前端那么痛。可能是因为我们实际开发中对比较深的回调会换成用delegate或notificaiton机制。但这种混杂的使用对代码质量是个挑战,想要保证代码质量需要团队做很多约束并且有很高的执行力,这其实很困难。 ...

November 5, 2019 · 3 min · jiezi

Kotlin协程教程1启动

协程协程简单的来说,就是用户态的线程。 emmm,还是不明白对吧,那想象一个这样的场景,如果在一个单核的机器上有两个线程需要执行,因为一次只能执行一个线程里面的代码,那么就会出现线程切换的情况,一会需要执行一下线程A,一会需要执行一下线程B,线程切换会带来一些开销。 假设两个线程,交替执行,如下图所示 线程会因为Thread.sleep方法而进入阻塞状态(就是什么也不会执行),这样多浪费资源啊。 能不能将代码块打包成一个个小小的可执行片段,由一个统一的分配器去分配到线程上去执行呢,如果我的代码块里要求sleep一会,那么就去执行别的代码块,等会再来执行我呢。 协程就是这样一个东西,我们作为使用者不需要再去考虑创建一个新线程去执行一坨代码,也不需要关心线程怎么管理。我们需要关心的是,我要异步的执行一坨代码,待会我要拿到它的结果,我要异步的执行很多坨代码,待会我要按某种顺序,或者某种逻辑得到它们的结果。 总而言之,协程是用户态的线程,它是在用户态实现的一套机制,可以避免线程切换带来的开销,可以高效的利用线程的资源。 从代码上来讲,也可以更漂亮的写各种异步逻辑。 这里想再讲讲一个概念,阻塞与非阻塞是什么意思 阻塞与非阻塞简单来说,阻塞就是不执行了,非阻塞就是一直在执行。比如 Thread.wait() // 阻塞了// 这里执行不到了但是,如果 while (true) { // 一直在运行,没有阻塞 i++;}// 这里也执行不到了runBlocking:连接阻塞与非阻塞的世界runBlocking是启动新协程的一种方法。 runBlocking启动一个新的协程,并阻塞它的调用线程,直到里面的代码执行完毕。 举个例子 println("aaaaaaaaa ${Thread.currentThread().name}")runBlocking { for (i in 0..10) { println("$i ${Thread.currentThread().name}") delay(100) }}println("bbbbbbbbb ${Thread.currentThread().name}")上面代码的输出为: aaaaaaaaa main0 main1 main2 main3 main4 main5 main6 main7 main8 main9 main10 mainbbbbbbbbb mainemmm,这并没有什么稀奇,所有的代码都在主线程执行,按照顺序来,去掉runBlocking也是一样的嘛。 但是,runBlocking可以指定参数,就可以让runBlocking里面的代码在其他线程执行,但同样可以阻塞外部线程。 println("aaaaaaaaa ${Thread.currentThread().name}")runBlocking(Dispatchers.IO) { // 注意这里 for (i in 0..10) { println("$i ${Thread.currentThread().name}") delay(100) }}println("bbbbbbbbb ${Thread.currentThread().name}")上面的代码,给runBlocking添加了一个参数,Dispatchers.IO,这样里面的代码块就会执行到其他线程了。 ...

August 28, 2019 · 2 min · jiezi

Swoole-44支持-CURL-协程化

在4.4之前的版本中,Swoole一直不支持CURL协程化,在代码中无法使用curl。由于curl使用了libcurl库实现,无法直接hook它的socket,4.4版本使用Swoole\Coroutine\Http\Client模拟实现了curl的API,并在底层替换了curl_init等函数的C Handler。 提示CURL Hook的特性尚处于试验阶段,请勿在生产环境中直接使用暂不支持文件上传、CURL Multi仍然需要依赖curl,请务必安装curl扩展支持的特性列表GET/POSTHeaderCookieHttps经过验证Guzzle CURL完全可以使用开启使用Runtime::enableCoroutine来开启CURL Hook。 默认不开启CURL HookSwoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL);Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_CURL);使用$n = 10;while($n--) { go(function () { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "http://www.xinhuanet.com/"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HEADER, 0); $output = curl_exec($ch); if ($output === FALSE) { echo "CURL Error:" . curl_error($ch); } curl_close($ch); echo strlen($output) . " bytes\n"; });}要将上面两段代码合并到一个文件中执行运行结果htf@LAPTOP-0K15EFQI:~/swoole-src/examples$ time php curl.php177173 bytes177173 bytes177173 bytes177173 bytes177173 bytes177173 bytes177173 bytes177173 bytes177173 bytes177173 bytesreal 0m0.534suser 0m0.031ssys 0m0.297s可以看到整个程序是并行的,进程没有任何阻塞。 strace 跟踪使用strace跟踪发现,所有系统调用均变成epoll+socket的异步非阻塞调用了。 ...

June 5, 2019 · 10 min · jiezi

Swoole协程抢占式调度

前言Swoole内核团队开设的专栏,会逐渐投入精力写文章介绍Swoole的开发历程,实现原理,应用实践等,大家可以更好的交流,共同学习,建设PHP生态。 协程调度去年Swoole推出了4.0版本后,完整的支持PHP协程,我们可以基于协程实现CSP编程,身边的开发者惊呼,原来PHP代码还可以这样写。Swoole的协程默认是基于IO调度,程序中有阻塞会自动让出当前协程,协程的各种优势我们不在这里展开讨论。如果是IO密集型的场景,可以表现得很不错。但是对于CPU密集型的场景,会导致一些协程因为得不到CPU时间片被饿死。 抢占式调度我们在今年年初就计划实现Swoole的抢占式调度,以满足实现有些场景下的不均衡调度带来的问题。我们中间经历了几个版本,在这里和大家分享一下开发过程中的动机和解决办法。 起初,我们的想法是可以从PHP的循环中自动检测执行实践,若达到限制,可以自动让出当前协程。因为毕竟很少有人一马平川的写出占用很多CPU的代码,大都通过循环条件来控制。我们hook循环指令,每次执行循环指令的时候,都来检查协程的执行时间,我们很欣喜的得到了最初的版本。但是这样做比较hack,而且opcode经过opcache优化后,情况会变得有些复杂。 后来我们使用PHP的ticks机制,也就是在PHP代码编译期间,注入ticks指令,可以执行相应的函数,我们可以在这些函数中检测处理协程的时间,达到抢占式的效果,但是这里有一个问题,PHP的declare(ticks=N)语法,只对当前脚本范围有效,也就是说项目稍微大点,require或者include进来的脚本,并不会自动注入ticks指令,这样Swoole开发者几乎是无法接受的。我们也试图给PHP官方提一个PR,可以在扩展层设置一个全局默认的ticks,但是官方不愿意采纳我们的提交,因为官方觉得这个功能对性能损耗比较大,而且有可能在PHP8移除这个功能。其实经过实测这个性能损耗并不大,而且我们已经在生产环境验证,并取得了显著的效果,即可以让出某些CPU密集的逻辑部分,使得服务整个相应时间更加均衡。 想要做抢占式调度,对于PHP来说,有两个途径 单线程的PHP的执行流,通过执行指令做文章,可以在PHP执行流程中注入逻辑,以检查执行时间,再急上Swoole的协程能力,可以在不同的协程中切换,以达到抢占CPU的目的。考虑开线程,负责检查当前执行协程执行时间。经过以上办法的尝试,注入指令的路数基本是无法得到官方的支持,我们只能另谋出路,多开一个线程,只负责检查当前协程。具体的做法是,利用PHP-7.1.0引入的VM interrupt机制,默认每隔5ms检查一下当前协程是否达到最大执行时间,默认为10ms,如果超过,则让出当前协程,达到被其他协程抢占的目的。 来一段代码 <?phpCo::set(['enable_preemptive_scheduler' => 1]);$start = microtime(1);echo "start\n";$flag = 1;go(function () use (&$flag) { echo "coro 1 start to loop\n"; $i = 0; for (;;) { if (!$flag) { break; } $i++; } echo "coro 1 can exit\n";}); $end = microtime(1);$msec = ($end - $start) * 1000;echo "use time $msec\n";go(function () use (&$flag) { echo "coro 2 set flag = false\n"; $flag = false;});echo "end\n";//输出结果startcoro 1 start to loopuse time 11.121988296509coro 2 set flag = falseendcoro 1 can exit可以发现,代码逻辑可以从第一个协程的死循环中自动yield出来,执行第二个协程,如果没有这个特性,第二个协程永远不会被执行,导致被饿死。而这样做,第二个协程可以顺利被执行,最后执行结束后,第一个协程也会接着继续往下执行。达到我们的第二个协程主动抢占第一个协程CPU的效果。 ...

May 21, 2019 · 1 min · jiezi

php框架

php框架的功能通用的路由,autoload。服务端mysql封装,日志组件。前端的页面渲染(smarty封装个)。在工作时听说别的部门换框架性能提升,所以调研了下常见的框架,包含ci,laravel,yii,yaf,会介绍下功能,另外给出号称最快框架yaf和常用yii和裸写框架的性能差。另外想实现rpc并发,http一般用过multi_curl可以,公司用的thrift没有实现并发,所以研究了下php协程,curl_multi,swoole异步,rpc中并发实现,corotine等。 常用php框架提供功能ci http://codeigniter.org.cn/use...Route.autoload.载入文件正常controller 中load->helper(xx)。直接用xxlog. db. hook.公共函数。代码逻辑分层。ui抽象。模板。laravel https://laravel-china.org/doc...路由中间件(前置后置)配置区分环境,本地和线上密码不放其中数据库 创建表,编辑,删除,迁移,回滚,软删除和恢复(标记删除位)。ORM链式操作依赖注入,依赖自动发现。 IOC 平时的if new 这种工厂模式,IoC模式看作工厂模式的升华,以前在工厂模式里写死了的对象,IoC模式 改为配置XML文件,这就把工厂和要生成的对象两者隔离类(DatabaseQueue,queue,QueueContract),serviceprovider(外部调这个)=>bind(将类绑定到容器)。调用Queue::xx。依赖注入可以直接调用$类->method。通过门面可以类::method【https://www.cnblogs.com/shiwenhu/p/6882340.html】事件 事件映射protected $listen = [ 'App\Events\OrderShipped' => [ 'App\Listeners\SendShipmentNotification', ],];写事件OrderShipped,写监听SendShipmentNotification 可以继承队列分发事件:public function ship($orderId) { order=Order::findOrFail(orderId); // 订单的发货逻辑... event(new OrderShipped($order)); }队列 ,redis,db, 广播js监听任务调度 只是cron yii https://www.yiichina.com/doc/... 功能全面读介于ci和laravel之间,前端支持功能丰富。组件和行为是它的特色行为 要定义行为,通过继承 yii\base\Behavior 。覆盖其中的events方法,public function events() { return [ ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate', ]; } public function beforeValidate($event) { // 处理器方法逻辑 },附加行为:lass User extends ActiveRecord { public function behaviors() { return [ [ 'class' => MyBehavior::className(), 'prop1' => 'value1', 'prop2' => 'value2', ]]}}或者attach组件就可以使用行为了。直观感受下 ...

May 15, 2019 · 3 min · jiezi

协程 C/C++ 扩展开发指南(1):内存安全

Swoole4 协程的出现使得 PHP 底层上从原来串行模式变成了并发模式。有很多 PHP 的C/C++扩展在开发时未能考虑到并发性、可重入问题,导致无法在Swoole协程中使用。本文会详细讲解如何编写协程并发安全的C/C++代码。可重入性示例代码:int t;void test1(int *x, int *y) { t = *x; *x = *y; //fun1 函数中可能会存在协程切换 fun1(); //错误代码 y = t;}t是一个全局变量或者static静态变量在协程A中调用了test1函数,使用了全局变量t当函数内调用了fun1(),这个函数中如果发生了协程切换,这时假如另外一个协程B也执行了test1函数,那么t的值可能会被修改协程B挂起时,重新回到协程A,这时y = t,会得到一个错误的值引用栈内存这也是一个严重的风险点。协程1将自身栈内存的指针发送给另外一个协程2,协程1退出时会释放协程栈内存。协程2的生命周期长于1,继续读写此内存,就会导致segment fault。示例:void co1() { char buf[2048]; //这里启动一个新的协程,buf 是协程1栈上内存 co2(buf); //协程1 退出时会释放栈内存}void co2(char *buf) { for(int i=0; i<2048; i++) { Coroutine::sleep(1); //这里 buf 内存可能已经释放了 buf[i] = 1; }}协程安全代码为了保证安全性,在Swoole4协程编程中:不要使用static变量和全局变量,坚持只用局部变量若必须访问全局变量,必须保证只用于计算逻辑,不得存在任何IO或Sleep等引起协程切换的操作不调用其它任何不可重入的函数不要引用栈上内存

March 17, 2019 · 1 min · jiezi

PHP的yield是个什么玩意(一)

其实,我并不是因为迭代或者生成器或者研究PHP手册才认识的yield,要不是协程,我到现在也不知道PHP中还有yield这么个鬼东西。人家这个东西是从PHP 5.5就开始引入了,官方名称叫做生成器。你要说为什么5.5年代的东西,现在才拿出来。我还想问你哟,PHP 5.3就有了的namespace为毛到最近这几年才开始正式投产。那么,问题来了,这东西到底是有何用?先来感受一个问题,给你100Kb的内存(是的,你没有看错,就是100Kb),然后让你迭代输出一个从1开始一直到10000的数组,步进为1。愈先迭代数组,必先创造数组。所以,脑门一拍,代码一坨如下:<?php$start_mem = memory_get_usage();$arr = range( 1, 10000 );foreach( $arr as $item ){ //echo $item.’,’;}$end_mem = memory_get_usage();echo " use mem : “. ( $end_mem - $start_mem ) .‘bytes’.PHP_EOL;一顿操作猛如虎,运行一下成绩1-5,你们感受一下:528440bytes,约莫就是528Kb,几乎是100Kb的五倍了,妈的这日子没法过了。毕竟你们也知道,最近内存价格确实贵,国家也在号召低碳节能减排,你多耗费5倍内存,就意味着多排放5倍的二氧化碳,就意味着要为多用的内存多花钱贡献给棒子… …你想想,那可是棒子。人都是被逼出来的,于是yield可以来救场了,大概代码如下,注意看操作:<?php$start_mem = memory_get_usage();function yield_range( $start, $end ){ while( $start <= $end ){ $start++; yield $start; }}foreach( yield_range( 0, 9999 ) as $item ){ echo $item.’,’;}$end_mem = memory_get_usage();echo " use mem : “. ( $end_mem - $start_mem ) .‘bytes’.PHP_EOL;运行一下,你们感受一下:首先,我们观察一下yield_range这个函数跟普通函数不一样的地方,就是普通函数往往都是使用return来返回结果,而这个中则是yield。其次是普通函数中return只能返回一次,这个yield能返回好多次。那么,我们来分析一波儿这个神奇的yield_range函数。这个yield关键字到底返回的是什么?我们简单看一下:<?phpfunction yield_range( $start, $end ){ while( $start <= $end ){ $start++; yield $start; }}$rs = yield_range( 1, 100 );var_dump( $rs );/object(Generator)#1 (0) {}/yield返回的是一个叫做Generator(中文名就是生成器)的object对象,而这个生成器是实现了Iterator接口(至于Iterator接口,你们去PHP手册上搜索吧)。所以,既然实现了Iterator接口(也正是因为如此,这个东西可以使用foreach进行迭代,明白了吧?),所以可以有如下代码:<?phpfunction yield_range( $start, $end ){ while( $start <= $end ){ yield $start; $start++; }}$generator = yield_range( 1, 10 );// valid() current() next() 都是Iterator接口中的方法while( $generator->valid() ){ echo $generator->current().PHP_EOL; $generator->next();}运行结果如下所示:重点来了:这个yield_range函数似乎能够记住它上一次运行到哪儿了,上一次运行的结果是什么,然后紧接着在下一次运行的时候继续从上次终止的地方继续开始。这不是普通的PHP函数可以做得到的!我们知道,操作系统在调度进程的时候,会触发一个叫做“进程上下文切换”的概念。比如CPU从进程A调度给进程B了,那么当再次从进程B调度给进程A的时候,当初进程A运行到哪儿了、临时的数据结果是什么都是需要被还原的,不然,一切都要从头,那就要出大问题了。而,这个yield关键字,似乎在用户态(非系统内核级)就可以实现这个概念。所以说,用yield搞迭代,怕是真的很没出息的一件事,它能做的太多。紧接着,我们需要认识一个生成器对象的一个方法,叫做send,简单看下下面这坨代码:<?phpfunction yield_range( $start, $end ){ while( $start <= $end ){ $ret = yield $start; $start++; echo “yield receive : “.$ret.PHP_EOL; }}$generator = yield_range( 1, 10 );$generator->send( $generator->current() * 10 );运行结果如图所示:send方法可以修改yield的返回值,但是,你也不能想当然,比如下面这坨代码,你们以为运行结果是什么样呢?<?phpfunction yield_range( $start, $end ){ while( $start <= $end ){ $ret = yield $start; $start++; echo “yield receive : “.$ret.PHP_EOL; }}$generator = yield_range( 1, 10 );foreach( $generator as $item ){ $generator->send( $generator->current() * 10 );}本来以为运行结果是类似于这样的:<?phpyield receive : 10yield receive : 20yield receive : 30yield receive : 40yield receive : 50yield receive : 60yield receive : 70yield receive : 80yield receive : 90yield receive : 100然而,唯物主义告诉我们:结果是打脸的,你们感受一下:原因是什么呢?原因是当你在外部向yield发送send的时候,会自动触发一次next,自己动手试下吧。最近开了一个微信公众号,所有文章都在这里(手贱弄成服务号了) ...

March 11, 2019 · 1 min · jiezi

填坑之PHP的yield和协程在一起的日子里(二)

首先是,这是我第一次把公众号文章复制粘贴到sf.gg来。其次是,很久很久之前,我挖了一个yield的一个坑,自己挖的坑自己填,不然迟早会把自己埋掉。最后是,如果想看之前那个坑,请发送“yield”给文章末尾的公众号,我开通了高大上的自动回复功能,稀罕地不得了!PS:那篇文章中在最后我犯了一个错误,误下了一个结论:foreach中不能使用send并猜测这是PHP的bug,实际上并不是,真实的原因粗暴简单的理解就是send会让生成器继续执行一次导致。这件事情告诉我们:除了装逼之外,甩锅也是有打脸风险的那篇坑里,内容和你能在百毒上搜索到的大多数文章都是差不多的,不过我那篇坑标题起得好:《yield是个什么玩意(上)》,也就是暗示大家还有下篇,所以起标题也是需要一定技术含量的。我坚信,在座的各位辣鸡在看完上篇坑文后最想说的注定是泰迪熊这句话(这是文化属性,不以各位的意志而转移):回到今天主旨上来,强调几点:虽然文章标题中有“yield和协程”这样的关键字,但实际上yield并不是协程,看起来有不少人直接将yield和协程划了等号。yield的本质是生成器,英文名字叫做Generator。yield只能用在function中,但用了yield就已经不是传统意义上的function了,同时如果你企图在function之外的其他地方用yield,你会被打脸。yield的最重要作用就是:自己中断一坨代码的执行,然后主动让出CPU控制权给路人甲;然后又能通过一些方式从刚才中断的地方恢复运行。这个就比较屌了,假如你请求了一个费时10s的服务器API,此时是可以让出CPU给路人甲。粗暴地说上面的过程就算是协程的基本概念。多线程和多进程都是操作系统参与的调度,而协程是用户自主实现的调度,协程的关键点实际上是“用户层实现自主调度”,大概有“翻身农奴把歌唱”的意思。下面我通过一坨代码来体会一把“翻身农奴”,你们感受一下:<?phpfunction gen1() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN1 : {$i}".PHP_EOL; // sleep没啥意思,主要就是运行时候给你一种切实的调度感,你懂么 // 就是那种“你看!你看!尼玛,我调度了!卧槽” sleep( 1 ); // 这句很关键,表示自己主动让出CPU,我不下地狱谁下地狱 yield; }}function gen2() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN2 : {$i}".PHP_EOL; // sleep没啥意思,主要就是运行时候给你一种切实的调度感,你懂么 // 就是那种“你看!你看!尼玛,我调度了!卧槽” sleep( 1 ); // 这句很关键,表示自己主动让出CPU,我不下地狱谁下地狱 yield; }}$task1 = gen1();$task2 = gen2();while( true ) { // 首先我运行task1,然后task1主动下了地狱 echo $task1->current(); // 这会儿我可以让task2介入进来了 echo $task2->current(); // task1恢复中断 $task1->next(); // task2恢复中断 $task2->next();}上面代码执行结果如下图:虽然我话都说到这里了,但是肯定还是有人get不到“所以,到底发生了什么?”。你要知道,如果function gen1和function gen2中没有yield,而是普通函数,你是无法中断其中的for循环的,诸如下面这样的代码:<?phpfunction gen1() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN1 : {$i}".PHP_EOL; sleep( 1 ); }}function gen2() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN2 : {$i}".PHP_EOL; }}gen1();gen2();// 看这里,看这里,看这里!// 上面的代码一旦运行,一定是先运行完gen1函数中的for循环// 其次才能运行完gen2函数中的for循环,绝对不会出现// gen1和gen2交叉运行这种情况我似乎已然精通了yield写到这里后我也开始蹩了,和以往的憋了三天蹦不出来个屁有所不同,我这次蹩出了一个比较典型的应用场景:curl。下面我们基于上面那坨辣鸡代码将gen1修改为一个耗时curl网络请求,gen2将向一个文本文件中写内容,我们的目的就是在耗时的curl开始后主动让出CPU,让gen2去写文件,以实现CPU的最大化利用。<?php$ch1 = curl_init();// 这个地址中的php,我故意sleep了5秒钟,然后输出一坨jsoncurl_setopt( $ch1, CURLOPT_URL, “http://www.selfctrler.com/index.php/test/test1" );curl_setopt( $ch1, CURLOPT_HEADER, 0 );$mh = curl_multi_init();curl_multi_add_handle( $mh, $ch1 );function gen1( $mh, $ch1 ) { do { $mrc = curl_multi_exec( $mh, $running ); // 请求发出后,让出cpu yield; } while( $running > 0 ); $ret = curl_multi_getcontent( $ch1 ); echo $ret.PHP_EOL; return false;}function gen2() { for ( $i = 1; $i <= 10; $i++ ) { echo “gen2 : {$i}".PHP_EOL; file_put_contents( “./yield.log”, “gen2”.$i, FILE_APPEND ); yield; }}$gen1 = gen1( $mh, $ch1 );$gen2 = gen2();while( true ) { echo $gen1->current(); echo $gen2->current(); $gen1->next(); $gen2->next();}上面的代码,运行以后,我们再等待curl发起请求的5秒钟内,同时可以完成文件写入功能,如果换做平时的PHP程序,就只能是先阻塞等待curl拿到结果后才能完成文件写入。文章太长,就像“老太太的裹脚布一样,又臭又长”,所以,最后再对代码做个极小幅度的改动就收尾不写了!<?php$ch1 = curl_init();// 这个地址中的php,我故意sleep了5秒钟,然后输出一坨jsoncurl_setopt( $ch1, CURLOPT_URL, “http://www.selfctrler.com/index.php/test/test1" );curl_setopt( $ch1, CURLOPT_HEADER, 0 );$mh = curl_multi_init();curl_multi_add_handle( $mh, $ch1 );function gen1( $mh, $ch1 ) { do { $mrc = curl_multi_exec( $mh, $running ); // 请求发出后,让出cpu $rs = yield; echo “外部发送数据{$rs}".PHP_EOL; } while( $running > 0 ); $ret = curl_multi_getcontent( $ch1 ); echo $ret.PHP_EOL; return false;}function gen2() { for ( $i = 1; $i <= 10; $i++ ) { echo “gen2 : {$i}".PHP_EOL; file_put_contents( “./yield.log”, “gen2”.$i, FILE_APPEND ); $rs = yield; echo “外部发送数据{$rs}".PHP_EOL; }}$gen1 = gen1( $mh, $ch1 );$gen2 = gen2();while( true ) { echo $gen1->current(); echo $gen2->current(); $gen1->send(“gen1”); $gen2->send(“gen2”);}我们修改了内容:将$gen1->next()修改成了$gen1->send(“gen1”)在function gen1中yield有了返回值,并且将返回值打印出来这件事情告诉我们:yield和send,是可以双向通信的,同时告诉我们send可以用来恢复原来中断的代码,而且在恢复中断的同时可以携带信息回去。写到这里,你是不是觉得这玩意的可利用价值是不是比原来高点儿了?我知道,有人肯定叨叨了:“老李,你代码特么写的真是辣鸡啊!你之前保证过了的 — 只在公司生产环境写辣鸡代码的。可你看看你这辣鸡光环到笼罩都到demo里了,你连demo都不放过了!你怎么说?!”。兄dei,“又不是不能用”。而且我告诉你,上面这点儿curl demo来讲明白yield还是不够的,后面还有两三篇yield呢,照样是烂代码恶心死你,爱看不看。我劝你心放宽,你想想你这么烂的代码都经历了,还有什么不能经历的?文章最后补个小故事:其实yield是PHP 5.5就已经添加进来了,这个模块的作者叫做Nikita Popov,网络上的名称是Nikic。我们知道PHP7这一代主力是惠新宸,下一代PHP主力就是Nikic了。早在2012年,Nikic就发表了一篇关于PHP yield多任务的文章,链接我贴出来大家共赏一下 — http://nikic.github.io/2012/1…最近开了一个微信公众号,所有文章都在这里(手贱弄成服务号了) ...

March 11, 2019 · 2 min · jiezi

Swoole 2019 :化繁为简、破茧成蝶

Swoole开源项目从2012年开始发布第一个版本,到现在已经有近7年的历史。在这七年的时间里:提交了8821次代码变更发布了287个版本收到并解决1161次issue反馈合并了603次pull request共有100位开发者贡献代码在GitHub收获了11940颗星协程2018年我们推出了全新的Swoole4版本,在此之前Swoole主要的编程方式还是同步阻塞模式或异步回调。新的基于协程实现的CSP编程逐渐成为我们唯一推荐使用的编程模式。协程将纷繁复杂异步编程大大简化。使用Swoole4协程,既简单又强大。在未来的Swoole5版本,我们计划删除非协程的相关特性和代码,减少历史包袱,提升稳定性,降低复杂度,减少不必要的选项,纯粹协程化。 过去前六我们的团队主要以兼职开发为主,团队成员大多来自于腾讯、阿里、滴滴、百度、360、小米等国内一线互联网企业,还有一部分是国外的PHP开发者,甚至PHP语言ZendVM内核作者Dmitry Stogov也曾向Swoole贡献了代码。除此之外,我们还招募了一些在校大学生为Swoole编写代码,逐步培养年轻一代开发者。在2018年7月份我们组建了全职开发团队,专注于Swoole内核以及Swoole Cloud云原生组件和生态链的开发。告别过去的草莽班子,转变为专业化的开源技术研发团队。我们的目标是让Swoole项目成为Node.js、Go这样的工业级技术,成为PHP编程语言的在异步IO和网络通信方面的基石。研发管理成立全职研发团队后,我们逐渐建立了非常完善的研发管理体系,提升Swoole的软件质量。主要包括以下几个方面:测试驱动(TDD)现在我们投入大量精力实现单元测试脚本、压测脚本、自动化测试,提升单元测试覆盖率。目前已有680项测试用例,17项压测项目,在Travis-CI平台可以看到每一次Commit和Pull Request的编译、测试结果。研发工作也基于TDD进行,在开发新特性、重构、Bug Fix时,会先编写对应的单元测试脚本,测试覆盖到代码变更的所有场景。代码审查(Code Review)团队成员之间进行代码交叉审查、互相Code Review,对于代码变更的细节进行充分的评估和讨论。重大变更,会进行团队Review,花费数小时甚至数天讨论每一行代码变更细节。RFC 机制对于非Bug Fix、非性能提升、非重构,新特性或有可能改变底层行为的变更,我们会分为4个步骤进行。发起RFC的提案,https://github.com/swoole/rfc…,提案内容会详细阐述此项变更的前因后果、相关配置项、影响的范围、使用方法、示例。提案讨论,我们会对提案进行充分的讨论,刨根问底,分析优劣,推敲细节。所有问题均讨论清楚后,最终立项,开始实现。开发负责人创建git分支,编写单元测试脚本,编写代码,实现提案中的所有内容,最终发起Pull Request交叉评审,检查代码,提出改进意见,反馈给开发负责人,继续完善细节。最终合并到主干。整个过程均是在GitHub平台公开进行的,对Swoole项目感兴趣的PHPer均可参与。灰度测试为了保证正式版本的稳定性,我们在发布前会在内部项目上进行灰度测试,检验新版本的稳定性。另外我们与大部分Swoole框架作者建立了联系,新版本会先发给各大框架的作者提前试用。有重大底层变更、或不兼容项会提前与其他Swoole之上的开源项目作者进行沟通。重构2018年下半年我们对底层的代码进行了多次重构,在代码结构、可读性、复用性、封装度方面进行了很多优化。使得Swoole软件更为简洁、优雅。编程语言方面,我们现在逐渐使用C++替代C语言。C++提供的面向对象、智能指针、容器、模板等特性能够帮助我们进一步提升我们团队的开发效率。在此也欢迎各位PHPer参与Swoole项目,贡献代码。文档Swoole的文档也是广为开发者诟病的一个方面。在2018年我们团队在文档方面逐渐加大投入。重新编写梳理文档,加入丰富的例子程序,加入更详细的配图,修复细节问题,删除带有感情色彩的语句,更加客观中立严谨。总结在过去的几年,Swoole项目做的并不是很专业,存在较多BUG和难用的地方,也让很多使用者踩到了不少坑。最近半年成立全职研发团队后,我们在研发管理方面进步飞快,Swoole的稳定性、成熟度方面已今非昔比。稳定性始终是第一位的,我们在未来将会更加谨慎、严谨,保证质量。2019 未来新的一年我们主要有3个方向上发力。做减法删除非协程的特性,删除不必要的模块,减少历史包袱,提升稳定性、降低复杂度,减少不必要的选项,化繁为简,更简单。Swoole内核层面仍然会继续不断重构、精简,减少代码行数,清理冗余代码,尽可能地实现代码复用。深入项目在2018年底,我们已经开始逐渐与在生产环境上大量使用Swoole的企业建立联系,包括腾讯云、阅文、好未来、陌陌、优信等企业。了解实际应用场景、业务模式,进行深度交流合作,提供建议,帮助企业技术团队更好的解决业务问题,接受反馈改进底层。生态链2019年我们会基于Swoole4协程开发一些配套的工具和组件,弥补PHP在Cloud Native时代生态链方面的不足。

January 21, 2019 · 1 min · jiezi

Java 并发方案全面学习总结

并发与并行的概念并发(Concurrency): 问题域中的概念—— 程序需要被设计成能够处理多个同时(或者几乎同时)发生的事件并行(Parallelism): 方法域中的概念——通过将问题中的多个部分 并行执行,来加速解决问题。进程、线程与协程它们都是并行机制的解决方案。进程: 进程是什么呢?直白地讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。进程拥有代码和打开的文件资源、数据资源、独立的内存空间。启动一个进程非常消耗资源,一般一台机器最多启动数百个进程。线程: 线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。线程拥有自己的栈空间。在进程内启动线程也要消耗一定的资源,一般一个进程最多启动数千个线程。操作系统能够调度的最小单位就是线程了。协程: 协程又从属于线程,它不属于操作系统管辖,完全由程序控制,一个线程内可以启动数万甚至数百万协程。但也正是因为它由程序控制,它对编写代码的风格改变也最多。Java的并行执行实现JVM中的线程主线程: 独立生命周期的线程守护线程: 被主线程创建,随着创建线程结束而结束线程状态要注意的是,线程不是调用start之后马上进入运行中的状态,而是在"可运行"状态,由操作系统来决定调度哪个线程来运行。Jetty中的线程Web服务器都有自己管理的线程池, 比如轻量级的Jetty, 就有以下三种类型的线程:AcceptorSelectorWorker最原始的多线程——Thread类继承类 vs 实现接口继承Thread类实现Runnable接口实际使用中显然实现接口更好, 避免了单继承限制。Runnable vs CallableRunnable:实现run方法,无法抛出受检查的异常,运行时异常会中断主线程,但主线程无法捕获,所以子线程应该自己处理所有异常Callable:实现call方法,可以抛出受检查的异常,可以被主线程捕获,但主线程无法捕获运行时异常,也不会被打断。需要返回值的话,就用Callable接口一个实现了Callable接口的对象,需要被包装为RunnableFuture对象, 然后才能被新线程执行, 而RunnableFuture其实还是实现了Runnable接口。Future, Runnable 和FutureTask的关系如下:可以看出FutureTask其实是RunnableFuture接口的实现类,下面是使用Future的示例代码public class Callee implements Callable { AtomicInteger counter = new AtomicInteger(0); private Integer seq=null; public Callee() { super(); } public Callee(int seq) { this.seq = seq; } /** * call接口可以抛出受检查的异常 * @return * @throws InterruptedException / @Override public Person call() throws InterruptedException { Person p = new Person(“person”+ counter.incrementAndGet(), RandomUtil.random(0,150)); System.out.println(“In thread("+seq+”), create a Person: “+p.toString()); Thread.sleep(1000); return p; }}Callee callee1 = new Callee();FutureTask<Person> ft= new FutureTask<Person>(callee1);Thread thread = new Thread(ft);thread.start();try { thread.join();} catch (InterruptedException e) { e.printStackTrace(); return;}System.out.println(“ft.isDone: “+ft.isDone());Person result1;try { result1 = ((Future<Person>) ft).get();} catch (InterruptedException e) { e.printStackTrace(); result1 = null;} catch (ExecutionException e) { e.printStackTrace(); result1 = null;}Person result = result1;System.out.println(“main thread get result: “+result.toString());线程调度Thread.yield() 方法:调用这个方法,会让当前线程退回到可运行状态,而不是阻塞状态,这样就留给其他同级线程一些运行机会Thread.sleep(long millis):调用这个方法,真的会让当前线程进入阻塞状态,直到时间结束线程对象的join():这个方法让当前线程进入阻塞状态,直到要等待的线程结束。线程对象的interrupt():不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出异常,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!Object类中的wait():线程进入等待状态,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个状态跟加锁有关,所以是Object的方法。Object类中的notify():唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。同步与锁内存一致性错误public class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c–; } public int value() { return c; }}volatilepublic class Foo { private int x = -1; private volatile boolean v = false; public void setX(int x) { this.x = x; v = true; } public int getX() { if (v == true) { return x; } return 0; }}volatile关键字实际上指定了变量不使用寄存器, 并且对变量的访问不会乱序执行。但仅仅对原始类型变量本身生效,如果是++或者–这种“非原子”操作,则不能保证多线程操作的正确性了原子类型JDK提供了一系列对基本类型的封装,形成原子类型(Atomic Variables),特别适合用来做计数器import java.util.concurrent.atomic.AtomicInteger;class AtomicCounter { private AtomicInteger c = new AtomicInteger(0); public void increment() { c.incrementAndGet(); } public void decrement() { c.decrementAndGet(); } public int value() { return c.get(); }}原子操作的实现原理,在Java8之前和之后不同Java7public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; }}Java8public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1);}至于Compare-and-Swap,以及Fetch-and-Add两种算法,是依赖机器底层机制实现的。线程安全的集合类BlockingQueue: 定义了一个先进先出的数据结构,当你尝试往满队列中添加元素,或者从空队列中获取元素时,将会阻塞或者超时ConcurrentMap: 是 java.util.Map 的子接口,定义了一些有用的原子操作。移除或者替换键值对的操作只有当 key 存在时才能进行,而新增操作只有当 key 不存在时。使这些操作原子化,可以避免同步。ConcurrentMap 的标准实现是 ConcurrentHashMap,它是 HashMap 的并发模式。ConcurrentNavigableMap: 是 ConcurrentMap 的子接口,支持近似匹配。ConcurrentNavigableMap 的标准实现是 ConcurrentSkipListMap,它是 TreeMap 的并发模式。ThreadLocal-只有本线程才能访问的变量ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal< String >而言即为 String 类型变量),在不同的 Thread 中有不同的副本(实际是不同的实例,后文会详细阐述)。这里有几点需要注意因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。后文会通过实例详细阐述该观点。另外,该场景下,并非必须使用 ThreadLocal ,其它方式完全可以实现同样的效果,只是 ThreadLocal 使得实现更简洁。synchronized关键字方法加锁:其实不是加在指定的方法上,而是在指定的对象上,只不过在方法开始前会检查这个锁静态方法锁:加在类上,它和加在对象上的锁互补干扰代码区块锁:其实不是加在指定的代码块上,而是加在指定的对象上,只不过在代码块开始前会检查这个锁。一个对象只会有一个锁,所以代码块锁和实例方法锁是会互相影响的需要注意的是:无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问,每个对象只有一个锁(lock)与之相关联加锁不慎可能会造成死锁线程池(Java 5)用途真正的多线程使用,是从线程池开始的,Callable接口,基本上也是被线程池调用的。线程池全景图线程池的使用 ExecutorService pool = Executors.newFixedThreadPool(3); Callable<Person> worker1 = new Callee(); Future ft1 = pool.submit(worker1); Callable<Person> worker2 = new Callee(); Future ft2 = pool.submit(worker2); Callable<Person> worker3 = new Callee(); Future ft3 = pool.submit(worker3); System.out.println(“准备通知线程池shutdown…”); pool.shutdown(); System.out.println(“已通知线程池shutdown”); try { pool.awaitTermination(2L, TimeUnit.SECONDS); System.out.println(“线程池完全结束”); } catch (InterruptedException e) { e.printStackTrace(); }线程池要解决的问题任务排队:当前能并发执行的线程数总是有限的,但任务数可以很大线程调度:线程的创建是比较消耗资源的,需要一个池来维持活跃线程结果收集:每个任务完成以后,其结果需要统一采集线程池类型newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。newSingleThreadScheduledExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。线程池状态线程池在构造前(new操作)是初始状态,一旦构造完成线程池就进入了执行状态RUNNING。严格意义上讲线程池构造完成后并没有线程被立即启动,只有进行“预启动”或者接收到任务的时候才会启动线程。这个会后面线程池的原理会详细分析。但是线程池是出于运行状态,随时准备接受任务来执行。线程池运行中可以通过shutdown()和shutdownNow()来改变运行状态。shutdown()是一个平缓的关闭过程,线程池停止接受新的任务,同时等待已经提交的任务执行完毕,包括那些进入队列还没有开始的任务,这时候线程池处于SHUTDOWN状态;shutdownNow()是一个立即关闭过程,线程池停止接受新的任务,同时线程池取消所有执行的任务和已经进入队列但是还没有执行的任务,这时候线程池处于STOP状态。一旦shutdown()或者shutdownNow()执行完毕,线程池就进入TERMINATED状态,此时线程池就结束了。isTerminating()描述的是SHUTDOWN和STOP两种状态。isShutdown()描述的是非RUNNING状态,也就是SHUTDOWN/STOP/TERMINATED三种状态。任务拒绝策略Fork/Join模型(Java7)用途计算密集型的任务,最好很少有IO等待,也没有Sleep之类的,最好是本身就适合递归处理的算法分析在给定的线程数内,尽可能地最大化利用CPU资源,但又不会导致其他资源过载(比如内存),或者大量空线程等待。ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。所以当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程无法像任务队列中再添加一个任务并且在等待该任务完成之后再继续执行。而使用ForkJoinPool时,就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。以上程序的关键是fork()和join()方法。在ForkJoinPool使用的线程中,会使用一个内部队列来对需要执行的任务以及子任务进行操作来保证它们的执行顺序。那么使用ThreadPoolExecutor或者ForkJoinPool,会有什么性能的差异呢?首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。ps:ForkJoinPool在执行过程中,会创建大量的子任务,导致GC进行垃圾回收,这些是需要注意的。原理与使用ForkJoinPool首先是ExecutorService的实现类,因此是特殊的线程池。创建了ForkJoinPool实例之后,就可以调用ForkJoinPool的submit(ForkJoinTask<T> task) 或invoke(ForkJoinTask<T> task)方法来执行指定任务了。其中ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务,而RecusiveAction代表没有返回值的任务。个人认为ForkJoinPool设计不太好的地方在于,ForkJoinTask不是个接口,而是抽象类,实际使用时基本上不是继承RecursiveAction就是继承RecursiveTask,对业务类有限制。示例典型的一个例子,就是一串数组求和public interface Calculator { long sumUp(long[] numbers);}public class ForkJoinCalculator implements Calculator { private ForkJoinPool pool; private static class SumTask extends RecursiveTask<Long> { private long[] numbers; private int from; private int to; public SumTask(long[] numbers, int from, int to) { this.numbers = numbers; this.from = from; this.to = to; } @Override protected Long compute() { // 当需要计算的数字小于6时,直接计算结果 if (to - from < 6) { long total = 0; for (int i = from; i <= to; i++) { total += numbers[i]; } return total; // 否则,把任务一分为二,递归计算 } else { int middle = (from + to) / 2; SumTask taskLeft = new SumTask(numbers, from, middle); SumTask taskRight = new SumTask(numbers, middle+1, to); taskLeft.fork(); taskRight.fork(); return taskLeft.join() + taskRight.join(); } } } public ForkJoinCalculator() { // 也可以使用公用的 ForkJoinPool: // pool = ForkJoinPool.commonPool() pool = new ForkJoinPool(); } @Override public long sumUp(long[] numbers) { return pool.invoke(new SumTask(numbers, 0, numbers.length-1)); }}这个例子展示了当数组被拆分得足够小(<6)之后,就不需要并行处理了,而更大的数组就拆为两半,分别处理。Stream(Java 8)概念别搞混了,跟IO的Stream完全不是一回事,可以把它看做是集合处理的声明式语法,类似数据库操作语言SQL。当然也有跟IO类似的地方,就是Stream只能消费一次,不能重复使用。看个例子:int sum = widgets.stream().filter(w -> w.getColor() == RED) .mapToInt(w -> w.getWeight()) .sum();流提供了一个能力,任何一个流,只要获取一次并行流,后面的操作就都可以并行了。例如:Stream<String> stream = Stream.of(“a”, “b”, “c”,“d”,“e”,“f”,“g”);String str = stream.parallel().reduce((a, b) -> a + “,” + b).get();System.out.println(str);流操作生成流Collection.stream()Collection.parallelStream()Arrays.stream(T array) or Stream.of()java.io.BufferedReader.lines()java.util.stream.IntStream.range()java.nio.file.Files.walk()java.util.SpliteratorRandom.ints()BitSet.stream()Pattern.splitAsStream(java.lang.CharSequence)JarFile.stream()示例// 1. Individual valuesStream stream = Stream.of(“a”, “b”, “c”);// 2. ArraysString [] strArray = new String[] {“a”, “b”, “c”};stream = Stream.of(strArray);stream = Arrays.stream(strArray);// 3. CollectionsList<String> list = Arrays.asList(strArray);stream = list.stream();需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:IntStream、LongStream、DoubleStream。当然我们也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。Intermediate一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。已知的Intermediate操作包括:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered。Terminal一个流只能有一个 terminal操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。已知的Terminal操作包括:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iteratorreduce解析: reduce本质上是个聚合方法,它的作用是用流里面的元素生成一个结果,所以用来做累加,字符串拼接之类的都非常合适。它有三个参数初始值:最终结果的初始化值,可以是一个空的对象聚合函数:一个二元函数(有两个参数),第一个参数是上一次聚合的结果,第二个参数是某个元素多个部分结果的合并函数:如果流并发了,那么聚合操作会分为多段进行,这里显示了多段之间如何配合collect: collect比reduce更强大:reduce最终只能得到一个跟流里数据类型相同的值, 但collect的结果可以是任何对象。简单的collect也有三个参数:最终要返回的数据容器把元素并入返回值的方法多个部分结果的合并两个collect示例//和reduce相同的合并字符操作String concat = stringStream.collect(StringBuilder::new, StringBuilder::append,StringBuilder::append).toString();//等价于上面,这样看起来应该更加清晰String concat = stringStream.collect(() -> new StringBuilder(),(l, x) -> l.append(x), (r1, r2) -> r1.append(r2)).toString();//把stream转成mapStream stream = Stream.of(1, 2, 3, 4).filter(p -> p > 2);List result = stream.collect(() -> new ArrayList<>(), (list, item) -> list.add(item), (one, two) -> one.addAll(two));/ 或者使用方法引用 */result = stream.collect(ArrayList::new, List::add, List::addAll);协程协程,英文Coroutines,也叫纤程(Fiber)是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。协程实际上是在语言底层(或者框架)对需要等待的程序进行调度,从而充分利用CPU的方法, 其实这完全可以通过回调来实现, 但是深层回调的代码太变态了,所以发明了协程的写法。理论上多个协程不会真的"同时"执行,也就不会引起共享变量操作的不确定性,不需要加锁(待确认)。pythone协程示例Pythone, Golang和C#都内置了协程的语法,但Java没有,只能通过框架实现,常见的框架包括:Quasar,kilim和ea-async。Java ea-async 协程示例import static com.ea.async.Async.await;import static java.util.concurrent.CompletableFuture.completedFuture;public class Store{ //购物操作, 传一个商品id和一个价格 public CompletableFuture<Boolean> buyItem(String itemTypeId, int cost) { //银行扣款(长时间操作) if(!await(bank.decrement(cost))) { return completedFuture(false); } try { //商品出库(长时间操作) await(inventory.giveItem(itemTypeId)); return completedFuture(true); } catch (Exception ex) { await(bank.refund(cost)); throw new AppException(ex); } }}参考资料《七周七并发模型》电子书深入浅出Java Concurrency——线程池Java多线程学习(吐血超详细总结)Jetty基础之线程模型Jetty-server高性能,多线程特性的源码分析Java 编程要点之并发(Concurrency)详解Java Concurrency in Depth (Part 1)Java进阶(七)正确理解Thread Local的原理与适用场景Java 并发编程笔记:如何使用 ForkJoinPool 以及原理ForkJoinPool简介多线程 ForkJoinPoolJava 8 中的 Streams API 详解Java中的协程实现漫画:什么是协程学习源码 ...

January 4, 2019 · 4 min · jiezi

PHP 协程:Go + Chan + Defer

Swoole4提供了强大的PHP CSP协程编程模式。底层提供了3个关键词,可以方便地实现各类功能。本文基于Swoole-4.2.9和PHP-7.2.9版本关键词go :创建一个协程chan :创建一个通道defer :延迟任务,在协程退出时执行,先进后出这3个功能底层实现全部为内存操作,没有任何IO资源消耗。就像PHP的Array一样是非常廉价的。如果有需要就可以直接使用。这与socket和file操作不同,后者需要向操作系统申请端口和文件描述符,读写可能会产生阻塞的IO等待。协程并发使用go函数可以让一个函数并发地去执行。在编程过程中,如果某一段逻辑可以并发执行,就可以将它放置到go协程中执行。顺序执行function test1() { sleep(1); echo “b”;} function test2() { sleep(2); echo “c”;}test1();test2();执行结果:htf@LAPTOP-0K15EFQI:$ time php b1.phpbcreal 0m3.080suser 0m0.016ssys 0m0.063shtf@LAPTOP-0K15EFQI:$上述代码中,test1和test2会顺序执行,需要3秒才能执行完成。并发执行使用go创建协程,可以让test1和test2两个函数变成并发执行。Swoole\Runtime::enableCoroutine();go(function () { sleep(1); echo “b”;}); go(function () { sleep(2); echo “c”;});Swoole\Runtime::enableCoroutine()作用是将PHP提供的stream、sleep、pdo、mysqli、redis等功能从同步阻塞切换为协程的异步IO执行结果:bchtf@LAPTOP-0K15EFQI:$ time php co.phpbcreal 0m2.076suser 0m0.000ssys 0m0.078shtf@LAPTOP-0K15EFQI:$可以看到这里只用了2秒就执行完成了。顺序执行耗时等于所有任务执行耗时的总和 :t1+t2+t3…)并发执行耗时等于所有任务执行耗时的最大值 :max(t1, t2, t3, …)协程通信有了go关键词之后,并发编程就简单多了。与此同时又带来了新问题,如果有2个协程并发执行,另外一个协程,需要依赖这两个协程的执行结果,如果解决此问题呢?答案就是使用通道(Channel),在Swoole4协程中使用new chan就可以创建一个通道。通道可以理解为自带协程调度的队列。它有两个接口push和pop:push:向通道中写入内容,如果已满,它会进入等待状态,有空间时自动恢复pop:从通道中读取内容,如果为空,它会进入等待状态,有数据时自动恢复使用通道可以很方便地实现并发管理。$chan = new chan(2);# 协程1go (function () use ($chan) { $result = []; for ($i = 0; $i < 2; $i++) { $result += $chan->pop(); } var_dump($result);});# 协程2go(function () use ($chan) { $cli = new Swoole\Coroutine\Http\Client(‘www.qq.com’, 80); $cli->set([’timeout’ => 10]); $cli->setHeaders([ ‘Host’ => “www.qq.com”, “User-Agent” => ‘Chrome/49.0.2587.3’, ‘Accept’ => ’text/html,application/xhtml+xml,application/xml’, ‘Accept-Encoding’ => ‘gzip’, ]); $ret = $cli->get(’/’); // $cli->body 响应内容过大,这里用 Http 状态码作为测试 $chan->push([‘www.qq.com’ => $cli->statusCode]);});# 协程3go(function () use ($chan) { $cli = new Swoole\Coroutine\Http\Client(‘www.163.com’, 80); $cli->set([’timeout’ => 10]); $cli->setHeaders([ ‘Host’ => “www.163.com”, “User-Agent” => ‘Chrome/49.0.2587.3’, ‘Accept’ => ’text/html,application/xhtml+xml,application/xml’, ‘Accept-Encoding’ => ‘gzip’, ]); $ret = $cli->get(’/’); // $cli->body 响应内容过大,这里用 Http 状态码作为测试 $chan->push([‘www.163.com’ => $cli->statusCode]);});执行结果:htf@LAPTOP-0K15EFQI:/swoole-src/examples/5.0$ time php co2.phparray(2) { [“www.qq.com”]=> int(302) [“www.163.com”]=> int(200)}real 0m0.268suser 0m0.016ssys 0m0.109shtf@LAPTOP-0K15EFQI:/swoole-src/examples/5.0$这里使用go创建了3个协程,协程2和协程3分别请求qq.com和163.com主页。协程1需要拿到Http请求的结果。这里使用了chan来实现并发管理。协程1循环两次对通道进行pop,因为队列为空,它会进入等待状态协程2和协程3执行完成后,会push数据,协程1拿到了结果,继续向下执行延迟任务在协程编程中,可能需要在协程退出时自动实行一些任务,做清理工作。类似于PHP的register_shutdown_function,在Swoole4中可以使用defer实现。Swoole\Runtime::enableCoroutine();go(function () { echo “a”; defer(function () { echo “a”; }); echo “b”; defer(function () { echo “b”; }); sleep(1); echo “c”;});执行结果:htf@LAPTOP-0K15EFQI:/swoole-src/examples/5.0$ time php defer.phpabcbareal 0m1.068suser 0m0.016ssys 0m0.047shtf@LAPTOP-0K15EFQI:/swoole-src/examples/5.0$结语Swoole4提供的Go + Chan + Defer为PHP带来了一种全新的CSP并发编程模式。灵活使用Swoole4提供的各项特性,可以解决工作中各类复杂功能的设计和开发。 ...

December 4, 2018 · 1 min · jiezi

PHP协程:并发 shell_exec

在PHP程序中经常需要用shell_exec执行一些命令,而普通的shell_exec是阻塞的,如果命令执行时间过长,那可能会导致进程完全卡住。在Swoole4协程环境下可以用Co::exec并发地执行很多命令。本文基于Swoole-4.2.9和PHP-7.2.9版本协程示例<?php$c = 10;while($c–) { go(function () { //这里使用 sleep 5 来模拟一个很长的命令 co::exec(“sleep 5”); });}协程结果htf@htf-ThinkPad-T470p:/workspace/debug$ time php t.phpreal 0m5.089suser 0m0.067ssys 0m0.038shtf@htf-ThinkPad-T470p:/workspace/debug$只用了 5秒,程序就跑完了。下面换成 PHP 的 shell_exec 来试试。阻塞代码<?php$c = 10;while($c–) { //这里使用 sleep 5 来模拟一个很长的命令 shell_exec(“sleep 5”);}阻塞结果htf@htf-ThinkPad-T470p:/workspace/debug$ time php s.php real 0m50.119suser 0m0.066ssys 0m0.058shtf@htf-ThinkPad-T470p:/workspace/debug$ 可以看到阻塞版本花费了50秒才完成。Swoole4提供的协程,是并发编程的利器。在工作中很多地方都可以使用协程,实现并发程序,大大提升程序性能。

November 29, 2018 · 1 min · jiezi

PHP下的异步尝试二:初识协程

PHP下的异步尝试系列如果你还不太了解PHP下的生成器,你可以根据下面目录翻阅PHP下的异步尝试一:初识生成器PHP下的异步尝试二:初识协程PHP下的异步尝试三:协程的Co自动执行器 [待开发]PHP下的异步尝试四:PHP版的Promise [待开发]…多任务 (并行和并发)在讲协程之前,先谈谈多进程、多线程、并行和并发。对于单核处理器,多进程实现多任务的原理是让操作系统给一个任务每次分配一定的 CPU 时间片,然后中断、让下一个任务执行一定的时间片接着再中断并继续执行下一个,如此反复。由于切换执行任务的速度非常快,给外部用户的感受就是多个任务的执行是同时进行的。多进程的调度是由操作系统来实现的,进程自身不能控制自己何时被调度,也就是说: 进程的调度是由外层调度器抢占式实现的而协程要求当前正在运行的任务自动把控制权回传给调度器,这样就可以继续运行其他任务。这与抢占式的多任务正好相反, 抢占多任务的调度器可以强制中断正在运行的任务, 不管它自己有没有意愿。如果仅依靠程序自动交出控制的话,那么一些恶意程序将会很容易占用全部 CPU 时间而不与其他任务共享。协程的调度是由协程自身主动让出控制权到外层调度器实现的回到刚才生成器实现 xrange 函数的例子,整个执行过程的交替可以用下图来表示:协程可以理解为纯用户态的线程,通过协作而不是抢占来进行任务切换。相对于进程或者线程,协程所有的操作都可以在用户态而非操作系统内核态完成,创建和切换的消耗非常低。简单的说协程 就是提供一种方法来中断当前任务的执行,保存当前的局部变量,下次再过来又可以恢复当前局部变量继续执行。我们可以把大任务拆分成多个小任务轮流执行,如果有某个小任务在等待系统 IO,就跳过它,执行下一个小任务,这样往复调度,实现了 IO 操作和 CPU 计算的并行执行,总体上就提升了任务的执行效率,这也便是协程的意义多线程在单核下,多线程必定是并发的;不过现在的统一进程的多线程是可以运行在多核CPU下,所以可以是并行的并发(Concurrency)是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生。并行(Parallesim)是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行。多个操作可以在重叠的时间段内进行。并行和并发区别并发指的是程序的结构,并行指的是程序运行时的状态并行一定是并发的,并行是并发设计的一种单线程永远无法达到并行状态协程协程的支持是在生成器的基础上, 增加了可以回送数据给生成器的功能(调用者发送数据给被调用的生成器函数). 这就把生成器到调用者的单向通信转变为两者之间的双向通信.我们在上篇文章已经讲过了send方法, 下面让我们理解下协程同步代码在没有涉及到异步执行代码之前,我们的代码都是这样的function printNum($max, $caller){ for ($i=0; $i<$max; $i++ ) { echo “调度者:” . $caller . " 打印:" . $i . PHP_EOL; }}printNum(3, “caller1”);printNum(3, “caller2”);# output调度者:caller1 打印:0调度者:caller1 打印:1调度者:caller1 打印:2调度者:caller2 打印:0调度者:caller2 打印:1调度者:caller2 打印:2使用协程后改进的代码初稿,手动调整生成器执行# 本代码手动调整了进程执行代码的顺序,当然本代码实现不用协程也可以,只是利用本流程说明协程作用# 生成器给了我们函数中断,协程[生成器send]给了我们重新唤起生成器函数的能力function printNumWithGen($max){ for ($i=0; $i<$max; $i++ ) { $res = yield $i; echo $res; }}$gen1 = printNumWithGen(3);$gen2 = printNumWithGen(3);// 手动执行caller1 再 caller2$gen1->send(“调度者: caller1 打印:” . $gen1->current() . PHP_EOL);$gen2->send(“调度者: caller2 打印:” . $gen2->current() . PHP_EOL);// 手动执行caller1 再 caller2$gen1->send(“调度者: caller1 打印:” . $gen1->current() . PHP_EOL);$gen2->send(“调度者: caller2 打印:” . $gen2->current() . PHP_EOL);// 手动执行caller2 再 caller1$gen2->send(“调度者: caller2 打印:” . $gen2->current() . PHP_EOL);$gen1->send(“调度者: caller1 打印:” . $gen1->current() . PHP_EOL);# output调度者: caller1 打印:0调度者: caller2 打印:0调度者: caller1 打印:1调度者: caller2 打印:1调度者: caller2 打印:2调度者: caller1 打印:2总结上面案例应该让大家理解了协程设计的意义和如何使用协程那么接下去我们为我们的协程自动一个自动调度器(Co自动执行器),无需再手动来中断和恢复了 ...

September 20, 2018 · 1 min · jiezi

Swoole 4.1.0 正式版发布,支持原生 Redis/PDO/MySQLi 协程化

重大新特性支持 Redis/PDO/MySQLi从4.1.0版本开始支持了对PHP原生Redis、PDO、MySQLi协程化的支持。可使用SwooleRuntime::enableCorotuine()将普通的同步阻塞Redis、PDO、MySQLi操作变为协程调度的异步非阻塞IOSwooleRuntime::enableCoroutine();go(function () { $redis = new redis; $retval = $redis->connect(“127.0.0.1”, 6379); var_dump($retval, $redis->getLastError()); var_dump($redis->get(“key”)); var_dump($redis->set(“key”, “value”)); $redis->close();});协程跟踪新版本增加了两个方法用于跟踪协程运行。Coroutine::listCoroutines()可遍历当前所有协程Coroutine::getBackTrace($cid)可获取某个协程的函数调用栈function test1() { test2();}function test2() { while(true) { co::sleep(10); echo FUNCTION." n"; }}$cid = go(function () { test1();});go(function () use ($cid) { while(true) { echo “BackTrace[$cid]:n———————————————–n”; //返回数组,需要自行格式化输出 var_dump(co::getBackTrace($cid)).“n”; co::sleep(3); }});BackTrace[1]:———————————————–#0 SwooleCoroutine::sleep(10) called at [/home/htf/workspace/swoole/examples/coroutine/backtrace.php:8]#1 test2() called at [/home/htf/workspace/swoole/examples/coroutine/backtrace.php:3]#2 test1() called at [/home/htf/workspace/swoole/examples/coroutine/backtrace.php:14]其他修改重构 CoChannel C底层代码为C++, 解决复杂场景的非预期结果, 实现高稳定重构 CoHttpClient C底层代码为C++协程模式, 解决异步时序问题, 实现高稳定支持在协程和Server中使用exit, 此时将会抛出可捕获的SwooleExitException异常移除所有迭代器(table/connection/coroutine_list)的PCRE依赖限制增加open_websocket_close_frame配置, 可以在onMessage事件中接收close帧废弃HttpResponse->gzip()方法,改为使用http_compression配置项。底层会自动判断客户端传入的Accept-Encoding选择合适的压缩方法, 新增谷歌BR压缩支持增加CoHttpClient->addData()方法,可将内存中的数据作为上传文件内容进行发送Solaris系统支持Http2支持MAX_FRAME_SIZE分帧发送和MAX_HEADER_LIST_SIZE处理, 客户端增加isStreamExist方法检测是否存在对应流swoole_http_response->status增加reason参数修复MySQL prepare 中无符号参数使用了有符号值导致数值溢出的问题修复HTTP2的onRequest回调中没有协程的问题修复tasking_num某些特殊情况下变为-1的问题修复HTTP2-server的window-update帧构造错误修复所有PHP版本下的所有级别的编译warningGCC版本小于4.8时将会产生编译错误修复MySQL使用prepare时未使用参数绑定导致的内存分配不断增长修复HTTP2重连时旧stream内存丢失泄露底层开发相关统一文件命名 #970CoHttpClient使用了create_obj和free_obj保证内存安全, 防止错误的PHP代码引发内存问题 ...

September 1, 2018 · 1 min · jiezi