SetThreadIdealProcessor:软提示(推荐优先跑在这个核,不是强制),有用,但约束力很弱;
配套的强绑定API是 SetThreadAffinityMask(硬亲和,强制只能在指定核心集合运行)。
说“多线程抢占式,多此一举”,普通业务场景确实多此一举;但游戏多开脚本场景,有收益,但要分清软/硬的区别
1、SetThreadIdealProcessor 详细说明
返回 (SetThreadIdealProcessor (线程句柄, 32))
- 含义:告诉Windows调度器:优先尽量把这个线程放在逻辑CPU #32上跑,只是建议,不是锁死
- 什么时候会被系统抢走:
- 32号核心被别的高优先级线程占满、繁忙;
- 系统负载高,调度器为了均衡负载,会直接把你的线程挪到别的核。
- 优点:
- 不会破坏系统全局负载均衡;
- 线程尽量固定在同一个核心,减少CPU缓存来回刷新(cache miss);
- 开销很小,启动线程后立刻调用没问题。
- 缺点:不保证一定在指定核心。你写死32,低配电脑根本没有32号逻辑核,API直接失效。
重点坑:CPU编号从0开始。
32= 第33个逻辑核心,不是第32核。
2、孪生API:SetThreadAffinityMask(硬绑定,很多人混淆)
这个就是大家常说的“绑定CPU核心”命令,硬约束:
线程只能在掩码中bit=1的那些逻辑CPU上运行,不允许跑到其他核心。
掩码规则:
- 1(二进制
0001)→ 只能跑在逻辑CPU0 - 2(
0010)→ CPU1 - 4(
0100)→ CPU2 - 8 → CPU3
- 1+2=3 → 允许在CPU0、CPU1两个核心跑
' 易语言DLL声明示例
.版本 2
.DLL命令 SetThreadAffinityMask, 整数型, "kernel32.dll", "SetThreadAffinityMask"
.参数 hThread, 整数型
.参数 dwThreadAffinityMask, 整数型
调用:SetThreadAffinityMask(线程句柄, 1 << 2) 绑定到逻辑核心2。
⚠️ 硬绑定风险:
- 如果把N个游戏线程全部绑定同一个核心,会严重排队、卡顿;
- 系统负载均衡被你破坏,其他软件抢不到核心;
- 换电脑,CPU核心数量不一样,写死掩码直接失效。
3、那个争论:Windows多线程本来就是抢占式调度,是不是多此一举?
✅ 普通IO等待类线程(等待窗口、读网络、休眠):完全没必要,多此一举。 这类线程大部分时间在休眠,CPU占用低,Windows原生调度足够好,强行绑定反而会降低整机负载均衡。
✅ 计算密集、循环高频轮询(你的游戏脚本:持续找图、OCR、键鼠循环):有收益 这类线程持续跑,频繁被操作系统在不同核心之间迁移。跨核迁移会清空L1/L2缓存,造成抖动、偶发卡顿。
SetThreadIdealProcessor:温和方案,给调度器一个偏好,尽量不迁移,不破坏系统负载均衡,推荐优先试这个;SetThreadAffinityMask:激进方案,锁死核心,适合大批量游戏多开,把N个脚本线程均匀分配到不同逻辑核;但必须动态探测CPU总数,不能写死编号。
场景:剑网怀旧服多开脚本,大量循环、OCR、图像识别,适合用SetThreadIdealProcessor做软亲和,不要直接硬绑死。
4、前代码的BUG
返回 (SetThreadIdealProcessor (线程句柄, 32))
- 硬写死32号逻辑核:普通4核/8核电脑根本不存在32号核心,调用直接无效;
- 没有动态探测本机逻辑CPU总数;
- 批量多开线程,应该循环分配核心(0,1,2,3,0,1,2,3…),全部指定32等于全部挤在同一个核。
参_CPU序号备注原文: CPU序号的或运算值:1 (0001) 代表只运行在CPU1,2 (0010) 代表只运行在CPU2,3 (0011) 代表可以运行在CPU1和CPU2,以此类推。
⚠️ 精易模块这里的命名坑:模块文档里叫CPU1,对应Windows底层API的逻辑核0
- 掩码
1→ 二进制第0位 → Windows逻辑核心0(模块叫CPU1) - 掩码
2→ 二进制第1位 → Windows逻辑核心1(模块叫CPU2) - 掩码
4→ 二进制第2位 → Windows逻辑核心2(模块叫CPU3)
也就是说:模块参数的掩码 = 2^(模块里的CPU编号 -1)
之前代码逻辑没问题,只是要理解文档描述的命名习惯。
5.修正后【线程_置CPU 硬绑定版本】适配精易模块V3.7.4)
.版本 2
.子程序 线程_CPU优化, 整数型, 公开, 自动切换指定句柄的线程到一个空闲的CPU上运行
.参数 线程句柄, 整数型, , 放到启动线程后面,紧跟启动线程,让产生的线程自动切换到空闲的CPU上运行
.局部变量 cpu信息, 类_CPU信息
.局部变量 逻辑核心总数, 整数型
.局部变量 staticIndex, 整数型, 静态
.局部变量 掩码, 整数型
逻辑核心总数 = cpu信息.取线程数 ()
.如果真 (逻辑核心总数 ≤ 0)
返回 (-1)
.如果真结束
staticIndex = staticIndex + 1
.如果真 (staticIndex ≥ 逻辑核心总数)
staticIndex = 0
.如果真结束
' staticIndex:0=Windows逻辑核0,对应模块掩码1
掩码 = 左移 (1, staticIndex)
返回 (线程_置CPU (线程句柄, 掩码))
举例验证
staticIndex=0 → 左移(1,0)=1 → 掩码=1,模块文档写:只运行在CPU1(底层逻辑核0)✅ staticIndex=1 → 左移(1,1)=2 → 掩码=2,模块文档写:只运行在CPU2(底层逻辑核1)✅
软提示版本 SetThreadIdealProcessor(推荐优先测试,不锁核)
.版本 2
.DLL命令 SetThreadIdealProcessor, 整数型, "kernel32.dll", "SetThreadIdealProcessor"
.参数 hThread, 整数型
.参数 dwIdealProcessor, 整数型
.子程序 线程_CPU优化_软提示, 整数型, 公开, 仅给系统调度器偏好提示,不强制锁定核心
.参数 线程句柄, 整数型
.局部变量 cpu信息, 类_CPU信息
.局部变量 逻辑核心总数, 整数型
.局部变量 staticIndex, 整数型, 静态
逻辑核心总数 = cpu信息.取线程数 ()
.如果真 (逻辑核心总数 ≤ 0)
返回 (-1)
.如果真结束
staticIndex = staticIndex + 1
.如果真 (staticIndex ≥ 逻辑核心总数)
staticIndex = 0
.如果真结束
返回 (SetThreadIdealProcessor (线程句柄, staticIndex))
SetThreadIdealProcessor 的第二个参数是Windows原生逻辑核编号,从0开始,和staticIndex完全匹配,不存在文档命名混淆。
多开脚本场景选型建议
- 优先测试【软提示版本】
线程_CPU优化_软提示- 只是告诉Windows调度器优先放这个核,不锁死;系统负载高时可以自动迁移,不会出现某个核心卡死、其他核心闲置。
- 副作用最小,适合大量游戏脚本(OCR/找图循环)减少CPU缓存抖动。
- 硬绑定版本
线程_CPU优化(线程_置CPU)- 强制只能跑在掩码对应的逻辑核心;
- 坑:超线程CPU,不要连续分配0、1(同一个物理核心的两个逻辑线程),两个高负载脚本放这里性能会打架。
- ,步长=2,只分配不同物理核心的版本,跳过同物理核的超线程。
使用示例
.版本 2
.局部变量 线程句柄, 整数型
.如果真 (启动线程 (&脚本主循环, , 线程句柄))
线程_CPU优化_软提示(线程句柄)
.如果真结束
注意:启动线程拿到线程句柄后立刻调用。
测试代码,运行后打开任务管理器,验证线程是否被分配到预期的逻辑核心?
配套:
取系统信息_逻辑CPU数量(),精易模块自带,读取本机有多少逻辑CPU,循环轮转分配核心,不会写死32。
6、选型建议(你的游戏多开场景)
- 优先 SetThreadIdealProcessor(软提示),启动线程后立刻调用;
- 不要一上来就用
SetThreadAffinityMask硬绑定,容易出现单核心拥堵; - 超线程CPU注意:同一个物理核心的两个逻辑核不要分配给高负载脚本,性能会打架;
- 测试手段:打开任务管理器→详细信息,右键选择“选择列”,勾选理想处理器,观察线程是否落在你期望的核心上。