批量修改封装_<1%修改量,让单线程代码完美并行

本文介绍将成品单线程代码转为支持多线程并行的简单方式。分析目标场景,指出问题源于内部无上下文封装、存在静态变量导致数据竞争。解决方案是使用c++11的thread_local关键字让静态变量有独立实例。通过示范案例验证,重构后实现完美并行且无bug。

众所周知,多线程开发要处理各种同步和竞争问题,一不留神就会原地爆炸。

那么问题来了,如果手头有一份成品单线程代码,如何让它支持多线程并行?

本文将介绍一个最为简单粗暴的方式,重构改动1%以下,几乎不会引入任何问题,并且在目标场景下可以完美并行。

目标场景

将这份代码当做黑盒使用,不强求内部多线程,而是将输入输出当做一个任务,多个任务之间可以并行。

问题分析

该目标场景是最简单的并行场景了,理论上没任何数据竞争,多个任务之间的关系就和多个进程一样完全隔离的。

从“输入——执行——输出”这条链条来看,包装为任务的方式,输入输出先天就隔离开来,不存在数据竞争,数据竞争主要是执行过程中的内部状态,也就是副作用。

这不还是废话么,又绕回开头了。

不不不,仔细想想,引起副作用的内部状态是怎么来的?

原因很简单,就是内部无上下文封装,所有执行共享一个上下文,因此并行场景存在竞争。

而这些内部共享的上下文是怎么存在的呢?从编码角度来看,必然是静态变量。

解决方案

问题分析到这里,解决方案已经相当清晰了,让这些静态变量每次执行时都使用独立实例就行,这样上下文/状态自然就隔离开了。

要做到这一点,无需要重构数据结构和代码逻辑,只需要引入c++11的一个关键字——thread_local,在所有静态变量的声明处加上这个关键字即可。

thread_local关键字确保了该对象在每个线程上都有一个副本。然后我们再让每个任务都在一个线程里执行,问题就解决了。

残留问题

该手段下,每次执开启一个新线程和直接复用线程池,存在一定的差异。但实际运行结果,在绝大多数场景下应该是没区别的。

如果复用线程的话,在同一个线程里多次执行,内部状态还是沿用上次的,和修改前顺序调用多次一样。

此时内部状态是否会导致多次运行结果不一致,需要看目标代码本身的逻辑。

但考虑到修改之前的单线程代码,本来就是顺序执行多次,每次都沿用上一次的内部状态,所以理论上和修改后在线程池的运行结果不会存在差异。

示范案例

我手头有一个业务需求,将一组二进制数据文件转为excel表格导出。

工具库用的QtXlsx,该库是单线程设计,未对多线程做考虑。

实际表现是:

  • 生成数据对象过程中可以并行,数据内容包含在独立的c++对象中。
  • 保存至xlsx时,因为xlsx有数据复用的机制,相同的内容会引用同一个数据对象,因此库内部使用静态字典来整理这些复用的数据。
  • 二进制文件读取到生成xlsx内存对象耗时大约24s(秒表计时),多任务可以并行。
  • 保存文件过程无法并行,通过加锁进行同步,每个文件保存需要14s(手掐秒表)。
  • 因此总耗时为24 + 14*4 = 80s。

全文搜索代码中的关键字,大约有十多个static对象。无脑给所有对象都加上thread_local标注后,编译运行:

  • 总耗时38s(手动秒表);
  • 38 = 24 + 14,符合设想结果,完美实现并行。

为确认并行计算并未出错,进行如下验证:

  1. 将二进制文件复制四份;
  2. 用互斥锁同步版批量导出四个文件;
  3. 用完美并行版批量导出四个文件;
  4. 将八个xlsx表格用7zip解压(-x的office文件本质是zip压缩包,包含xml格式+数据文件,文本表格的数据文件也是纯文本);
  5. 用diff对比八组xml和文本文件。

对比结果完全相同,说明本重构方案无bug,以上。

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="jp.co.sej.ssc.or.common.db.BatchDisplayOrderSendDAO"> <!-- 陳列順送信ワークに登録する。 --> <update id="delDisplayOrderSend"> TRUNCATE TABLE w_display_order_send </update> <insert id="insertDisplayOrderSend"> INSERT INTO w_display_order_send( store_code , item_code , gondola_furniture_type , display_gondola , display_shelf , display_row , display_face , display_order_input_datetime , extraction_date , update_datetime , update_function_id , update_person_id , update_count ) SELECT tmp.store_code , tmp.item_code , CASE WHEN tmp.gondola_furniture_type IS NULL THEN NULL ELSE LPAD(TRIM(tmp.gondola_furniture_type), 3, '0') END AS gondola_furniture_type , tmp.display_gondola , tmp.display_shelf , tmp.display_row , tmp.display_face , tmp.display_order_input_datetime , #{systemDate} :: date , CURRENT_TIMESTAMP , #{taskId} , #{taskId} , 0 FROM ( SELECT te1.store_code , tn.item_code , gl1.gondola_furniture_type , tn.display_gondola , tn.display_shelf , tn.display_row , tn.display_face , tn.display_order_input_datetime , ROW_NUMBER() OVER ( PARTITION BY te1.store_code , tn.item_code , tn.label_code ORDER BY tn.display_order_input_datetime DESC ) AS registrationOrder FROM m_store_number AS te1 INNER JOIN m_display AS tn ON te1.original_store_code = tn.original_store_code LEFT JOIN m_gondola_layout AS gl1 ON gl1.version = #{gondolaLayoutVersion} AND tn.original_store_code = gl1.original_store_code AND tn.display_gondola = gl1.gondola_number WHERE te1.version = #{storeVersion} AND #{systemDate} :: date BETWEEN te1.apply_start_date AND te1.apply_end_date ) AS tmp WHERE tmp.registrationOrder <= 2 UNION ALL SELECT DISTINCT te2.store_code , sh1.item_code , CASE WHEN gl2.gondola_furniture_type IS NULL THEN NULL ELSE LPAD(TRIM(gl2.gondola_furniture_type), 3, '0') END AS gondola_furniture_type , tt1.display_gondola , tt1.display_shelf , NULL :: numeric AS display_row , NULL :: numeric AS display_face , tt1.special_display_input_datetime , #{systemDate} :: date , CURRENT_TIMESTAMP , #{taskId} , #{taskId} , 0 FROM m_store_number AS te2 INNER JOIN m_specific_display_order AS tt1 ON te2.original_store_code = tt1.original_store_code LEFT JOIN m_gondola_layout AS gl2 ON gl2.version = #{gondolaLayoutVersion} AND tt1.original_store_code = gl2.original_store_code AND tt1.display_gondola = gl2.gondola_number INNER JOIN m_pattern AS pt ON pt.version = #{patternVersion} AND tt1.original_store_code = pt.original_store_code AND #{systemDate} :: date BETWEEN pt.apply_start_date AND pt.apply_end_date INNER JOIN m_item AS sh1 ON sh1.version = #{itemVersion} AND pt.pattern_type = sh1.pattern_type AND pt.pattern_code = sh1.pattern_code AND #{systemDate} :: date BETWEEN sh1.apply_start_date AND sh1.apply_end_date AND tt1.information_category_code = sh1.information_category_code LEFT JOIN m_license AS li1 ON li1.version = #{licenseVersion} AND te2.original_store_code = li1.original_store_code AND sh1.license_code = li1.license_code LEFT JOIN m_item_by_specific_store_recommendation ts ON ts.version = #{specificItemVersion} AND tt1.original_store_code = ts.original_store_code AND sh1.item_code = ts.item_code AND #{systemDate} :: date BETWEEN ts.apply_start_date AND ts.apply_end_date WHERE te2.version = #{storeVersion} AND #{systemDate} :: date BETWEEN te2.apply_start_date AND te2.apply_end_date AND ( sh1.license_code = '00' OR ( sh1.license_code != '00' AND li1.licenseditem_adopt_flag != '2' ) ) AND ( sh1.specific_item_type = ' ' OR ( sh1.specific_item_type IN ('1', '2', 'G') AND ts.original_store_code IS NOT NULL ) ) UNION ALL SELECT DISTINCT te3.store_code , '999999' , CASE WHEN gl3.gondola_furniture_type IS NULL THEN NULL ELSE LPAD(TRIM(gl3.gondola_furniture_type), 3, '0') END AS gondola_furniture_type , tt2.display_gondola , tt2.display_shelf , NULL :: numeric AS display_row , NULL :: numeric AS display_face , tt2.special_display_input_datetime , #{systemDate} :: date , CURRENT_TIMESTAMP , #{taskId} , #{taskId} , 0 FROM m_store_number AS te3 INNER JOIN m_specific_display_order_by_specific_category AS tt2 ON te3.original_store_code = tt2.original_store_code LEFT JOIN m_gondola_layout AS gl3 ON gl3.version = #{gondolaLayoutVersion} AND tt2.original_store_code = gl3.original_store_code AND tt2.display_gondola = gl3.gondola_number INNER JOIN m_recommendation_group_by_specific_category AS rg ON rg.version = #{recommendationGroupVersion} AND tt2.original_store_code = rg.original_store_code AND #{systemDate} :: date BETWEEN rg.apply_start_date AND rg.apply_end_date INNER JOIN m_item_by_specific_category AS sh2 ON sh2.version = #{itemSpecificCategory} AND rg.recommendation_group_type = sh2.recommendation_group_type AND rg.recommendation_group_code = sh2.recommendation_group_code AND #{systemDate} :: date BETWEEN sh2.apply_start_date AND sh2.apply_end_date AND tt2.information_category_code = sh2.information_category_code LEFT JOIN m_license AS li2 ON li2.version = #{licenseVersion} AND te3.original_store_code = li2.original_store_code AND sh2.license_code = li2.license_code WHERE te3.version = #{storeVersion} AND #{systemDate} :: date BETWEEN te3.apply_start_date AND te3.apply_end_date AND ( sh2.license_code = '00' OR ( sh2.license_code != '00' AND li2.licenseditem_adopt_flag != '2' ) ) </insert> </mapper> sql改善要求:  1.SQL分割して並行して行う   2.INSERT処理をストアドに変更 対象レコードを絞る   1.仮想ビュー追加   2.一部検索条件を結合条件に変更 不影响插入数据的情况下怎么改,
最新发布
07-30
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值