SAP ABAP 多线程,还在用老掉牙的 NEW TASK RFC?看完你会删掉那堆 WAIT UNTIL
副标题:第一次学并行,先让 10 个快递同时盖章,再套到批量 BAPI。
读者:会写内表、LOOP、CALL FUNCTION,还在用CALL FUNCTION ... STARTING NEW TASK自己数任务、自己WAIT UNTIL。
读完你能:丢掉那堆 RFC 样板,用CL_ABAP_PARALLEL拆包并行,并看懂物料扩展程序ZMMU002S为什么那样写。
建议先只看第 1~6 节。看懂「10 个快递同时签收」再往后翻。第 7 节以后是「套到真实程序」和附录。
1. 先用大白话说清楚
1.1 你现在的程序多半是这样
LOOP AT 内表. 调 BAPI 处理这一行. " 大约 1 秒 ENDLOOP.1 万行就是大约 1 万秒。人在屏幕前干等。
1.2 「并行」想做的事
不要一条接一条。把 1 万行切成 10 包,每包 1000 行,让系统里空闲的对话窗口进程各拿一包同时干。
打个比方:
| 生活 | 程序 |
|---|---|
| 你自己做 1000 份盒饭 | 一个 LOOP 串行跑 BAPI |
| 请 5 个厨师,每人 200 份 | 5 个对话进程各处理一包 |
| 你是领班:分完盘子,站着等全做完 | CL_ABAP_PARALLEL负责分发和等待 |
ABAP 里说的「多线程」,不是Windows 那种线程。它是:
再开几个对话工作进程(DIA),每个进程跑你的同一段逻辑,只是数据不同。
对话工作进程可以先记成:系统里那种平时给你跑前台程序的工人。闲着的时候,可以借来干活。
1.3 什么时候值得并行
同时满足这三条,再考虑:
- 活很重:一条就要零点几秒到几秒(BAPI、远程接口)。
- 行和行基本不相干:A 物料扩工厂,不需要等 B 物料先成功。
- 一次数量大:几百、几千、几万行。
下面这些不要并行:
- 循环里只是给字段赋值、改个字符串(太快,分发的时间比干活还长)
- 第 2 行必须用第 1 行刚写进数据库的结果
- 必须弹 GUI、读 Excel、
GUI_UPLOAD(并行工人没有 SAP 窗口)
2. SAP 已经写好了「领班」
以前要自己写:RFC 函数、STARTING NEW TASK、自己数开了几个、自己WAIT、自己收结果。
现在用一个标准类就行:
CL_ABAP_PARALLEL
你只需要做两件事:
- 写一个小类:说明「一包数据怎么处理」。这个类要实现接口
IF_ABAP_PARALLEL,里面只有一个方法,名字叫DO。 - 在主程序里:把数据切成几包,每包
NEW一个小类,交给RUN_INST,然后等。
三个名字先混个脸熟(后面例子会反复出现):
| 名字 | 把它当成 | 干什么 |
|---|---|---|
CL_ABAP_PARALLEL | 领班 | 决定请几个厨师、发盘子、等收工 |
IF_ABAP_PARALLEL | 厨师手册封面 | 规定厨师必须会做的那一道工序:DO |
你写的本地类,比如LCL_TASK | 一个厨师 + 他手上那一笼菜 | 属性里放这一包数据,DO里炒这一包 |
DO没有IMPORTING/EXPORTING。
这一包的输入、输出,全部放在对象的属性(DATA)里。领班会把整个对象「打包寄」到另一个进程,那边拆开再调用DO。
「打包寄」的术语叫序列化。第一次听只记一句:
对象必须能被复印成一串字节寄走。所以属性里只能放数字、字符串、内表、结构,不能放 GUI 控件、Excel OLE 这种带窗口的东西。
接口源码一共就这么几行,没有隐藏魔法:
INTERFACE if_abap_parallel PUBLIC. INTERFACES if_serializable_object. " 声明:我可以被打包寄走 METHODS do. " 到了那边,就调这个方法 ENDINTERFACE.3. 先抄一个能跑的最小例子(请务必跑一遍)
先不要想物料、BAPI。
想象网点柜台上堆了10 个快递:
- 你一个人盖章:盖完 1 号再盖 2 号,一个 2 秒,一共 20 秒。
- 请几个仓管同时盖:10 个件差不多同一时间盖完,大约 2 秒出头。
程序要做的就是这件事:每个快递一个任务,任务里假装忙 2 秒,然后写上「已签收」。
在测试系统新建可执行程序,整段粘贴、激活、F8。跑的时候旁边开事务SM50,能看到好几个人同时在盖章。
REPORT z_para_hello. "------------------------------------------------- " 1)一个对象 = 一个快递 " 单号是输入,签收结果是输出 "------------------------------------------------- CLASS lcl_express DEFINITION. PUBLIC SECTION. INTERFACES if_abap_parallel. METHODS constructor IMPORTING iv_bill TYPE i. " 创建时把单号塞进来 DATA mv_bill TYPE i. " 这件货的单号,比如 1、2、3 DATA mv_msg TYPE string. " 盖完章后写的话,比如「单号3已签收」 ENDCLASS. CLASS lcl_express IMPLEMENTATION. METHOD constructor. mv_bill = iv_bill. " 先记下来,先不干活 ENDMETHOD. METHOD if_abap_parallel~do. " 这句话运行在「另一个工人」身上,不是你当前这个窗口 WAIT UP TO 2 SECONDS. " 假装扫码、盖章,故意慢一点方便你去 SM50 看 mv_msg = |单号{ mv_bill }已签收|. ENDMETHOD. ENDCLASS. "------------------------------------------------- " 2)你当调度:把 10 个件分出去,然后等全盖完 "------------------------------------------------- START-OF-SELECTION. DATA lt_in TYPE cl_abap_parallel=>t_in_inst_tab. " 待分出去的快递清单 DATA lt_out TYPE cl_abap_parallel=>t_out_inst_tab. " 盖完拿回来的回执 DATA lo_one TYPE REF TO lcl_express. " 柜台上放 10 个件,单号 1~10。这时还没人盖章 DO 10 TIMES. APPEND NEW lcl_express( sy-index ) TO lt_in. ENDDO. " 开始分件。程序会停在这一行,直到 10 个都盖完(或有人失手) NEW cl_abap_parallel( p_percentage = 50 )->run_inst( EXPORTING p_in_tab = lt_in IMPORTING p_out_tab = lt_out ). " 把回执打在屏幕上 LOOP AT lt_out INTO DATA(ls_out). IF ls_out-message IS NOT INITIAL OR ls_out-inst IS NOT BOUND. WRITE: / '第', ls_out-index, '个件没盖成:', ls_out-message. CONTINUE. ENDIF. lo_one ?= ls_out-inst. " 回执上只写「一个工人」,转回我们的快递对象才能看单号 WRITE: / lo_one->mv_msg, ' 花了', ls_out-time, '秒'. ENDLOOP.正常的话,大约 2~4 秒整屏刷完(不是 20 秒),类似:
单号1已签收 花了 2 秒 单号2已签收 花了 2 秒 ... 单号10已签收 花了 2 秒如果还是要等大约 20 秒,说明几乎没请到帮手,去 SM50 看是不是只有一条对话进程。
3.1 对着代码看,就三件事
创建的时候不盖章,盖章发生在DO里
NEW lcl_express( 3 ) → 只是把「单号3」写在对象身上 run_inst → 把这个对象交给空闲工人 工人调用 DO → 等 2 秒,写下「单号3已签收」 对象寄回来 → 你从 mv_msg 里读到这句话所以DO不需要IMPORTING:单号已经在MV_BILL里了。
run_inst这一行会卡住
不是「分完件你就去干别的」。10 个件全回来(或失败),这行才结束。你是调度,要在现场等。
回来的LT_OUT是回执本,一行一个件
| 字段 | 人话 |
|---|---|
INST | 盖完寄回来的那个快递对象(失败时可能是空的) |
INDEX | 当初清单里第几个(1 号、2 号…) |
TIME | 这个工人盖这个件花了几秒 |
MESSAGE | 有字 = 这个件出事了(超时、程序 dump) |
lo_one ?= ls_out-inst:回执上没写「这是快递」,只写「一个会做 DO 的工人」。要转回LCL_EXPRESS,才能看MV_MSG。
失败时不要转——东西可能没寄回来。所以先看MESSAGE和INST IS NOT BOUND。
3.2 用 SM50 确认「真的有几个人在盖」
F8 之后立刻打开SM50。这 2 秒里应能看到多条对话进程同时亮,程序名经常是RS_ABAP_PARALLEL_INST。那就是被借来的仓管。
想看得更久,把WAIT UP TO 2 SECONDS改成 5。看完改回去即可。
4. 运行时到底发生了什么
主程序所在的进程,下文叫协调进程(领班)。
被借来跑DO的进程,叫工作进程(厨师)。
你的报表(调度,站在柜台) │ │ NEW 出 10 个快递对象,每个身上写着单号 │ ▼ RUN_INST 开始(你站着等) │ ├── 把「单号1」递给工人 A ──► 盖章 2 秒 ──► 写上已签收 ──► 送回 ├── 把「单号2」递给工人 B ──► 盖章 2 秒 ──► ... └── 把「单号3」递给工人 C ──► ... (工人不够就排队,盖完一件再拿下一件) │ ▼ 10 个回执都齐了 │ RUN_INST 结束,你 WRITE 屏幕有三句话,第一次学最容易忽略:
1. 工作进程是「新开的一次程序运行」。
它会重新加载你的报表。选择屏幕上勾的钩、你在屏幕填的工厂,不会自动出现在工作进程里。要带过去的东西,必须在NEW任务对象时塞进属性。
2. 两个进程的内表不是同一块内存。
协调进程的GT_OUT和线程里的GT_OUT各是各的。线程里改了,协调进程看不见,除非:
- 写进任务对象属性,随对象寄回来;或
- 线程里自己
COMMIT写数据库,协调进程再查表。
3.EXPORT TO MEMORY(ABAP 内存)跨不过去。
那是当前会话的抽屉。工作进程是另一个会话,抽屉是空的。共享数据请放对象属性,或写数据库(例如INDX)。
5. 从「一个快递」改成「一包物料」(只改三处)
刚才一个任务 =一个快递单号。
真实批导里一个任务通常 =一包内表(比如 1000 条待扩展物料)。
还是盖章那套:工人还是工人,只是手里从「一个件」换成「一筐件」。
把任务类想象成填空:
CLASS lcl_task DEFINITION. PUBLIC SECTION. INTERFACES if_abap_parallel. METHODS constructor IMPORTING it_data TYPE 你的内表类型 " 这一包要处理的数据 is_flag TYPE 你的开关结构. " 选择屏幕勾了哪些视图,也要带上 METHODS get_results EXPORTING et_data TYPE 你的内表类型. PRIVATE SECTION. DATA mt_data TYPE 你的内表类型. DATA ms_flag TYPE 你的开关结构. ENDCLASS. CLASS lcl_task IMPLEMENTATION. METHOD constructor. mt_data = it_data. ms_flag = is_flag. ENDMETHOD. METHOD if_abap_parallel~do. " 下面三行是关键:工作进程里全局变量是空的,要自己灌 gt_out = mt_data. p_accou = ms_flag-p_accou. p_qm = ms_flag-p_qm. PERFORM 原来的单条或单包处理. " 里面调 BAPI、COMMIT 本包 mt_data = gt_out. " 处理结果带回来 ENDMETHOD. METHOD get_results. et_data = mt_data. ENDMETHOD. ENDCLASS.主程序从「DO 10 TIMES」变成「按包 LOOP」:
" 1. 先把全部待处理数据放进 gt_all(和以前串行一样) " 2. 每 1000 条切一包,NEW 一个任务 DATA lt_pack TYPE 你的内表类型. DATA lt_in TYPE cl_abap_parallel=>t_in_inst_tab. DATA ls_flag TYPE 你的开关结构. ls_flag-p_accou = p_accou. " 屏幕上的勾,拷进结构 ls_flag-p_qm = p_qm. LOOP AT gt_all INTO DATA(ls_row). APPEND ls_row TO lt_pack. IF lines( lt_pack ) >= 1000. APPEND NEW lcl_task( it_data = lt_pack is_flag = ls_flag ) TO lt_in. CLEAR lt_pack. ENDIF. ENDLOOP. IF lt_pack IS NOT INITIAL. APPEND NEW lcl_task( it_data = lt_pack is_flag = ls_flag ) TO lt_in. ENDIF. " 3. 还是这一句 NEW cl_abap_parallel( p_percentage = 50 )->run_inst( EXPORTING p_in_tab = lt_in IMPORTING p_out_tab = DATA(lt_out) ). " 4. 收结果:和快递例子一样,先看失败,再 ?= 转回任务类原来的 BAPI FORM 可以不重写。并行框架只负责换厨房;炒什么菜,还是你原来那盘。
6. 第一次实战只记这 6 条
① 一包里的行,不要抢同一把锁
同一物料号 + 同一工厂,不要同时出现在两个正在跑的包里。
否则两个对话进程一起改同一条主数据,会互等,甚至超时。
分包前按业务主键去重;同一主键只进一包。
② 选择屏幕的值,必须放进任务对象
工作进程看不到P_ACCOU、P_WERKS1。
漏传的表现非常像 bug:「我明明勾了会计视图,扩出来利润中心是空的」。其实线程里那个勾是初始值。
③ 每一包自己 COMMIT
工作进程跑完,对象就被收回。没提交的数据库更改会丢。
BAPI 成功后在这一包里BAPI_TRANSACTION_COMMIT。
包 A 成功、包 B 失败,互不影响——这正是并行的好处。
④ 包别切太碎,也别切太大
| 切法 | 后果 |
|---|---|
| 一条一个任务 | 寄对象的时间可能比 BAPI 还长 |
| 一包五万条 | 一个对话进程占太久,容易超时,失败就整包重来 |
| 一包几百~一千条(BAPI 类) | 比较常见的起点 |
⑤ 先顺序跑通,再开并行
RUN_INST有个P_DEBUG。传到'X'时,不请厨师,就在当前程序里一个一个调DO。这样你才能下断点。
lo_par->run_inst( EXPORTING p_in_tab = lt_in p_debug = 'X' " 开发时打开;测并行时去掉 IMPORTING p_out_tab = lt_out ).⑥ 很短的活不要并行
官方说得很直白:把数据搬到另一个对话进程是要花时间的。
赋值、拼字符串这种,并行只会更慢。
7. 对照:物料扩展程序 ZMMU002S 是怎么套的
看懂第 3~5 节之后,这段只是「一个快递换成一包物料」。不必一次记完所有表名。
7.1 它在并行什么
从源工厂把物料主数据扩到目标工厂。一条大约要:
BAPI_MATERIAL_SAVEDATA- 必要时质量视图 BAPI
- 必要时 MRP 区域函数
一条大约 1 秒,一次上万条,所以值得并行。
读 Excel 仍然在前台做(要 SAP 窗口),读完再并行。
7.2 和最小例子的对应关系
| 快递例子 | ZMMU002S |
|---|---|
LCL_EXPRESS | LCL_MAT_PARALLEL(写在同一个报表里的本地类) |
MV_BILL一个单号 | MT_DATA:这一筐待扩展物料 |
| 没有开关 | MS_PARAM:销售/采购/会计等勾选 |
DO里等 2 秒再写「已签收」 | DO里灌全局变量,再PERFORM MATERIEL_IMPORT |
| 10 个快递 = 10 个任务 | 每 1000 条(P_PKG)= 1 个任务 |
RUN_INST | 同样,P_PERCENTAGE = 60 |
屏幕WRITE签收结果 | 从对象GET_RESULTS,失败则按批次号更新日志表 |
任务类的DO只有三步,和填空模板一样:
METHOD if_abap_parallel~do. init_log_records( ). " 准备本包日志内表 execute_import( ). " 参数写回 P_* / GT_*,再调原来的导入 save_log_final( ). " MODIFY 日志表并 COMMIT ENDMETHOD.EXECUTE_IMPORT里把属性倒回报表全局变量——因为原来的 FORM 读的就是这些全局变量:
p_accou = ms_param-p_accou. p_qm = ms_param-p_qm. " ... 其它勾选同样抄一遍 gt_tq34 = mt_tq34. " 质量类型,协调进程查一次带进来 gt_mdma = mt_mdma. " MRP 区域是否已存在,也是带进来 gt_out = mt_data. PERFORM materiel_import USING gt_mdma. mt_data = gt_out.7.3 分包用了批次号
每条待处理数据带一个BATCHNO,例如20260924-170000-0001。
结构里BATCHNO必须是第一个字段,才能用AT NEW batchno把同一包切出来(AT NEW会看这个字段左边的所有字段)。
批次号还有第二个用处:某个工作进程短 dump 了,对象回不来,协调进程仍可:
UPDATE zmmt146_log SET status = 'E' msg_mm = ls_out-message WHERE batchno = 这一包的批次号.把「整包任务挂了」和「某一条 BAPI 报错」分开记。
7.4 只读表不要每个线程查一遍
| 表 | 谁查 | 为什么传入 |
|---|---|---|
TQ34 | 程序开头查一次 | 每个线程质量视图都要用 |
MDMA | 开线程前按本批物料查一次 | 已经有 MRP 区域就跳过创建 |
这些内表作为任务属性被一起寄走。不要把全厂无关数据塞进每一个包。
7.5 用户要关窗口时:作业里面再并行
RUN_INST会让当前这个进程一直等到全部包结束。
用户如果直接把 GUI 关了,领班死了,厨师也会被收掉。
所以勾了后台执行时:
- 前台只负责读 Excel,把物料号写入数据库簇表
INDX JOB_OPEN+SUBMIT 本程序 VIA JOB- 用户可以关窗
- 后台作业当领班:取回物料号、生成待处理内表、再
RUN_INST
作业是后台进程(BTC),RUN_INST仍然去借对话进程(DIA)。
SM37 里看到的是作业总时间;SM50 里才能看到几条 DIA 在真正跑 BAPI。
8. 怎么判断「真的并行了」
| 你看到的 | 说明 |
|---|---|
SM50 同时有多条RS_ABAP_PARALLEL_INST | 并行在干活 |
| SM50 长期只有 1 条 | 几乎没并行:百分比太低、DIA 不够、或P_DEBUG还开着 |
| SM37 显示 2 小时 | 那是领班等待的墙钟时间,不是「一个进程串行了 2 小时」 |
| 50 个包、同时只有 5 个厨师 | 总时间大约是「10 波 × 每包时间」,不是 50 倍加速 |
粗算:
总时间 ≈ 向上取整(包数 ÷ 同时在跑的DIA数) × 一包时间5 万条、每包 1000 条 → 50 包。
同时 5 个 DIA、每包 3 分钟 → 大约 30 分钟,而不是 3 分钟。
若 SM50 只有 5 个 DIA 可借,把百分比从 60 调到 80 也变不出更多厨师。
9. 领班的两个常用旋钮
第一次只用这两个:
DATA(lo_par) = NEW cl_abap_parallel( p_percentage = 50 " 最多占用「可借给并行的对话进程」的一半,默认就是 50 p_timeout = 0 " 0 = 不另设超时,沿用系统限制 ). lo_par->run_inst( ... ).P_PERCENTAGE不是「占全部对话进程的 50%」。
系统会先留一些对话进程给真正的前台用户(防止别人登不进去),剩下那部分才拿来并行。再在这上面乘百分比。
生产上不要开到 100。物料扩展这种重活,案例里用了 60,已经略激进。
P_TIMEOUT不要随手填 60。
一包 1000 条 BAPI 可能跑很多分钟,填 60 秒会把正常包全部判失败。
其它参数(只用本服务器、最多几个任务、一挂就停)放在文末附录,第一次可以不看。
10. 你的系统够不够用这套写法
在 ADT 或 SE24 打开类CL_ABAP_PARALLEL,看有没有方法RUN_INST。
| 看到什么 | 怎么办 |
|---|---|
有RUN_INST | 按本文写(一般是 7.54 及以上,S/4 常见) |
只有RUN,没有RUN_INST | 系统偏老。要用另一种「自己把数据打成 XSTRING」的写法,见附录 C |
| 类都没有 | 基线太老,继续用传统 aRFC,或让 Basis 看 Note 2791374 能否下发 |
案例程序所在系统是SAP_BASIS 7.56,有RUN_INST。
11. 学完后动手的顺序
- 把第 3 节快递程序跑通。十个件应大约 2 秒结束,SM50 里能看到多个人同时盖章。
- 打开
P_DEBUG = 'X',对DO下断点,确认能停住(这时会变回一个一个盖,大约 20 秒)。 - 把「一个单号」换成「你的内表的一小包」,
DO里调现有 FORM。 - 确认这一包在调试模式下 BAPI + COMMIT + 日志都对。
- 关掉
P_DEBUG,P_PERCENTAGE先 30 再 50。 - 用 SM50 数实际有几个工人,再决定一筐装多少条。
不要一上来就把 5 万条、Excel、后台作业、日志表一起改。那是第 7 节的完整形态,不是第一天的作业。
附录 A:构造参数全表(需要调优再看)
7.56 系统里CL_ABAP_PARALLEL构造方法:
| 参数 | 第一次要不要管 | 含义 |
|---|---|---|
P_PERCENTAGE | 要 | 占用可借 DIA 的百分比,默认 50 |
P_TIMEOUT | 通常 0 | 单个任务最多跑多少秒 |
P_NUM_TASKS | 可选 | 同时跑的任务上限。和百分比一起传时,实际取更保守的那个 |
P_LOCAL_SERVER | 一般不传 | 'X'= 只用当前应用服务器。任务若碰本机文件再打开 |
P_NUM_PROCESSES | 不要用 | 类注释写明已过时 |
P_ABORT_ON_ERROR | 可选 | 有任务因 RFC/短 dump 挂掉时,剩下的任务不再进DO |
另外还有FORK/JOIN(不必等全部结束就能收已完成的包)、后台提交类方法。那是进阶,第一次做批导用不到。RUN_INST是等到全部结束才返回。
附录 B:回来的结果行怎么判断
LOOP AT lt_out INTO DATA(ls_done). IF ls_done-message IS NOT INITIAL OR ls_done-inst IS NOT BOUND. " 任务级失败:整包没正常跑完 " 用 ls_done-index 找回当初那一包,去更新日志 CONTINUE. ENDIF. DATA(lo_task) = CAST lcl_task( ls_done-inst ). lo_task->get_results( IMPORTING et_data = DATA(lt_part) ). " 业务级失败(某条 BAPI 报 E)已经写在 lt_part 或日志表里 ENDLOOP.两种失败不要混:
- 任务失败:厨师把灶台烧了(dump、超时)。对象可能没了。
- 业务失败:厨师把菜做完了,但这道菜盐放多了(BAPI 返回 E)。对象还在。
附录 C:没有 RUN_INST 时的另一条路(了解即可)
更老的系统只有RUN:自己把每包打成XSTRING,任务类继承CL_ABAP_PARALLEL并重写DO,在DO里再解开。样板代码明显更多。能用RUN_INST就不要走这条。
社区博文Using class CL_ABAP_PARALLEL for mass parallel dialog work processes(Dan Mazurowski)有完整RUN示例。
附录 D:参考
- 事务 SE24 / ADT:类
CL_ABAP_PARALLEL上按 F2,看 ABAP Doc(比 Help 网页全) - SAP Help Portal 搜Parallel Processing+
CL_ABAP_PARALLEL - SAP Note 2791374(类下发到 7.50)、2880858(
RUN_INST从 7.54 起,不下放到更低版本) - 接口
IF_ABAP_PARALLEL:继承IF_SERIALIZABLE_OBJECT,方法只有DO - SM50 里的函数名
RS_ABAP_PARALLEL_INST:用来确认并行在跑,不要当业务 API 去调
结束前再重复一遍
你真正要写的,永远是这三块:
- 一个任务类:属性 = 一包数据 + 配置;
DO= 处理这一包。 - 主程序切包:
NEW出 N 个对象。 - 一句
RUN_INST:卡住直到全部结束,再按成功/失败收卷。
快递例子就是这三块。物料扩展也是这三块,只是一筐更重、多传了选择屏幕和两张缓存表、多写了一张日志表。先把 10 个快递跑通,再往筐里塞 BAPI。