2024年7月

开心一刻

今天,我的又一个好哥们脱单了,只剩下我自己单身了

我向一个我喜欢的女生吐苦水

我:我这辈子是找不到女朋友了

她:怎么可能,你很优秀的,会有很多女孩子愿意当你女朋友的

我内心窃喜,问道:那你愿意当我女朋友吗

她:我都在开导你了,你不要恩将仇报!

不要恩将仇报

线上问题

生产环境突然告警,告警信息:

attempt to unlock lock, not locked by current thread by node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52

查看日志,找到对应的堆栈信息

Exception in thread "thread0" java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52
	at org.redisson.RedissonLock.lambda$unlockAsync$4(RedissonLock.java:616)
	at org.redisson.misc.RedissonPromise.lambda$onComplete$0(RedissonPromise.java:187)
	at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:578)
	at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:552)
	at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:491)
	at io.netty.util.concurrent.DefaultPromise.addListener(DefaultPromise.java:184)
	at org.redisson.misc.RedissonPromise.onComplete(RedissonPromise.java:181)
	at org.redisson.RedissonLock.unlockAsync(RedissonLock.java:607)
	at org.redisson.RedissonLock.unlock(RedissonLock.java:492)
	at com.qsl.ResissonTest.testLock(ResissonTest.java:41)
	at java.lang.Thread.run(Thread.java:748)

翻译过来就是

企图去释放锁,不被当前线程(node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52)锁住

也就是:当前线程企图去释放别的线程的锁

怎么能释放别人的锁?

指指点点

基础回顾

在排查问题之前,我们先弄清楚

node id: b9df1975-5595-42eb-beae-bdc5d67bce49 thread-id: 52

node id

thread-id
是什么

关于
thread-id
,我相信大家都理解,就是抛异常的线程的 id,没问题吧?那
node id
呢?

我用八股文引导下你们

问:
redisson
用的
redis
的什么数据类型来实现锁的

答:
hash

问:那
hash
中的
key

field

value
的值分别是什么

答:
key
的值是锁名,
field
的值是
线程id

value
的值是重入次数

问:如果多个服务同时去获取一把锁,
field
的值是不是有可能相同,比如服务A获取锁的线程的
thread-id
是 52,服务B获取锁的线程的的
thread-id
也是 52

此时你是不是有点慌了,但依旧嘴硬的回答:有可能相同

问:那没问题吗,A服务的线程(
thread-id=52
)拿到锁后,正在执行业务处理,B服务的线程(
thread-id=52
)也能拿到锁,这不是锁了个寂寞?

答:呃...嗯...

很显然漏了个细节,那就是
field
,其值不是
线程id
,而是
node id:thread-id
,例如:
b9df1975-5595-42eb-beae-bdc5d67bce49:52
,而这个
node id
就是
redisson

实例id
,用以区分分布式下的
redisson
实例

Redisson 分布式锁实现之源码篇 → 为什么推荐用 Redisson 客户端
有很详细的介绍,值得你们看看

释放别人的锁

talk is sheap show me the code

testLock

这代码,我相信大家都能看懂,但我还是说明下

  1. 构造锁
  2. 尝试获取锁,等待时间1s,持锁3s
  3. 如果获取到锁,则进行业务处理,没获取到锁,则打印
    锁获取失败
  4. finally
    保证异常和非异常情况下,锁都能释放

是不是很正常,但真的没
bug

640 (15)

我们调整下代码

testLock_异常

运行
multiThreadLock
,异常就来了

异常信息

从打印信息,我们应该能分析出问题出在哪

  1. 线程52获取到锁,执行业务中
  2. 线程53尝试获取锁,但锁被线程52持有
  3. 线程53 1s内获取锁失败
  4. 线程53 来到
    finally
    ,判断锁是否被持有,发现是被持有的,释放锁
  5. redisson
    释放锁的时候,发现锁的持有线程并非当前线程,抛出异常

线程53,你怎么回事,怎么能释放别人的锁?可不能怪线程53,代码可是我们写的,看看提交记录,非得把这个二臂揪出来!!!

哪个二臂写的

算了算了,还是别揪了,我们继续看如何修复

问题修复

既然找到问题了,修复问题就很简单了,方式有以下几种

提高等待时长

将获取锁的等待时长提高,但这种方式只能减少异常,并不是完全修复异常;因为会有多个线程同时竞争锁,等待时长设置成多少都不合适,除非设置成不超时,但是设置成不超时,可能会导致等待的线程太多,造成线程不够用的情况。不推荐该方式

自动释放

去掉
finally
,相当于把产生异常的源头给干掉了,那肯定就不会有异常了嘛,这不就是我们常提到的

解决不了问题,那就把提出问题的人解决掉

不主动释放锁,让锁自动到期释放,因为我们设置了锁持有时长是 3s,3s 后就自动到期释放了。但在实际业务中,我们往往会把锁持有时长设置的比较大(远大于业务执行的平均时长),保证业务不会并发执行,如果业务执行完了不主动释放锁,就会导致很长时间内锁被无效占用,后面的线程获取锁也只能白白等待。不推荐该方式

记录获取状态

直接看代码,你们就懂了

记录获取状态_1

如果业务执行时间超过 3s,会怎么样,我们把睡眠时间改成 5s,执行下
testLock
,你会发现同样的异常又出现了!!!

记录获取状态_异常

我们来分析下,锁持有时长是 3s,而业务执行时长是 5s,也就说业务还没执行完,锁已到期,
redis
自动释放了,业务执行完之后我们再去释放锁,锁都没了,怎么释放?所以
redisson
抛出异常了;所以释放锁的时候,还需要加一个条件

if (acquired && lock.isLocked())

acquired
表示当前线程是否获取到锁了,而
lock.isLocked()
表示是否有线程持有锁,如果都为
true
,那就说明是当前线程持有锁,释放就没问题了。可以用,但不推荐,因为有更优雅的处理方式

判断持有者

这种写法更优雅

判断持有者

就直接判断锁是不是当前线程持有,是就可以释放;就不用去管锁是别的线程持有,还是到期自动释放了。推荐该方式

总结

  1. 示例代码地址:
    redisson-spring-boot-demo
  2. 加锁的目的就是为了保证业务单线程执行,所以锁的持有时长一定要设置大一点,不然极端情况下,业务还在执行中,锁却到期了,就违背了加锁的初衷
  3. 锁一定要主动释放、一定要主动释放、一定要主动释放,与业务无关
  4. 释放锁的时候,要判断是否是当前线程持有,都不是你的锁,你凭什么释放

前言

在写
vue3编译原理揭秘
电子书的时候,发现有不少粉丝还傻傻分不清楚什么是编译时?什么是运行时?这篇文章我们来让你彻底搞清楚编译时和运行时的区别。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

编译时

我将
编译
这个词语理解为
翻译
,这句话是什么意思呢?

比如你要和一个老外沟通,你的英文超级烂。所以你说的是中文,老外却只理解英文。那你们两个人怎么沟通呢?所以你需要一个翻译器来将你说的中文转换为英文,这样老外就能理解你说的话了,
这个翻译过程就是我们常说的编译时

translate

看完上一个例子如果你还没理解的话,我们再来看一个
vue
的例子。我们平时写
vue
代码时一般都是写在文件后缀名为
.vue
文件中,也就是官方说的单文件组件 (SFC)。

但是浏览器认识后缀为
.vue
的单文件组件 (SFC)吗?

很明显浏览器是不认识的,所以这个时候需要将后缀为
.vue
的单文件组件 (SFC)编译(翻译)为浏览器认识的js文件,
这一过程就是我们常说的编译时

在前端中,一般来说
编译时
就是代码跑在
node.js
的阶段。

大家都知道前端主要分为两个环境:生产环境和开发环境。

  • 对于生产环境来说,
    编译时
    就是在执行类似
    yarn build
    之类的命令,将源代码打包成浏览器可直接执行的代码这一过程。打包生成的代码文件是存在磁盘中,这一过程是在
    node.js
    中完成的。

  • 对于开发环境来说,
    编译时
    就是在执行类型
    yarn dev
    这种启动命令,同样将源代码编译成浏览器可直接执行的代码这一过程。和生产环境不同的是生成的代码文件是存在内存中,并不会写到磁盘中,同样这一过程是在
    node.js
    中完成的。

运行时

还是以
vue
举例,大家都知道浏览器的渲染过程是将一个
html
文件渲染到页面上的。在SPA单页面中浏览器接收到的
index.html
一般是下面这样的,如下图:
html

从上图中可以看到接收到的html文件中只有一个
<div id="app"></div>
,那么浏览器又是怎么从这个空
div
渲染成丰富多彩的页面呢?

熟悉
vue
源码的同学应该比较清楚,首先是生成一个
app
对象,然后调用
app
对象的
mount
方法将经过编译时处理后拿到的vue组件对象挂载到
<div id="app"></div>
上面。
这一过程就是所谓的运行时。

对于前端来说,运行时就是代码执行在浏览器的阶段。

在浏览器中编译

看到这里有的小伙伴会有疑惑了,vue好像还提供了一种全局构建的版本。在这个版本中我们可以直接在
html
文件中使用
vue
,无需使用
webpack
或者
vite
这种打包工具打包。比如下面这样:
global

从上图中可以看到,这种用法中除了将
*.vue
文件名替换为
*.html
文件名之外,其他的写法基本是一模一样的。在这种用法中由于没有使用到构建工具
webpack
或者
vite
,当然就没有在
node.js
中执行的编译时,那么这种用法中浏览器又是如何识别单文件组件 (SFC)中的
<template>

<script>

<style>
等模块呢?

答案是在这种全局构建版本的
vue
中会内置一个编译器。在浏览器中运行时如果发现了
<template>

<script>

<style>
等模块的代码就会使用内置的编译器将这些模块编译成浏览器可执行的代码。

所以我们前面才会讲:一般来说
编译时
就是代码跑在
node.js
的阶段。不一般的情况就是现在这种情况,vue直接内置了一个编译器,在浏览器中进行编译。但是话说回来,这种在浏览器中编译的模式,性能肯定不如使用构建工具
webpack
或者
vite
提前将资源进行打包。

总结

一般情况下
编译时
就是代码跑在
node.js
的阶段,比如执行
yarn dev
或者
yarn build
时代码在
node.js
中执行的阶段。后面我们又讲了运行时实际就是代码在浏览器中执行的阶段。

最后我们又讲了还有一种特殊的情况,像全局构建版本的
vue
中会内置一个编译器。让我们可以脱离
webpack
或者
vite
使用
vue
,这种情况就是在浏览器中进行编译的模式,当然这种模式的性能肯定不如使用构建工具
webpack
或者
vite
提前将资源进行打包。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

本文介绍如何使用Known开发框架搭建进销存管理系统的项目结构,以及开发前的一些配置和基础代码。

1. 创建项目结构

使用cmd命令工具,进入
D:\Publics\JxcLite
目录,先安装Known框架项目模板,再用命令创建项目结构。两个命令如下:

dotnet new install KnownTemplate
dotnet new known --name=JxcLite

命令执行成功后,该文件夹下面自动生成JxcLite项目文件,项目文件结构如下:

├─JxcLite          -> 项目模块类库。
├─JxcLite.Client   -> 项目前端页面类库。
├─JxcLite.Web      -> 项目后端业务逻辑类库。
├─JxcLite.WinForm  -> 项目WinForm App。
├─JxcLite.sln      -> 项目解决方案文件。

双击打开
JxcLite.sln
文件,即可通过VS2022开发工具修改和添加系统项目文件。

2. 开发前准备

2.1. 配置数据库

本项目默认使用Known框架模板提供的
SQLite
数据库,如果要更改其他数据库,请参考官方文档
更换数据库

2.2. 配置系统名称

用VS2022打开项目后,双击
JxcLite
项目中的
AppConfig.cs
文件,修改系统名称。代码如下:

public static class AppConfig
{
    public static string AppName => "进销存管理系统";
}

2.3. 配置系统扩展按钮

双击
JxcLite
项目中的
Resources\actions.txt
资源文件,在此扩展Known框架提供的功能按钮信息,例如本项目除增删改查导按钮之外,还需要退货、确认、开票、回款按钮。

Return  | 退货 | rollback   | primary
Confirm | 确认 | check      | primary
Invoice | 开票 | profile    | primary
Payment | 回款 | pay-circle | primary

2.4. 配置多语言

如果项目需要多语言,双击打开
JxcLite
项目中的
AppConfig.cs
文件,在
AddJxcLite
方法中修改多语言设置,代码如下:

public static class AppConfig
{
    public static void AddJxcLite(this IServiceCollection services)
    {
        services.AddKnown(info =>
        {
            ...
            info.IsLanguage = true;
            ...
        });
    }
}


JxcLite
项目中的
Resources\Locales
文件夹下修改对应语言的资源文件,资源文件为
Dictionary
对象的JSON数据,内容如下所示:

//简体中文
{
  "Menu.BasicData": "基础数据",
  "GoodsInfo": "商品信息",
}
//English
{
  "Menu.BasicData": "Basic Data",
  "GoodsInfo": "Goods Info",
}

2.5. 系统常量

系统常量包含商业伙伴类型(PartnerType)和业务单据类型(BillType),代码如下:

public class PartnerType
{
    public const string Supplier = "供应商";
    public const string Customer = "客户";
}

public class BillType
{
    public const string Import = "进货";
    public const string ImportReturn = "进退货";
    public const string Export = "销货";
    public const string ExportReturn = "销退货";
}

3. 安装运行

配置完成后,直接运行项目。第一次运行,需要先安装程序,填写企业信息和管理员密码,安装时,自动生成框架数据库表和示例数据。安装成功后,直接跳转到登录页面,输入Admin和安装时设置的管理员密码登录进入系统。

4. 配置数据字典


基础数据-数据字典
模块添加如下数据字典。

类别代码 类别名称 字典内容
GoodsType 商品类别 测试
Unit 计量单位 个、千克
SettleMode 结算方式 现金、微信、支付宝、对账

5. 配置功能菜单

下面我们根据需求文档整理好功能模块数据,在
系统管理-模块管理
中逐一添加进去,各模块数据表格如下:

一级模块 二级模块 代码 图标 Url 描述
基础数据 BaseData database
商品信息 GoodsList ordered-list /bds/goods 查询和维护商品信息。
供应商管理 SupplierList usergroup-delete /bds/suppliers 查询和维护供应商信息。
客户管理 CustomerList usergroup-add /bds/customers 查询和维护客户信息。
进货管理 Import import
采购进货单 ImportList unordered-list /bms/ImportList 查询和维护采购进货单信息。
采购退货单 ImportReturn unordered-list /bms/ImportReturn 查询和维护采购退货单信息。
销货管理 Export export
销售出货单 ExportList unordered-list /bms/ExportList 查询和维护销售出货单信息。
销售退货单 ExportReturn unordered-list /bms/ExportReturn 查询和维护销售退货单信息。
库存管理 Inventory appstore
商品库存表 GoodsInventory table /wms/GoodsInventory 查询和维护商品库存表。
财务管理 Finance property-safety
客户对账单 CustomerAccount unordered-list /fms/CustomerAccount 查询和维护客户对账单信息。
供应商对账单 SupplierAccount unordered-list /fms/SupplierAccount 查询和维护供应商对账单信息。
统计报表 Report line-chart
进货明细表 ImportDetail table /rms/ImportDetail 查询进货明细表。
进退货明细表 ImportReturnDetail table /rms/ImportReturnDetail 查询进退货明细表。
销货明细表 ExportDetail table /rms/ExportDetail 查询销货明细表。
销退货明细表 ExportReturnDetail table /rms/ExportReturnDetail 查询销退货明细表。
商品利润表 GoodsProfit table /rms/GoodsProfit 查询商品利润表。

您是否曾经难以理解一个提交在做什么或者为什么要做?在审查或协作代码更改时,您是否希望有更多的清晰度和上下文?如果您的回答是肯定的,那么您会喜欢 GitHub Copilot 为您所做的——生成提交注释。GitHub Copilot 可以分析您的代码差异,并生成简洁的总结,突出重点。没有更多的猜测或浪费时间来解释您的 Git 历史记录。您可以轻松地专注于编码和协作。最好的部分是,您还可以通过更好的文档和代码更改的透明度来改善您的沟通和团队协作。

借助 GitHub Copilot 阐明您的提交历史

浏览 Git 历史记录可能会让人望而生畏,但它通常是了解代码库或帮助识别 bug 起因的最佳方式。我们在提交细节窗口中添加了一个 GitHub Copilot 驱动的解释功能,使每次提交的内容更容易理解。您将获得与代码并列的更改摘要,突出显示关键差异及其背后的基本逻辑辑依据。由于 GitHub Copilot 需要查看所有更改,对于大型更改集或拉取请求,这可能需要一些时间。

双击任何提交,打开 Git Repository 窗口中的 Commit Details。然后,选择提交消息上方的 Explain 按钮,以获得更改的摘要。专业提示:使用扩展选项和摘要视图,可以更好地查看描述中的代码更改。

请分享您的想法!

越来越多的人发现这个选项很有用,所以我们想在这里强调它并广泛分享。我们很乐意听到您的想法,关于如何使提交解释更好,以及任何其他使 GitHub Copilot 成为的超级版本控制器的想法,特别是 Git 历史。

我们感谢您花时间报告问题/建议,并希望您在使用 Visual Studio 时继续给我们反馈,告诉我们您喜欢什么以及我们可以改进什么。您的反馈对于帮助我们使 Visual Studio 成为最好的工具至关重要!您可以通过开发者社区与我们分享反馈,通过发送反馈来报告问题或分享您的建议,推动对新功能或现有功能的改进。

通过在 YouTube, Twitter, LinkedIn, Twitch 和 Microsoft Learn 上关注我们与 Visual Studio 团队保持联系。

原文链接:https://devblogs.microsoft.com/visualstudio/demystify-history-with-github-copilot-commit-explanations/

大家好,我是码农先森。

一次偶然看到了国外某机构针对 PHP 周边生态框架及扩展的性能测试排行榜,看到 Workerman 竟遥遥领先 Swoole。在我们 PHP 程序员现有的认知里,Swoole 作为一个基于 C/C++ 语言编写的扩展程序,性能居然落后了。第一眼看到这个结果的时候,我的心情久久不能平复,脑子里不经的浮现着「难道 C/C++ 比 PHP 的性能还差了?」。

说到 Workerman 和 Swoole,就想起了那不争气的 PHP-FPM。这么多年以来,但凡 PHP-FPM 在异步通信领域能有所建树,也就没有 Workerman 和 Swoole 什么事了。Workerman 在测试排行榜上能达到 Top1 想必有其过人之处,那我来说说具体的原因。说 Workerman 之前,先介绍一下目前 PHP-FPM 的现状。

PHP-FPM 是基于多进程模型的 PHP 进程管理器,每个进程在处理请求时都是单线程的,一次只能处理一个请求,无法充分利用多核 CPU 并发处理。并且进程模型还是 IO 同步阻塞的形式,遇到 IO 操作还得苦苦等待。PHP 作为一种解释性语言,每次请求都需要初始化环境、调用各个扩展模块的 MINIT、解析编译代码以及数据库资源的连接,在请求处理完毕后再释放资源、销毁所有定义的类、实例、符号表等,然后按顺序调用各个扩展模块的 RSHUTDOWN。最后将请求生成的结果返回给代理服务,比如 Nginx、Apache 等。PHP-FPM 的这种运行模式,频繁的创建和销毁资源,会导致高的内存使用和低的执行效率,在系统处于高并发、高负载的情况下将会带来致命的后果。

看完 PHP-FPM 的现状不时感叹 Workerman 真是相见恨晚啊,PHP 程序员已经苦 PHP-FPM 久矣。很多人都说 Workerman 高性能,且官方还宣称在 AB 压力测试下 QPS 还超过单独的 Nginx。但有多少人知道为什么高性能呢?它比 PHP-FPM 又好在哪呢?可能大家一时半会也说不清,这里我来做个解释,不过在解释之前我们要先了解一下 IO 多路复用技术。

多路复用

IO 多路复用是通过一种机制实现同时监控多个 IO 流的技术,它的核心思想是通过一个单一的系统调用来同时监控多个 IO 操作。具体来说,IO 多路复用允许一个进程同时监视多个文件描述符,比如 Socket 套接字,并且只在至少一个文件描述符就绪可读、可写或异常等事件情况下才进行真正的 IO 操作。IO 多路复用技术可以让程序在遇到类似 MySQL 读写、Redis 操作、网络请求、文件读取等 IO 操作时,不会阻塞整个进程的执行,达到 IO 操作非阻塞的效果。大家耳熟能详的 Redis、Nginx、Go 也都采用了这种模型。

如果大家对 IO 多路复用技术理解的云里雾里,建议在网上看看其他相关的资料。现在我们只要知道这个技术很「牛逼」就行了,但凡只要涉及到高性能的程序或软件必定会用到 IO 多路复用技术。没错 Workerman 正是将 IO 多路复用技术应用在自己的底层架构里,站在了巨人的肩膀上造就了 Workerman,这便是 Workerman 高性能的根本原因。其次还有一些影响因素,比如常驻进程模式、无需重复加载文件等资源到内存、全局变量只需初始化一次等。Workerman 加持了这些技术,则在性能上远远赶超了 PHP-FPM,但是回到我们刚开始时提到的在某机构性能测试上「Workerman 竟遥遥领先 Swoole」这又是什么原因呢?且听我娓娓道来!

Workerman 采用了 IO 多路复用技术,难道 Swoole 就不知道应用吗?既然 Swoole 官方也同样宣称自己是高性能异步通信框架,那必然也使用了 IO 多路复用技术。Swoole 不仅仅只是简单的使用该技术,而是将该技术在 Swoole 上体现的淋漓尽致贯穿始终,连 Swoole 中引以为傲的协程都是基于事件循环「EventLoop」机制实现的。既然采用了该技术按理来说 Swoole 的性能应该不会差啊!从两者的本质差异上来分析 Workerman 利用的是 pcntl、posix 扩展实现了进程管理的功能,实际上还是基于 PHP 实现。而 Swoole 是基于 C/C++ 语言实现的扩展程序,是以扩展模块的形式在 PHP 中体现,在进程管理方面也完全采用 C/C++ 语言实现。

原因分析

从某机构的测试结果上来看,Workerman 比 Swoole 性能更强的原因,我认为有以下几点。一是:从 Workerman 和 Swoole 实现架构的源代码上来看,Workerman 的架构更简洁代码量更少,反观 Swoole 的 C/C++ 代码量更大内部的处理逻辑更加复杂,Workerman 本质上利用的是 PHP 基本扩展 pcntl、posix 扩展,而 Swoole 本身就是自行实现的扩展模块,从实际的情况上来看往往基本扩展模块比第三方的扩展模块在资源管理方面更加稳定可靠。二是:在单进程模式下,Swoole 没有办法利用多核 CPU 资源,那么 Swoole 中的利器「协程」便发挥不出实际的作用,因此在这种情况下 Swoole 的性能会略逊色于 Workerman。三是:从两者所提供的功能上来看,Workerman 没有类似 Swoole 的并发管理、协程管理、通道管理、通道通信、进程间的通信等底层功能,这些繁冗的功能在程序的运行过程中也存在着一定的系统开销,当程序的复杂度提升,也会显而易见的影响到整个服务的性能和效率。

在某些特定条件的相较之下 Workerman 性能突出,但这也并不妨碍 Swoole 依旧是 PHP 异步通信领域的优秀扩展。Swoole 所提供的功能更丰富,比如可以手动使用协程让程序异步化、可以自行创建数据库连接池提高连接资源的复用、可以利用协程进程间的通信共享内存资源等等。简单的说就是各有千秋,我们在实际的技术选项过程中更应该结合当下的业务场景来做出正确的抉择。

结语

最后,再谈一点个人的看法,Workerman 更适合 PHP 初学者接触网络通信领域,没有那么多类似协程、进程、事件循环、异步阻塞非阻塞等难以理解的概念,直接拿来即用「上手快」。反观 Swoole 很多人都止步于了扩展的安装上,扩展安装环境部署都搞的个半死,更别谈使用了。Swoole 更适合长期在 Linux 环境下编程,并且对操作系统、网络编程、网络协议有一定基础的人。有些人打心底里就看不起 PHP 就自认为基于 C/C++ 语言的 Swoole 就更高级,要学就学最「牛逼」的,往往这种还没有学会走就想要跑的心态,结果都是摔得最惨的。透过这篇文章来看基于 PHP 本身实现的 Workerman 也不是很差嘛,所以大家量力而行,鞋子合不合适只有穿在自己脚上才知道,别把名牌的鞋子硬生生的套在自己脚上,最终的结果反而不尽如人意得不偿失。

感谢阅读,希望对大家能有所启发。


欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。