并发编程中的WIP技巧

5 篇文章 0 订阅
4 篇文章 0 订阅
本文介绍了如何利用工作进行中(WIP)模式和CAS操作来解决并发场景下,确保某个方法在已有线程调用时,其他线程能等待并仅在首次调用完成后再次调用的方法。通过示例展示了两种技巧:1)任意线程作为消费者,2)线程池中的单个线程作为消费者,确保数据结构更新后的DB同步操作被串行化。同时强调了异常处理的重要性,避免系统进入错误状态。
摘要由CSDN通过智能技术生成

介绍

遇到如下场景:
对于某个方法, 调用它时如果有另外一个线程正在调用该方法, 那么当前线程放弃调用, 但另外一个线程调用完毕之后必须再调用一次该方法.

具体例子:
多个线程更新一个数据结构(这个行为是否并发安全不在本次讨论中), 然后一旦结构发生修改就将数据同步到DB. 显然同步到DB是一个相对高耗时的行为, 而且短期对数据结构的更新显然可以压缩成一次DB的更新. 这也就是我上述说的场景了.

显然, 你可以加锁来解决. 但本文介绍一种wip技巧使用CAS(Compare And Set)来解决.

WIP技巧

wip = Working-In-Progress
这种模式我也不知道该怎么称呼, 但我在很多地方都有看过它的用法, 就叫它WIP吧.
RxJava2
ReactorProject
GRPC

3种技巧

  1. 随意选一个生产者作为消费者
  2. 使用线程池上的任意线程作为消费者(但每个时刻只会有一个消费者)
  3. 用一个固定的线程作为消费者(本质和2一样, 只要创建一个单线程的线程池即可)

这里只讨论前2种技巧.

public class Wip1 {
	// 一般这种模式里都会有一个原子int变量用于cas竞争. 
	// 在某些变种里也会有 AtomicReference<Thread> 来表示. 本质是一样的.
    private final AtomicInteger                 wip = new AtomicInteger();
    // q是某个数据结构, 它的并发安全不在我们的讨论范围内, 这里假设它是一个ConcurrentLinkedQueue
    private final ConcurrentLinkedQueue<Object> q   = new ConcurrentLinkedQueue<>();

	// 任意线程(以生产者的身份)可以调用该方法
    public void produce(Object data) {
        q.offer(data);
        
        // (生产者)对数据结构做出修改之后, 立即调用drainLoop
        // 当然你可以将drainLoop的概念暴露出去, 从而实现多次produce只调用一次drainLoop, 效率更高一些.
        drainLoop();
    }

    private void drainLoop() {
    	// 多个线程(生产者)竞争wip, 第一个成功的线程获得执行权, 此后它变为消费者身份.
        if (wip.getAndIncrement() != 0) {
            return;
        }
        int delta = wip.get();
        do {

            // loop body: 有以下几个特征
            // 1. 它肯定是被串行执行, 但可能在不同的线程上
            // 2. 它一般包含一个循环, 处理一批元素: 这样做 1. 不会丢失元素(即元素在队列里, 但是没有处理它) 2.效率高
            // 3. 处理过程不允许抛异常(必须catch掉), 否则drainLoop流程被意外中断, 此时处于一个中间错误状态: wip非0但实际没有线程在执行任务.

            Object obj;
            while ((obj = q.poll()) != null) {
                // process obj
                System.out.println(obj);
            }

            // 这个方法使得wip快速收敛到0, 并且不会错误地丢失对drainLoop的需求.
            // wip/delta变量的用法是该wip模式实现的精髓, 需要细品.
            delta = wip.addAndGet(-delta);
        } while (delta != 0);
    }
}

技巧2只需在技巧1的基础上改改即可.

public class Wip2 {
    private static final Logger LOGGER = LoggerFactory.getLogger(Wip2.class);

    private final Executor                      executor;
    private final AtomicInteger                 wip = new AtomicInteger();
    private final ConcurrentLinkedQueue<Object> q   = new ConcurrentLinkedQueue<>();

    public Wip2(Executor executor) {
        this.executor = Objects.requireNonNull(executor);
    }

    public void produce(Object data) {
        q.offer(data);
        drainLoop();
    }

    private void drainLoop() {
        // 这个判断不用放入线程池
        if (wip.getAndIncrement() != 0) {
            return;
        }
		
        // 抢到执行权之后将后续动作放入线程池即可
        try {
            executor.execute(this::drainLoop0);
        } catch (RejectedExecutionException e) {
            // TODO 一般是线程池关闭或队列满了, 可以考虑降级到当前线程执行任务, 否则当前进入一个错误的状态了, 即q里有元素, 但没有线程在处理它们; 另外wip也没有减成0
            LOGGER.error("error", e);
            drainLoop0();
        }
    }

    private void drainLoop0() {
        int delta = wip.get();
        do {

            Object obj;
            while ((obj = q.poll()) != null) {
                // process obj
                System.out.println(obj);
            }

            // 这个方法使得wip快速收敛到0
            delta = wip.addAndGet(-delta);
        } while (delta != 0);
    }
}

注意

实现时我们需要非常注意异常的处理, 如果处理元素时抛异常并且没有接住, 那么该wip就进入一个错误模式(wip非零缺没有线程正在处理, 队列非空缺没有线程正在处理(很可能需要等下一个元素才能跳出当前局面)). 最坏情况下还会导致整个wip模式止步不前.

异常的处理其实也很简单, 就是对于所有非wip技巧自身可控的因素都进行try/catch.
就我们上述2个例子而言, 就是指:

  1. obj元素的处理
  2. executor.execute()

q的话很多时候实现为wip技巧内部的一个MPSC队列, 因此不讨论它.

用途

  1. 任意方法(一般是执行很快的, 并且返回值是void(不立即需要结果), 类似fire-and-forget)串行化
  2. 对象生命周期方法串行化: 可以保证某个对象的生命周期方法都在同一个线程上执行(GRPC里就是这么用的)
  3. 攒批发送: 假设元素一个一个流入地产生, 而我们想一批一批地将他们写到DB, 也可以用这种方式, 当然你需要再加个定时器强制写出(否则当元素很稀疏时候无法保证实时性).

参考资料

  1. Disruptor
  2. JCTools
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值