当前位置: 首页 > article >正文

告别阻塞!用 PHP TrueAsync 实现 PHP 脚本提速 10 倍

proc_open与shell_exec等函数不同proc_open是创建进程的丰富工具。PHP 核心甚至为它引入了特殊的hack来正确处理管道。管道是进程间通信的最佳方式之一也是最便捷的方式。唯一更好的方案是共享内存加文件事件这仅仅是因为内存区域位于操作系统内核之外。为什么管道方便因为它们假装是文件。可以像普通文件一样用fread读取、fwrite写入。这对开发者来说非常有价值因为代码变得更通用、更可移植。通信协议管道是字节流它不知道消息的概念只是简单地在父子进程间传输字节。为了让代码正常工作需要一种方式来界定消息边界。最简单的格式是 NDJSON每条消息占一行行与行之间用\n分隔。{id:1,value:42} {id:2,value:17} {id:3,value:88}为什么这种格式很合适因为 PHP 对 JSON 支持很好而且 PHP 有fgets函数可以从流中读取行。$line fgets(STDIN); // 读取到 \n 为止 $task json_decode($line); // 解析 JSON下面编写普通同步代码打开进程并通过 NDJSON 协议与之通信function run_worker(string $script, array $tasks): void { $process proc_open( [PHP_BINARY, $script], [ 0 [pipe, r], // 子进程 STDIN 1 [pipe, w], // 子进程 STDOUT 2 STDERR, ], $pipes ); foreach ($tasks as $task) { // 发送任务 fwrite($pipes[0], json_encode($task) . \n); // 等待响应 ← PHP 在这里阻塞直到读取到一行 $line fgets($pipes[1]); $result json_decode(trim($line), true); echo Task #{$result[task_id]}: {$result[result]}\n; } fclose($pipes[0]); // 关闭 STDIN → worker.php 将退出 fclose($pipes[1]); proc_close($process); } run_worker(worker.php, [ [id 1, value 42], [id 2, value 17], [id 3, value 88], ]);现在把这段代码放到协程里不是放一个而是放 10 个。$tasks [ [id 1, value 42], [id 2, value 17], // ... 还有 28 个任务 ]; for ($i 0; $i 10; $i) { spawn(run_worker(...), worker.php, $tasks); }只写了一行异步代码spawn(run_worker(...))但实际上proc_open、fgets和fclose从一开始就变成了非阻塞 I/O 操作。现在十个协程实际上是 11 个因为有主代码竞争 CPU 时间。大部分代码仍保持顺序执行。Channels目前每个协程都收到相同的任务数组这显然不是期望的行为。例如可能想从 Twig 模板生成静态站点的 HTML给定文件列表后让每个文件由独立的 worker 处理或者可能需要解析日志。Workers 相互独立运行因为工作量可能不同而且不知道 worker 是否忙碌。为此编写任务分发层相当复杂也不理想。需要一个快速、便捷的解决方案无需过多思考。为此TrueAsync 提供了 Channel正是为这类任务设计的。Channel 是一个队列用于在协程间交换数据。Channel 中的每条消息可以是一个任务。协程从同一个 Channel 读取并执行任务。无需检查 worker 是否忙碌只需把任务发到 Channelworker 准备好时就会执行。$taskQueue new Async\Channel(10); for ($i 0; $i 10; $i) { spawn(run_worker(...), worker.php, $taskQueue); } foreach ($tasks as $task) { $channel-send($task); } $channel-close();当然这段代码没有错误处理或边界检查但它尽可能简单且易读。然而在底层一些非平凡的事情正在发生。假设有 100 个任务和 10 个进程的池。当所有 worker 都忙碌时会发生什么$channel-send($task)会挂起协程直到队列有空间。队列大小与池大小匹配并非巧合任务供给函数会暂停防止内存被尚无法处理的任务填满。TaskGroup已编写的代码离生产就绪还很远。不仅跳过了错误处理而且没有控制协程。它们悬在空中如果应用开始关闭或出现问题异常无法保证协程不会因逻辑错误而挂起。这很糟糕而且在生产环境中代码挂起却无从排查原因非常令人不快。为最小化意外错误的风险使用 TaskGroup 模式。它专门设计用于一起启动协程、一起等待它们、一起销毁它们。$taskQueue new Async\Channel(10); $group new Async\TaskGroup(); try { // 在组管理下启动 workers for ($i 0; $i 10; $i) { $group-spawn(run_worker(...), worker.php, $taskQueue); } // 向 Channel 投喂任务 foreach ($tasks as $task) { $taskQueue-send($task); } } finally { // 关闭任务 Channel —— 尝试读取的协程 // 会收到 ChannelClosedException 并优雅关闭 $taskQueue-close(); // 封印组防止意外添加新协程 $group-seal(); // 防止添加新协程 // 等待尚未完成的任务 $group-all()-await(); // 等待所有 worker 完成 }增强代码的容错性前一个版本的代码不适合真实场景。如果 worker 进程崩溃fwrite($pipes[0], json_encode($task) . \n)可能失败。而且代码还隐藏着一个隐患。function run_worker(string $script, Channel $taskQueue): void { return; } $taskQueue new Channel(POOL_SIZE); $group new TaskGroup(); try { for ($i 0; $i POOL_SIZE; $i) { $group-spawn(run_worker(...), __DIR__ . /worker.php, $taskQueue); } foreach (generate_tasks(TASK_COUNT) as $task) { $taskQueue-send($task); } } finally { $taskQueue-close(); $group-seal(); $group-all()-await(); echo \nDone.\n; }结果会是死锁错误输出类似这样 DEADLOCK REPORT START Coroutines waiting: 1, active_events: 0 Coroutine 4 spawned at main:0, suspended at main.php:96 (main) waiting for: - Channel(capacity3, receivers0, senders1) DEADLOCK REPORT END 死锁调试输出可通过async.debug_deadlockini 指令控制。详见 https://true-async.github.io/en/docs/reference/ini-settings.html原因是主协程试图向已满的 Channel 发送任务但没有人会去读取。为避免此错误需要在逻辑上将 TaskGroup 的生命周期与$taskQueue绑定。这可以通过TaskGroup::all()-await()等待所有活跃任务来实现。spawn(function () use ($taskQueue, $group) { try { $group-all()-await(); } finally { $taskQueue-close(); } });更简洁的实现方式foreach (generate_tasks(TASK_COUNT) as $task) { $taskQueue-send($task, $group-all()); }Channel::send方法的第二个参数是取消令牌允许在任意条件下取消操作。$group-all()返回一个 Future当所有任务完成时解析。不幸的是即使$taskQueue-send($task, $group-all())也不是 100% 正确因为它没有处理 TaskGroup 中所有协程已完成但新协程尚未启动的情况。Channel 类的语义——在 TrueAsync 0.6.0 中与 Go 非常相似——是最有问题的实现之一值得单独关注。现在改进进程池代码本身的错误处理。foreach ($taskQueue as $task) { $encoded json_encode($task); if ($encoded false) { echo [worker] json_encode failed for task #{$task[id]}: . json_last_error_msg() . \n; return; } if (fwrite($pipes[0], $encoded . \n)) { echo [worker] fwrite failed for task #{$task[id]}: pipe may be broken\n; return; } $line fgets($pipes[1]); if ($line false stream_get_meta_data($pipes[1])[timed_out]) { echo [worker] timeout waiting for response on task #{$task[id]}\n; return; } else if ($line false) { echo [worker] fgets failed for task #{$task[id]}: pipe closed or EOF\n; return; } $result json_decode(trim($line), true); if ($result null) { echo [worker] json_decode failed for task #{$task[id]}: . json_last_error_msg() . (raw: . trim($line) . )\n; return; } }在循环前添加超时设置stream_set_timeout($pipes[1], 5)——五秒——防止父进程失控。故意在 worker_fail.php 中破坏 worker 代码例如添加sleep(10)看看会发生什么php.exe E:\php\examples\workers_process_cli\main2.php [worker] fwrite failed for task #1: pipe may be broken [worker] fwrite failed for task #2: pipe may be broken [worker] fwrite failed for task #3: pipe may be broken Done.现在可以确信即使 worker 崩溃主进程也不会挂起而是继续运行。如果进程池能自动启动新进程以保持总数恒定那就更好了。怎么做可以复杂化协程代码添加显式重启进程的循环。把这个练习留给喜欢复杂代码和分形的爱好者这不是我们的选择因为很容易不小心破坏什么。协程内部的线性、简单和最小分支是它的主要优点。让我们保持这样。既然协程在概念上等同于进程那么在进程交互结束时同时结束协程是合理的。这意味着可以响应协程完成事件。TaskGroup 类有合适的race()方法在协程完成时触发。唯一的问题是 TaskGroup 是幂等的多次调用race()总是返回相同结果。所以需要不同的工具。TaskSet 类非常适合这个需求——它是 TaskGroup 的孪生兄弟非常适合 supervisor 逻辑。大多数方法相同区别在于join*方法组它不仅返回任务结果同时从 TaskSet 中移除它。正是需要的$taskQueue new Channel(POOL_SIZE); $group new TaskSet(); try { for ($i 0; $i POOL_SIZE; $i) { $group-spawn(run_worker(...), __DIR__ . /worker.php, $taskQueue); } foreach (generate_tasks(TASK_COUNT) as $task) { $taskQueue-send($task, $group-joinAll()); } } finally { $taskQueue-close(); $group-seal(); $group-joinAll()-await(); echo \nDone.\n; }现在添加 supervisor 逻辑在进程崩溃时重启$supervisor spawn(function () use ($group, $taskQueue): void { while (true) { try { // 等待至少一个任务完成 $group-joinNext()-await(); } catch (\Exception $exception) { // 发生了真正意想不到的事 echo [supervisor] Exception: {$exception-getMessage()}\n; $group-seal(); $taskQueue-close(); return; } // 如果组被封印或 Channel 已关闭退出循环 —— // 进程被有意停止不应重启 if($group-isSealed() || $taskQueue-isClosed()) { return; } $group-spawn(run_worker(...), __DIR__ . /worker.php, $taskQueue); } });用 worker_fail.php 运行代码观察到尽管 worker 崩溃进程池仍在运行php.exe E:\php\examples\workers_process_cli\main2.php [worker] fwrite failed for task #1: pipe may be broken [worker] fwrite failed for task #2: pipe may be broken条目数应该等于 TASK_COUNT尽管没有任务被成功执行。太好了。supervisor 代码对任务如何执行一无所知。执行任务的协程对 supervisor 一无所知。这不仅产生了相当可读的代码而且产生了低耦合的代码易于维护。稍微改进 supervisor让它对重启不要过于宽容如果有无限多的任务且 worker.php 有 bug可能进入无限重启循环。$supervisor spawn(function () use ($group, $taskQueue): void { $cooldown 5; $threshold 2; $restarts 0; $lastFail time(); while (true) { try { $group-joinNext()-await(); } catch (\Exception $e) { echo [supervisor] Exception: {$e-getMessage()}\n; $group-seal(); $taskQueue-close(); return; } if ($group-isSealed() || $taskQueue-isClosed()) { return; } $now time(); if (($now - $lastFail) $cooldown) { if ($restarts $threshold) { echo [supervisor] Too many worker failures, shutting down pool.\n; $group-seal(); $taskQueue-close(); return; } $restarts; } $lastFail $now; $group-spawn(run_worker(...), __DIR__ . /worker.php, $taskQueue); } });现在给 worker 添加类似真实任务的东西。假设有一个需要解析并计算统计信息的大日志文件。解析文件是重度的 CPU 密集型操作非常适合进程池。统计聚合应该在主进程中发生。为最小化内存使用拆分工作主进程只读取日志文件并干净地分成块workers 处理这些块并返回结果。function feed_chunks(string $filePath, int $chunkBytes, Channel $queue): int { $fileSize filesize($filePath); $fp fopen($filePath, r); $offset 0; $chunkId 0; try { while ($offset $fileSize) { $end min($offset $chunkBytes, $fileSize); // 对齐到下一行避免把行切半 if ($end $fileSize) { fseek($fp, $end); fgets($fp); // 跳过被切分行的剩余部分 $end ftell($fp); // 现在处于干净的行边界 } $queue-send([ id $chunkId, file $filePath, offset $offset, length $end - $offset, ]); $offset $end; } } finally { fclose($fp); } return $chunkId; }只向 worker 发送偏移量和块长度worker 自己打开文件并读取所需部分。这种方式让主进程快速把大任务拆分成部分无需通过管道泵送真实数据。添加另一个 Channel 用于结果在单独的协程中处理。虽然可以直接从 worker 协程修改数组来达到相同效果因为处于单个进程内所有变量对任何协程都可见。但使用 Channel 看起来更Go 风格。function run_worker(int $workerId, string $script, Channel $taskQueue, Channel $resultQueue): void { $process proc_open( [PHP_BINARY, $script], [ 0 [pipe, r], 1 [pipe, w], 2 STDERR, ], $pipes ); if ($process false) { throw new RuntimeException(Worker #$workerId: proc_open failed); } echo Worker #$workerId started\n; // 这个函数只在 TrueAsync 中有效 —— 在普通 PHP 中无法给管道设置超时 stream_set_timeout($pipes[1], 30); try { foreach ($taskQueue as $task) { // 只发送轻量级描述符 —— 日志数据不通过管道传输 $encoded json_encode($task); if (!fwrite($pipes[0], $encoded . \n)) { throw new RuntimeException(Worker #$workerId: pipe broken on chunk #{$task[id]}); } $line fgets($pipes[1]); if ($line false stream_get_meta_data($pipes[1])[timed_out]) { throw new RuntimeException(Worker #$workerId: timeout on chunk #{$task[id]}); } elseif ($line false) { throw new RuntimeException(Worker #$workerId: process died on chunk #{$task[id]}); } $result json_decode(trim($line), true); if ($result null) { throw new RuntimeException(Worker #$workerId: invalid JSON on chunk #{$task[id]}); } printf( [Worker #%d | pid %5d] chunk #%03d (%s KB) → %d lines parsed\n, $workerId, $result[pid], $task[id], number_format($task[length] / 1024), $result[parsed], ); $resultQueue-send($result); } } finally { fclose($pipes[0]); fclose($pipes[1]); proc_close($process); } }再添加一个协程在结果到达时显示进度function run_collector(Channel $resultQueue): array { $totals [ ip_counts [], status_counts [], path_counts [], method_counts [], total_bytes 0, bot_requests 0, ]; $totalParsed 0; $totalErrors 0; try { foreach ($resultQueue as $result) { $totalParsed $result[parsed]; $totalErrors $result[errors]; merge_stats($totals, $result[stats]); } } catch (Async\ChannelException) { } return [$totals, $totalParsed, $totalErrors]; }最后把所有东西连接起来$taskQueue new Channel(POOL_SIZE); $resultQueue new Channel(POOL_SIZE); $group new TaskSet(); $workerId 0; try { // 启动收集器协程 —— 合并到达的结果 $collector spawn(run_collector(...), $resultQueue); // 启动 worker 池 for ($i 1; $i POOL_SIZE; $i) { $group-spawn(run_worker(...), $workerId, __DIR__ . /log_worker.php, $taskQueue, $resultQueue); } echo Start\n; $startTime microtime(true); // 分割文件直接把块描述符投喂到 Channel。 // send() 在所有 worker 忙碌时阻塞 → 自然的背压。 // joinAll() 作为取消令牌如果所有 worker 死亡停止投喂。 $totalChunks feed_chunks($logFile, CHUNK_BYTES, $taskQueue, $group); $taskQueue-close(); printf(\n Split into %d chunks of ~%d MB each\n, $totalChunks, CHUNK_BYTES / 1024 / 1024); try { $group-joinAll()-await(); } catch (\Exception $e) { echo Exception: {$e-getMessage()}\n; } } finally { $taskQueue-close(); $supervisor-cancel(); $group-seal(); $group-joinAll()-await(); $resultQueue-close(); // 从收集器获取合并后的结果 [$stats, $totalParsed, $totalErrors] \Async\await($collector); $elapsed microtime(true) - $startTime; print_report($stats, $totalParsed, $totalErrors, $elapsed); }

相关文章:

告别阻塞!用 PHP TrueAsync 实现 PHP 脚本提速 10 倍

proc_open 与 shell_exec 等函数不同,proc_open 是创建进程的丰富工具。PHP 核心甚至为它引入了特殊的"hack"来正确处理管道。管道是进程间通信的最佳方式之一,也是最便捷的方式。唯一更好的方案是共享内存加文件事件,这仅仅是因为…...

-python-langchain框架(3-6-pdf文件分页加载 )

一、PDF分页加载的核心应用场景在实际开发中,分页加载并非多余操作,而是针对特定场景的最优解,尤其适合以下几种情况:大型PDF文件处理:单文件几十页、上百页,甚至更大,一次性加载全部内容会占用…...

Node.js——dns模块

dns模块1、resolve方法将域名解析为DNS记录2、lookup方法查询IP地址3、reverse方法反向解析IP地址4、dns模块中的各种错误代码在网络编程中,开发者更倾向于使用域名,而不是IP地址来指定网络连接的目标地址。在Node.js中,提供dns模块&#xff…...

从源码到实践:拆解PX4飞控如何处理Mavros的GPS/ENU坐标指令(附精准转换代码)

从源码到实践:拆解PX4飞控如何处理Mavros的GPS/ENU坐标指令(附精准转换代码) 当你在户外操控四旋翼无人机时,是否曾疑惑过GPS坐标如何精确转换为飞控能理解的ENU坐标系?PX4飞控内部究竟如何处理这些位置指令&#xff1…...

告别subfloat!LaTeX中minipage+subfigure排版多图的最佳实践

LaTeX多图排版进阶指南:minipage与subfigure的黄金组合 在学术论文和技术文档写作中,图片排版往往是让人头疼的问题。特别是当需要处理多张图片并为其添加子标题时,传统的subfloat方法常常会遇到标题溢出、无法自动换行等令人沮丧的情况。本文…...

解锁论文新境界:书匠策AI——你的毕业论文超级助手

在学术的征途中,毕业论文无疑是每位学子必须跨越的一道重要门槛。它不仅是对你四年学习成果的全面检验,更是你学术生涯的一次重要启航。然而,面对繁琐的选题、海量的文献、复杂的结构搭建以及无尽的文字雕琢,许多学子常常感到力不…...

商道融绿ESG评级实战指南:从数据获取到企业绿色转型效果验证

商道融绿ESG评级实战指南:从数据获取到企业绿色转型效果验证 当某制造业上市公司ESG负责人张总监第一次向董事会汇报绿色转型方案时,遭遇的质疑声至今记忆犹新:"这些环保投入真能带来实际效益吗?"直到他们运用商道融绿E…...

Scratch 3.0二次开发实战:从零构建自定义插件

1. 为什么需要自定义Scratch插件? Scratch作为全球最受欢迎的少儿编程工具,其模块化积木设计让编程学习变得直观有趣。但你可能遇到过这种情况:想做一个天气预报项目,却发现内置积木无法获取实时天气数据;或者想开发一…...

游戏开发中的乒乓缓存实战:Unity双缓冲技术如何提升渲染性能

游戏开发中的乒乓缓存实战:Unity双缓冲技术如何提升渲染性能 在Unity游戏开发中,渲染性能优化一直是开发者关注的焦点。当画面复杂度和特效层级不断提升时,传统的单缓冲机制往往难以满足流畅渲染的需求,这时乒乓缓存(P…...

10G DWDM/OTN系统DCM色散补偿

一、色散补偿的基本原则优先欠补偿,整体必需欠补偿。整体尽量均匀补偿。二、色散常识是线性的,可预测的,可逆的。这是色散能够补偿的根本原因,无论是传统的DCF方式还是100G的算法补偿。正如彩虹现象,白光经过色散作用&…...

5分钟搞懂阻抗匹配:从L型网络到Smith圆图实战指南(附ADS仿真步骤)

5分钟搞懂阻抗匹配:从L型网络到Smith圆图实战指南(附ADS仿真步骤) 引言:为什么阻抗匹配如此重要? 想象一下,你正在调试一块射频电路板,信号强度总是达不到预期。用频谱仪观察波形时,…...

-python-langchain框架(3-3-常用的几种文本分割 )

一、文本分割的核心逻辑与关键参数无论哪种分割方法,核心都是通过参数控制文本块的大小和关联性,先理清这几个核心参数,后续方法理解会事半功倍:chunk_size:单个文本块的最大长度(字符 / Token 数&#xff…...

LLM之Agent(三十七)|AI Agents(六):从Workflow到Agent的架构演进与选型指南

1. 从Workflow到Agent的技术演进路径 十年前我刚接触自动化系统时,所有流程都是硬编码的if-else逻辑。如今AI技术的爆发式发展,让系统架构经历了三次重要迭代: 第一代规则引擎就像铁路轨道,每个岔路口都需要人工预设判断条件。我参…...

基于Vivado工程的FPGA多通道以太网实时同步采集系统——AD7606八通道同步采集与UD...

基于FPGA多通道以太网实时同步采集系统 FPGA: Vivado工程 AD芯片:AD7606 传输协议:UDP 上位机开发软件:QT5.12 AD7606八通道同步采集,最高采样率200KHz,上位机发送指令开始采集,上位机通过千兆以太网udp进行数据传输 QT上位机软件:udp上位…...

传统永磁同步电机的FOC离散化simulink模型,效果较好 附赠传递函数离散化推导的文档

传统永磁同步电机的FOC离散化simulink模型,效果较好 附赠传递函数离散化推导的文档,初学者可以入手直接上干货。今天咱们聊聊永磁同步电机FOC控制在Simulink里的离散化实现,重点说说怎么让仿真模型更贴近实际DSP的运行环境。别被那些花里胡哨…...

python zipfile

# Python 的 zipfile:不只是打包文件那么简单 如果你用过压缩软件,大概知道 ZIP 格式是用来把多个文件打包成一个,顺便还能压缩节省空间。Python 里的 zipfile 模块就是干这个的,但它的能力远不止“打包”这么简单。 它到底是什么…...

python codecs

# 聊聊Python里的codecs模块 平时写Python处理文本文件,最常打交道的可能就是open()函数了。但不知道你有没有遇到过这种情况:打开一个文件,明明看着是中文,读出来却是一堆乱码。或者从某个老系统导出的数据,用普通方式…...

python binascii

## 关于Base64,你可能需要知道这些 在编程的世界里,数据并不总是以我们熟悉的形式存在。有时候,一段文字、一张图片,或者任何其他类型的数据,需要被转换成另一种形式才能在不同的系统中安全传输或存储。Base64就是这样…...

claw-code 源码详细分析:Parity Audit——如何用工程对比把「像不像」从口水战变成可重复报告?

涉及源码:src/parity_audit.py、src/reference_data/archive_surface_snapshot.json、commands_snapshot.json、tools_snapshot.json,src/main.py,tests/test_porting_workspace.py。1. 要解决什么问题 大型移植里,「我们已经很像…...

GetQzonehistory:如何一键完整导出QQ空间所有说说的终极指南

GetQzonehistory:如何一键完整导出QQ空间所有说说的终极指南 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 还记得那些年在QQ空间写下的青春记忆吗?那些深夜的心…...

5分钟免费指南:如何将旧手机变成Linux高清摄像头

5分钟免费指南:如何将旧手机变成Linux高清摄像头 【免费下载链接】droidcam GNU/Linux/nix client for DroidCam 项目地址: https://gitcode.com/gh_mirrors/dr/droidcam 想让闲置的旧手机发挥新价值吗?DroidCam正是你需要的开源解决方案&#xf…...

5大核心功能深度解析:AltDrag如何重新定义Windows窗口管理效率

5大核心功能深度解析:AltDrag如何重新定义Windows窗口管理效率 【免费下载链接】altdrag :file_folder: Easily drag windows when pressing the alt key. (Windows) 项目地址: https://gitcode.com/gh_mirrors/al/altdrag 在Windows系统中,窗口管…...

RePKG:5个高效技巧助你掌握Wallpaper Engine资源处理与格式转换

RePKG:5个高效技巧助你掌握Wallpaper Engine资源处理与格式转换 【免费下载链接】repkg Wallpaper engine PKG extractor/TEX to image converter 项目地址: https://gitcode.com/gh_mirrors/re/repkg RePKG是一款专注于Wallpaper Engine资源处理的开源工具&…...

环模式饲料制粒机设计【农业机械】【论文+14张CAD图纸+proe三维+答辩稿】

环模式饲料制粒机作为农业机械领域的关键设备,其核心作用在于将粉状饲料原料通过物理挤压转化为颗粒状成品。这一过程不仅提升了饲料的适口性与营养保留率,更通过颗粒的规则形态减少了运输与储存过程中的粉尘损失,为规模化养殖提供了稳定高效…...

5分钟搞定抖音音频提取:免费高效的douyin-downloader终极指南

5分钟搞定抖音音频提取:免费高效的douyin-downloader终极指南 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallbac…...

PowerToys Image Resizer:三步解决全场景图片批量处理难题

PowerToys Image Resizer:三步解决全场景图片批量处理难题 【免费下载链接】PowerToys Microsoft PowerToys is a collection of utilities that supercharge productivity and customization on Windows 项目地址: https://gitcode.com/GitHub_Trending/po/Power…...

Autoware Docker部署实战:从环境配置到镜像拉取全解析

1. Autoware与Docker环境部署概述 自动驾驶开发环境搭建一直是让开发者头疼的问题,尤其是像Autoware这样的大型开源框架。传统本地安装方式需要处理大量依赖关系,稍有不慎就会陷入"依赖地狱"。而Docker容器化部署正好能解决这个痛点——它把整…...

如何搭建终极游戏串流平台:Sunshine免费开源方案完整指南

如何搭建终极游戏串流平台:Sunshine免费开源方案完整指南 【免费下载链接】Sunshine Self-hosted game stream host for Moonlight. 项目地址: https://gitcode.com/GitHub_Trending/su/Sunshine 想要在任何设备上畅玩PC游戏大作?Sunshine开源游戏…...

告别B站资源无法保存的烦恼:BiliTools跨平台工具箱完整使用指南

告别B站资源无法保存的烦恼:BiliTools跨平台工具箱完整使用指南 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliToo…...

MaaYuan自动化辅助工具高效配置避坑指南:零基础入门三步完成环境部署

MaaYuan自动化辅助工具高效配置避坑指南:零基础入门三步完成环境部署 【免费下载链接】MaaYuan 代号鸢 / 如鸢 一键长草小助手 项目地址: https://gitcode.com/gh_mirrors/ma/MaaYuan MaaYuan作为一款基于MaaFramework的自动化辅助工具,专为游戏日…...