问题

(1)Phaser是什么?

(2)Phaser具有哪些特性?

(3)Phaser相对于CyclicBarrier和CountDownLatch的优势?

简介

Phaser,翻译为阶段,它适用于这样一种场景,一个大任务可以分为多个阶段完成,且每个阶段的任务可以多个线程并发执行,但是必须上一个阶段的任务都完成了才可以执行下一个阶段的任务。

这种场景虽然使用CyclicBarrier或者CountryDownLatch也可以实现,但是要复杂的多。首先,具体需要多少个阶段是可能会变的,其次,每个阶段的任务数也可能会变的。相比于CyclicBarrier和CountDownLatch,Phaser更加灵活更加方便。

使用方法

下面我们看一个最简单的使用案例:

  1. public class PhaserTest {
  2. public static final int PARTIES = 3;
  3. public static final int PHASES = 4;
  4. public static void main(String[] args) {
  5. Phaser phaser = new Phaser(PARTIES) {
  6. @Override
  7. protected boolean onAdvance(int phase, int registeredParties) {
  8. // 【本篇文章由公众号“彤哥读源码”原创,请支持原创,谢谢!】
  9. System.out.println("=======phase: " + phase + " finished=============");
  10. return super.onAdvance(phase, registeredParties);
  11. }
  12. };
  13. for (int i = 0; i < PARTIES; i++) {
  14. new Thread(()->{
  15. for (int j = 0; j < PHASES; j++) {
  16. System.out.println(String.format("%s: phase: %d", Thread.currentThread().getName(), j));
  17. phaser.arriveAndAwaitAdvance();
  18. }
  19. }, "Thread " + i).start();
  20. }
  21. }
  22. }

这里我们定义一个需要4个阶段完成的大任务,每个阶段需要3个小任务,针对这些小任务,我们分别起3个线程来执行这些小任务,查看输出结果为:

  1. Thread 0: phase: 0
  2. Thread 2: phase: 0
  3. Thread 1: phase: 0
  4. =======phase: 0 finished=============
  5. Thread 2: phase: 1
  6. Thread 0: phase: 1
  7. Thread 1: phase: 1
  8. =======phase: 1 finished=============
  9. Thread 1: phase: 2
  10. Thread 0: phase: 2
  11. Thread 2: phase: 2
  12. =======phase: 2 finished=============
  13. Thread 0: phase: 3
  14. Thread 2: phase: 3
  15. Thread 1: phase: 3
  16. =======phase: 3 finished=============

可以看到,每个阶段都是三个线程都完成了才进入下一个阶段。这是怎么实现的呢,让我们一起来学习吧。

原理猜测

根据我们前面学习AQS的原理,大概猜测一下Phaser的实现原理。

首先,需要存储当前阶段phase、当前阶段的任务数(参与者)parties、未完成参与者的数量,这三个变量我们可以放在一个变量state中存储。

其次,需要一个队列存储先完成的参与者,当最后一个参与者完成任务时,需要唤醒队列中的参与者。

嗯,差不多就是这样子。

结合上面的案例带入:

初始时当前阶段为0,参与者数为3个,未完成参与者数为3;

第一个线程执行到phaser.arriveAndAwaitAdvance();时进入队列;

第二个线程执行到phaser.arriveAndAwaitAdvance();时进入队列;

第三个线程执行到phaser.arriveAndAwaitAdvance();时先执行这个阶段的总结onAdvance(),再唤醒前面两个线程继续执行下一个阶段的任务。

嗯,整体能说得通,至于是不是这样呢,让我们一起来看源码吧。

源码分析

主要内部类

  1. static final class QNode implements ForkJoinPool.ManagedBlocker {
  2. final Phaser phaser;
  3. final int phase;
  4. final boolean interruptible;
  5. final boolean timed;
  6. boolean wasInterrupted;
  7. long nanos;
  8. final long deadline;
  9. volatile Thread thread; // nulled to cancel wait
  10. QNode next;
  11. QNode(Phaser phaser, int phase, boolean interruptible,
  12. boolean timed, long nanos) {
  13. this.phaser = phaser;
  14. this.phase = phase;
  15. this.interruptible = interruptible;
  16. this.nanos = nanos;
  17. this.timed = timed;
  18. this.deadline = timed ? System.nanoTime() + nanos : 0L;
  19. thread = Thread.currentThread();
  20. }
  21. }

先完成的参与者放入队列中的节点,这里我们只需要关注threadnext两个属性即可,很明显这是一个单链表,存储着入队的线程。

主要属性

  1. // 状态变量,用于存储当前阶段phase、参与者数parties、未完成的参与者数unarrived_count
  2. private volatile long state;
  3. // 最多可以有多少个参与者,即每个阶段最多有多少个任务
  4. private static final int MAX_PARTIES = 0xffff;
  5. // 最多可以有多少阶段
  6. private static final int MAX_PHASE = Integer.MAX_VALUE;
  7. // 参与者数量的偏移量
  8. private static final int PARTIES_SHIFT = 16;
  9. // 当前阶段的偏移量
  10. private static final int PHASE_SHIFT = 32;
  11. // 未完成的参与者数的掩码,低16位
  12. private static final int UNARRIVED_MASK = 0xffff; // to mask ints
  13. // 参与者数,中间16位
  14. private static final long PARTIES_MASK = 0xffff0000L; // to mask longs
  15. // counts的掩码,counts等于参与者数和未完成的参与者数的'|'操作
  16. private static final long COUNTS_MASK = 0xffffffffL;
  17. private static final long TERMINATION_BIT = 1L << 63;
  18. // 一次一个参与者完成
  19. private static final int ONE_ARRIVAL = 1;
  20. // 增加减少参与者时使用
  21. private static final int ONE_PARTY = 1 << PARTIES_SHIFT;
  22. // 减少参与者时使用
  23. private static final int ONE_DEREGISTER = ONE_ARRIVAL|ONE_PARTY;
  24. // 没有参与者时使用
  25. private static final int EMPTY = 1;
  26. // 用于求未完成参与者数量
  27. private static int unarrivedOf(long s) {
  28. int counts = (int)s;
  29. return (counts == EMPTY) ? 0 : (counts & UNARRIVED_MASK);
  30. }
  31. // 用于求参与者数量(中间16位),注意int的位置
  32. private static int partiesOf(long s) {
  33. return (int)s >>> PARTIES_SHIFT;
  34. }
  35. // 用于求阶段数(高32位),注意int的位置
  36. private static int phaseOf(long s) {
  37. return (int)(s >>> PHASE_SHIFT);
  38. }
  39. // 已完成参与者的数量
  40. private static int arrivedOf(long s) {
  41. int counts = (int)s; // 低32位
  42. return (counts == EMPTY) ? 0 :
  43. (counts >>> PARTIES_SHIFT) - (counts & UNARRIVED_MASK);
  44. }
  45. // 用于存储已完成参与者所在的线程,根据当前阶段的奇偶性选择不同的队列
  46. private final AtomicReference<QNode> evenQ;
  47. private final AtomicReference<QNode> oddQ;

主要属性为stateevenQoddQ

(1)state,状态变量,高32位存储当前阶段phase,中间16位存储参与者的数量,低16位存储未完成参与者的数量【本篇文章由公众号“彤哥读源码”原创,请支持原创,谢谢!】;

(2)evenQ和oddQ,已完成的参与者存储的队列,当最后一个参与者完成任务后唤醒队列中的参与者继续执行下一个阶段的任务,或者结束任务。

构造方法

  1. public Phaser() {
  2. this(null, 0);
  3. }
  4. public Phaser(int parties) {
  5. this(null, parties);
  6. }
  7. public Phaser(Phaser parent) {
  8. this(parent, 0);
  9. }
  10. public Phaser(Phaser parent, int parties) {
  11. if (parties >>> PARTIES_SHIFT != 0)
  12. throw new IllegalArgumentException("Illegal number of parties");
  13. int phase = 0;
  14. this.parent = parent;
  15. if (parent != null) {
  16. final Phaser root = parent.root;
  17. this.root = root;
  18. this.evenQ = root.evenQ;
  19. this.oddQ = root.oddQ;
  20. if (parties != 0)
  21. phase = parent.doRegister(1);
  22. }
  23. else {
  24. this.root = this;
  25. this.evenQ = new AtomicReference<QNode>();
  26. this.oddQ = new AtomicReference<QNode>();
  27. }
  28. // 状态变量state的存储分为三段
  29. this.state = (parties == 0) ? (long)EMPTY :
  30. ((long)phase << PHASE_SHIFT) |
  31. ((long)parties << PARTIES_SHIFT) |
  32. ((long)parties);
  33. }

构造函数中还有一个parent和root,这是用来构造多层级阶段的,不在本文的讨论范围之内,忽略之。

重点还是看state的赋值方式,高32位存储当前阶段phase,中间16位存储参与者的数量,低16位存储未完成参与者的数量。

下面我们一起来看看几个主要方法的源码:

register()方法

注册一个参与者,如果调用该方法时,onAdvance()方法正在执行,则该方法等待其执行完毕。

  1. public int register() {
  2. return doRegister(1);
  3. }
  4. private int doRegister(int registrations) {
  5. // state应该加的值,注意这里是相当于同时增加parties和unarrived
  6. long adjust = ((long)registrations << PARTIES_SHIFT) | registrations;
  7. final Phaser parent = this.parent;
  8. int phase;
  9. for (;;) {
  10. // state的值
  11. long s = (parent == null) ? state : reconcileState();
  12. // state的低32位,也就是parties和unarrived的值
  13. int counts = (int)s;
  14. // parties的值
  15. int parties = counts >>> PARTIES_SHIFT;
  16. // unarrived的值
  17. int unarrived = counts & UNARRIVED_MASK;
  18. // 检查是否溢出
  19. if (registrations > MAX_PARTIES - parties)
  20. throw new IllegalStateException(badRegister(s));
  21. // 当前阶段phase
  22. phase = (int)(s >>> PHASE_SHIFT);
  23. if (phase < 0)
  24. break;
  25. // 不是第一个参与者
  26. if (counts != EMPTY) { // not 1st registration
  27. if (parent == null || reconcileState() == s) {
  28. // unarrived等于0说明当前阶段正在执行onAdvance()方法,等待其执行完毕
  29. if (unarrived == 0) // wait out advance
  30. root.internalAwaitAdvance(phase, null);
  31. // 否则就修改state的值,增加adjust,如果成功就跳出循环
  32. else if (UNSAFE.compareAndSwapLong(this, stateOffset,
  33. s, s + adjust))
  34. break;
  35. }
  36. }
  37. // 是第一个参与者
  38. else if (parent == null) { // 1st root registration
  39. // 计算state的值
  40. long next = ((long)phase << PHASE_SHIFT) | adjust;
  41. // 修改state的值,如果成功就跳出循环
  42. if (UNSAFE.compareAndSwapLong(this, stateOffset, s, next))
  43. break;
  44. }
  45. else {
  46. // 多层级阶段的处理方式
  47. synchronized (this) { // 1st sub registration
  48. if (state == s) { // recheck under lock
  49. phase = parent.doRegister(1);
  50. if (phase < 0)
  51. break;
  52. // finish registration whenever parent registration
  53. // succeeded, even when racing with termination,
  54. // since these are part of the same "transaction".
  55. while (!UNSAFE.compareAndSwapLong
  56. (this, stateOffset, s,
  57. ((long)phase << PHASE_SHIFT) | adjust)) {
  58. s = state;
  59. phase = (int)(root.state >>> PHASE_SHIFT);
  60. // assert (int)s == EMPTY;
  61. }
  62. break;
  63. }
  64. }
  65. }
  66. }
  67. return phase;
  68. }
  69. // 等待onAdvance()方法执行完毕
  70. // 原理是先自旋一定次数,如果进入下一个阶段,这个方法直接就返回了,
  71. // 如果自旋一定次数后还没有进入下一个阶段,则当前线程入队列,等待onAdvance()执行完毕唤醒
  72. private int internalAwaitAdvance(int phase, QNode node) {
  73. // 保证队列为空
  74. releaseWaiters(phase-1); // ensure old queue clean
  75. boolean queued = false; // true when node is enqueued
  76. int lastUnarrived = 0; // to increase spins upon change
  77. // 自旋的次数
  78. int spins = SPINS_PER_ARRIVAL;
  79. long s;
  80. int p;
  81. // 检查当前阶段是否变化,如果变化了说明进入下一个阶段了,这时候就没有必要自旋了
  82. while ((p = (int)((s = state) >>> PHASE_SHIFT)) == phase) {
  83. // 如果node为空,注册的时候传入的为空
  84. if (node == null) { // spinning in noninterruptible mode
  85. // 未完成的参与者数量
  86. int unarrived = (int)s & UNARRIVED_MASK;
  87. // unarrived有变化,增加自旋次数
  88. if (unarrived != lastUnarrived &&
  89. (lastUnarrived = unarrived) < NCPU)
  90. spins += SPINS_PER_ARRIVAL;
  91. boolean interrupted = Thread.interrupted();
  92. // 自旋次数完了,则新建一个节点
  93. if (interrupted || --spins < 0) { // need node to record intr
  94. node = new QNode(this, phase, false, false, 0L);
  95. node.wasInterrupted = interrupted;
  96. }
  97. }
  98. else if (node.isReleasable()) // done or aborted
  99. break;
  100. else if (!queued) { // push onto queue
  101. // 节点入队列
  102. AtomicReference<QNode> head = (phase & 1) == 0 ? evenQ : oddQ;
  103. QNode q = node.next = head.get();
  104. if ((q == null || q.phase == phase) &&
  105. (int)(state >>> PHASE_SHIFT) == phase) // avoid stale enq
  106. queued = head.compareAndSet(q, node);
  107. }
  108. else {
  109. try {
  110. // 当前线程进入阻塞状态,跟调用LockSupport.park()一样,等待被唤醒
  111. ForkJoinPool.managedBlock(node);
  112. } catch (InterruptedException ie) {
  113. node.wasInterrupted = true;
  114. }
  115. }
  116. }
  117. // 到这里说明节点所在线程已经被唤醒了
  118. if (node != null) {
  119. // 置空节点中的线程
  120. if (node.thread != null)
  121. node.thread = null; // avoid need for unpark()
  122. if (node.wasInterrupted && !node.interruptible)
  123. Thread.currentThread().interrupt();
  124. if (p == phase && (p = (int)(state >>> PHASE_SHIFT)) == phase)
  125. return abortWait(phase); // possibly clean up on abort
  126. }
  127. // 唤醒当前阶段阻塞着的线程
  128. releaseWaiters(phase);
  129. return p;
  130. }

增加一个参与者总体的逻辑为:

(1)增加一个参与者,需要同时增加parties和unarrived两个数值,也就是state的中16位和低16位;

(2)如果是第一个参与者,则尝试原子更新state的值,如果成功了就退出;

(3)如果不是第一个参与者,则检查是不是在执行onAdvance(),如果是等待onAdvance()执行完成,如果否则尝试原子更新state的值,直到成功退出;

(4)等待onAdvance()完成是采用先自旋后进入队列排队的方式等待,减少线程上下文切换;

arriveAndAwaitAdvance()方法

当前线程当前阶段执行完毕,等待其它线程完成当前阶段。

如果当前线程是该阶段最后一个到达的,则当前线程会执行onAdvance()方法,并唤醒其它线程进入下一个阶段。

  1. public int arriveAndAwaitAdvance() {
  2. // Specialization of doArrive+awaitAdvance eliminating some reads/paths
  3. final Phaser root = this.root;
  4. for (;;) {
  5. // state的值
  6. long s = (root == this) ? state : reconcileState();
  7. // 当前阶段
  8. int phase = (int)(s >>> PHASE_SHIFT);
  9. if (phase < 0)
  10. return phase;
  11. // parties和unarrived的值
  12. int counts = (int)s;
  13. // unarrived的值(state的低16位)
  14. int unarrived = (counts == EMPTY) ? 0 : (counts & UNARRIVED_MASK);
  15. if (unarrived <= 0)
  16. throw new IllegalStateException(badArrive(s));
  17. // 修改state的值
  18. if (UNSAFE.compareAndSwapLong(this, stateOffset, s,
  19. s -= ONE_ARRIVAL)) {
  20. // 如果不是最后一个到达的,则调用internalAwaitAdvance()方法自旋或进入队列等待
  21. if (unarrived > 1)
  22. // 这里是直接返回了,internalAwaitAdvance()方法的源码见register()方法解析
  23. return root.internalAwaitAdvance(phase, null);
  24. // 到这里说明是最后一个到达的参与者
  25. if (root != this)
  26. return parent.arriveAndAwaitAdvance();
  27. // n只保留了state中parties的部分,也就是中16位
  28. long n = s & PARTIES_MASK; // base of next state
  29. // parties的值,即下一次需要到达的参与者数量
  30. int nextUnarrived = (int)n >>> PARTIES_SHIFT;
  31. // 执行onAdvance()方法,返回true表示下一阶段参与者数量为0了,也就是结束了
  32. if (onAdvance(phase, nextUnarrived))
  33. n |= TERMINATION_BIT;
  34. else if (nextUnarrived == 0)
  35. n |= EMPTY;
  36. else
  37. // n 加上unarrived的值
  38. n |= nextUnarrived;
  39. // 下一个阶段等待当前阶段加1
  40. int nextPhase = (phase + 1) & MAX_PHASE;
  41. // n 加上下一阶段的值
  42. n |= (long)nextPhase << PHASE_SHIFT;
  43. // 修改state的值为n
  44. if (!UNSAFE.compareAndSwapLong(this, stateOffset, s, n))
  45. return (int)(state >>> PHASE_SHIFT); // terminated
  46. // 唤醒其它参与者并进入下一个阶段
  47. releaseWaiters(phase);
  48. // 返回下一阶段的值
  49. return nextPhase;
  50. }
  51. }
  52. }

arriveAndAwaitAdvance的大致逻辑为:

(1)修改state中unarrived部分的值减1;

(2)如果不是最后一个到达的,则调用internalAwaitAdvance()方法自旋或排队等待;

(3)如果是最后一个到达的,则调用onAdvance()方法,然后修改state的值为下一阶段对应的值,并唤醒其它等待的线程;

(4)返回下一阶段的值;

总结

(1)Phaser适用于多阶段多任务的场景,每个阶段的任务都可以控制得很细;

(2)Phaser内部使用state变量及队列实现整个逻辑【本篇文章由公众号“彤哥读源码”原创,请支持原创,谢谢!】;

(3)state的高32位存储当前阶段phase,中16位存储当前阶段参与者(任务)的数量parties,低16位存储未完成参与者的数量unarrived;

(4)队列会根据当前阶段的奇偶性选择不同的队列;

(5)当不是最后一个参与者到达时,会自旋或者进入队列排队来等待所有参与者完成任务;

(6)当最后一个参与者完成任务时,会唤醒队列中的线程并进入下一个阶段;

彩蛋

Phaser相对于CyclicBarrier和CountDownLatch的优势?

答:优势主要有两点:

(1)Phaser可以完成多阶段,而一个CyclicBarrier或者CountDownLatch一般只能控制一到两个阶段的任务;

(2)Phaser每个阶段的任务数量可以控制,而一个CyclicBarrier或者CountDownLatch任务数量一旦确定不可修改。

推荐阅读

1、死磕 java同步系列之开篇

2、死磕 java魔法类之Unsafe解析

3、死磕 java同步系列之JMM(Java Memory Model)

4、死磕 java同步系列之volatile解析

5、死磕 java同步系列之synchronized解析

6、死磕 java同步系列之自己动手写一个锁Lock

7、死磕 java同步系列之AQS起篇

8、死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁

9、死磕 java同步系列之ReentrantLock源码解析(二)——条件锁

10、死磕 java同步系列之ReentrantLock VS synchronized

11、死磕 java同步系列之ReentrantReadWriteLock源码解析

12、死磕 java同步系列之Semaphore源码解析

13、死磕 java同步系列之CountDownLatch源码解析

14、死磕 java同步系列之AQS终篇

15、死磕 java同步系列之StampedLock源码解析

16、死磕 java同步系列之CyclicBarrier源码解析


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java同步系列之Phaser源码解析的更多相关文章

  1. 死磕 java同步系列之CyclicBarrier源码解析——有图有真相

    问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier ...

  2. 死磕 java同步系列之StampedLock源码解析

    问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWrite ...

  3. 死磕 java同步系列之Semaphore源码解析

    问题 (1)Semaphore是什么? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什么场景中? (4)Semaphore的许可次数是否可以动态增减? (5)Semaph ...

  4. 死磕 java同步系列之ReentrantReadWriteLock源码解析

    问题 (1)读写锁是什么? (2)读写锁具有哪些特性? (3)ReentrantReadWriteLock是怎么实现读写锁的? (4)如何使用ReentrantReadWriteLock实现高效安全的 ...

  5. 死磕 java同步系列之ReentrantLock源码解析(二)——条件锁

    问题 (1)条件锁是什么? (2)条件锁适用于什么场景? (3)条件锁的await()是在其它线程signal()的时候唤醒的吗? 简介 条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等 ...

  6. 死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁

    问题 (1)重入锁是什么? (2)ReentrantLock如何实现重入锁? (3)ReentrantLock为什么默认是非公平模式? (4)ReentrantLock除了可重入还有哪些特性? 简介 ...

  7. 死磕 java同步系列之CountDownLatch源码解析

  8. 死磕 java同步系列之zookeeper分布式锁

    问题 (1)zookeeper如何实现分布式锁? (2)zookeeper分布式锁有哪些优点? (3)zookeeper分布式锁有哪些缺点? 简介 zooKeeper是一个分布式的,开放源码的分布式应 ...

  9. 死磕 java同步系列之redis分布式锁进化史

    问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...

随机推荐

  1. MSIL实用指南-this的生成

    C#关键字是非静态方法体内部,用Ldarg_0指代this例子ilGenerator.Emit(OpCodes.Ldarg_0);

  2. Mobx-React : 当前适合React的状态管理工具

    MobX 简单.可扩展的状态管理        MobX 是由 Mendix.Coinbase.Facebook 开源和众多个人赞助商所赞助的.    安装 安装: npm install mobx ...

  3. Spring Cloud Alibaba | Nacos动态网关路由

    Spring Cloud Alibaba | Gateway基于Nacos动态网关路由 本篇实战所使用Spring有关版本: SpringBoot:2.1.7.RELEASE Spring Cloud ...

  4. HDU-6229 ICPC-沈阳M- Wandering Robots 概率

    HDU - 6229 题意: 在一个n*n的地图中,有一个初始在(0,0)位子的机器人,每次等概率的向相邻的格子移动或者留在原地.问最后留在格子(x,y)(x+y>=n-1)的地方的概率. 思路 ...

  5. 洛谷P1246编码问题-排列组合,分类讨论

    编码问题 题意就是a,b,c.....ab.....编码,给你一个字符串,输出这是第几个: 这里可以用暴力枚举,但也可以用组合数学的高超知识: 既然这样我就说一下排列组合的方法,如果要弄一个 各位数字 ...

  6. 牛客多校第六场 J Heritage of skywalkert 随即互质概率 nth_element(求最大多少项模板)

    链接:https://www.nowcoder.com/acm/contest/144/J来源:牛客网 skywalkert, the new legend of Beihang University ...

  7. 洛谷 P1091合唱队列

    吾王剑之所指,吾等心之所向                           ——<Fate/stay night> 题目:https://www.luogu.org/problem/P ...

  8. 史上最全Docker环境安装指南-让安装docker简单到爆

    一.思考❓❔ 1.什么是Docker? 装应用的容器 开发.测试.运维都偏爱的容器化技术 轻量级 扩展性 一次构建.多次分享.随处运行 2.安装Docker难不难? So easy! 此文看过之后,读 ...

  9. Windows 7 上怎样打开SQL Server 配置管理器

    场景 在Windows 7 上打开 SQL Server 的配置管理器. 实现 右击电脑--管理 在计算机管理--服务和应用程序-SQL Server 配置管理器 注: 博客首页: https://b ...

  10. Winform中使用DevExpress时给控件添加子控件的方法

    场景 在WInform中使用DevExpress时经常使用PanelControl控件用来进行布局设计,因此需要在代码中生成控件并添加子控件. 实现 一种是设置要添加的自控件的Parent属性为容器控 ...