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。

⚠️ 硬绑定风险:

  1. 如果把N个游戏线程全部绑定同一个核心,会严重排队、卡顿
  2. 系统负载均衡被你破坏,其他软件抢不到核心;
  3. 换电脑,CPU核心数量不一样,写死掩码直接失效。

3、那个争论:Windows多线程本来就是抢占式调度,是不是多此一举?

普通IO等待类线程(等待窗口、读网络、休眠):完全没必要,多此一举。 这类线程大部分时间在休眠,CPU占用低,Windows原生调度足够好,强行绑定反而会降低整机负载均衡。

计算密集、循环高频轮询(你的游戏脚本:持续找图、OCR、键鼠循环):有收益 这类线程持续跑,频繁被操作系统在不同核心之间迁移。跨核迁移会清空L1/L2缓存,造成抖动、偶发卡顿。

  • SetThreadIdealProcessor:温和方案,给调度器一个偏好,尽量不迁移,不破坏系统负载均衡,推荐优先试这个
  • SetThreadAffinityMask:激进方案,锁死核心,适合大批量游戏多开,把N个脚本线程均匀分配到不同逻辑核;但必须动态探测CPU总数,不能写死编号

场景:剑网怀旧服多开脚本,大量循环、OCR、图像识别,适合用SetThreadIdealProcessor做软亲和,不要直接硬绑死

4、前代码的BUG

返回 (SetThreadIdealProcessor (线程句柄, 32))
  1. 硬写死32号逻辑核:普通4核/8核电脑根本不存在32号核心,调用直接无效;
  2. 没有动态探测本机逻辑CPU总数
  3. 批量多开线程,应该循环分配核心(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完全匹配,不存在文档命名混淆。

多开脚本场景选型建议

  1. 优先测试【软提示版本】线程_CPU优化_软提示
    • 只是告诉Windows调度器优先放这个核,不锁死;系统负载高时可以自动迁移,不会出现某个核心卡死、其他核心闲置。
    • 副作用最小,适合大量游戏脚本(OCR/找图循环)减少CPU缓存抖动。
  2. 硬绑定版本线程_CPU优化(线程_置CPU)
    • 强制只能跑在掩码对应的逻辑核心;
    • 坑:超线程CPU,不要连续分配0、1(同一个物理核心的两个逻辑线程),两个高负载脚本放这里性能会打架。
    • 步长=2,只分配不同物理核心的版本,跳过同物理核的超线程。

使用示例

.版本 2
.局部变量 线程句柄, 整数型
.如果真 (启动线程 (&脚本主循环, , 线程句柄))
    线程_CPU优化_软提示(线程句柄)
.如果真结束

注意:启动线程拿到线程句柄后立刻调用

测试代码,运行后打开任务管理器,验证线程是否被分配到预期的逻辑核心?

配套:取系统信息_逻辑CPU数量(),精易模块自带,读取本机有多少逻辑CPU,循环轮转分配核心,不会写死32。

6、选型建议(你的游戏多开场景)

  1. 优先 SetThreadIdealProcessor(软提示),启动线程后立刻调用;
  2. 不要一上来就用 SetThreadAffinityMask 硬绑定,容易出现单核心拥堵;
  3. 超线程CPU注意:同一个物理核心的两个逻辑核不要分配给高负载脚本,性能会打架;
  4. 测试手段:打开任务管理器→详细信息,右键选择“选择列”,勾选理想处理器,观察线程是否落在你期望的核心上。