xref: /aosp_15_r20/art/test/719-varhandle-concurrency/src/Main.java (revision 795d594fd825385562da6b089ea9b2033f3abf5a)
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 import java.lang.invoke.VarHandle;
18 import java.lang.invoke.MethodHandles;
19 import java.time.Duration;
20 import java.util.concurrent.atomic.AtomicInteger;
21 import java.util.function.Consumer;
22 
23 /**
24  * Runs tests to validate the concurrency guarantees of VarHandle.
25  *
26  * The tests involve having a lot of tasks and significantly fewer threads. The tasks are stored on
27  * a queue and each thread tries to grab a task from the queue using operations like
28  * VarHandle.compareAndSet(). If the operation works as specified, then each task would only be
29  * handled in a single thread, exactly once.
30  *
31  * The tasks just add atomically a specified integer to a total. If the total is different from the
32  * expected one, then either some tasks were run multiple times (on multiple threads), or some task
33  * were not run at all (skipped by all threads).
34  */
35 public class Main {
36     private static final VarHandle QA;
37     static {
38         QA = MethodHandles.arrayElementVarHandle(TestTask[].class);
39     }
40 
41     private static final int TASK_COUNT = 10000;
42     private static final int THREAD_COUNT = 20;
43     /* Each test may need several retries before a concurrent failure is seen. In the past, for a
44      * known bug, between 5 and 10 retries were sufficient. Use RETRIES to configure how many
45      * iterations to retry for each test scenario. However, to avoid the test running for too long,
46      * for example with gcstress, set a cap duration in MAX_RETRIES_DURATION. With this at least one
47      * iteration would run, but there could be fewer retries if each of them takes too long. */
48     private static final int RETRIES = 50;
49     // b/235431387: timeout reduced from 1 minute
50     private static final Duration MAX_RETRIES_DURATION = Duration.ofSeconds(15);
51 
main(String[] args)52     public static void main(String[] args) throws Throwable {
53         testConcurrentProcessing(new CompareAndExchangeRunnerFactory(), "compareAndExchange");
54         testConcurrentProcessing(new CompareAndSetRunnerFactory(), "compareAndSet");
55         testConcurrentProcessing(new WeakCompareAndSetRunnerFactory(), "weakCompareAndSet");
56     }
57 
testConcurrentProcessing(RunnerFactory factory, String testName)58     private static void testConcurrentProcessing(RunnerFactory factory, String testName)
59             throws Throwable {
60         final Duration startTs = Duration.ofNanos(System.nanoTime());
61         final Duration endTs = startTs.plus(MAX_RETRIES_DURATION);
62         for (int i = 0; i < RETRIES; ++i) {
63             concurrentProcessingTestIteration(factory, i, testName);
64             Duration now = Duration.ofNanos(System.nanoTime());
65             if (0 < now.compareTo(endTs)) {
66                 break;
67             }
68         }
69     }
70 
concurrentProcessingTestIteration( RunnerFactory factory, int iteration, String testName)71     private static void concurrentProcessingTestIteration(
72             RunnerFactory factory, int iteration, String testName) throws Throwable {
73         final TestTask[] tasks = new TestTask[TASK_COUNT];
74         final AtomicInteger result = new AtomicInteger();
75 
76         for (int i = 0; i < TASK_COUNT; ++i) {
77             tasks[i] = new TestTask(Integer.valueOf(i + 1), result::addAndGet);
78         }
79 
80         Thread[] threads = new Thread[THREAD_COUNT];
81         for (int i = 0; i < THREAD_COUNT; ++i) {
82             threads[i] = factory.createRunner(tasks);
83         }
84 
85         for (int i = 0; i < THREAD_COUNT; ++i) {
86             threads[i].start();
87         }
88 
89         for (int i = 0; i < THREAD_COUNT; ++i) {
90             threads[i].join();
91         }
92 
93         check(result.get(),
94               TASK_COUNT * (TASK_COUNT + 1) / 2,
95               testName + " test result not as expected",
96               iteration);
97     }
98 
99     /**
100      * Processes the task queue until there are no tasks left.
101      *
102      * The actual task-grabbing mechanism is implemented in subclasses through grabTask().
103      * This allows testing various mechanisms, like compareAndSet() and compareAndExchange().
104      */
105     private static abstract class TaskRunner extends Thread {
106 
107         protected final TestTask[] tasks;
108 
TaskRunner(TestTask[] tasks)109         TaskRunner(TestTask[] tasks) {
110             this.tasks = tasks;
111         }
112 
113         @Override
run()114         public void run() {
115             int i = 0;
116             while (i < TASK_COUNT) {
117                 TestTask t = (TestTask) QA.get(tasks, i);
118                 if (t == null) {
119                     ++i;
120                     continue;
121                 }
122                 if (!grabTask(t, i)) {
123                     continue;
124                 }
125                 ++i;
126                 VarHandle.releaseFence();
127                 t.exec();
128             }
129         }
130 
131         /**
132          * Grabs the next task from the queue in an atomic way.
133          *
134          * Once a task is retrieved successfully, the queue should no longer hold a reference to it.
135          * This would be done, for example, by swapping the task with a null value.
136          *
137          * @param t The task to get from the queue
138          * @param i The index where the task is found
139          *
140          * @return {@code true} if the task has been retrieved and is not available to any other
141          * threads. Otherwise {@code false}. If {@code false} is returned, then either the task was
142          * no longer present on the queue due to another thread grabbing it, or, in case of spurious
143          * failure, the task is still available and no other thread managed to grab it.
144          */
grabTask(TestTask t, int i)145         protected abstract boolean grabTask(TestTask t, int i);
146     }
147 
148     private static class TaskRunnerWithCompareAndExchange extends TaskRunner {
TaskRunnerWithCompareAndExchange(TestTask[] tasks)149         TaskRunnerWithCompareAndExchange(TestTask[] tasks) {
150             super(tasks);
151         }
152 
153         @Override
grabTask(TestTask t, int i)154         protected boolean grabTask(TestTask t, int i) {
155             return (t == QA.compareAndExchange(tasks, i, t, null));
156         }
157     }
158 
159     private static class TaskRunnerWithCompareAndSet extends TaskRunner {
TaskRunnerWithCompareAndSet(TestTask[] tasks)160         TaskRunnerWithCompareAndSet(TestTask[] tasks) {
161             super(tasks);
162         }
163 
164         @Override
grabTask(TestTask t, int i)165         protected boolean grabTask(TestTask t, int i) {
166             return QA.compareAndSet(tasks, i, t, null);
167         }
168     }
169 
170     private static class TaskRunnerWithWeakCompareAndSet extends TaskRunner {
TaskRunnerWithWeakCompareAndSet(TestTask[] tasks)171         TaskRunnerWithWeakCompareAndSet(TestTask[] tasks) {
172             super(tasks);
173         }
174 
175         @Override
grabTask(TestTask t, int i)176         protected boolean grabTask(TestTask t, int i) {
177             return QA.weakCompareAndSet(tasks, i, t, null);
178         }
179     }
180 
181 
182     private interface RunnerFactory {
createRunner(TestTask[] tasks)183         Thread createRunner(TestTask[] tasks);
184     }
185 
186     private static class CompareAndExchangeRunnerFactory implements RunnerFactory {
187         @Override
createRunner(TestTask[] tasks)188         public Thread createRunner(TestTask[] tasks) {
189             return new TaskRunnerWithCompareAndExchange(tasks);
190         }
191     }
192 
193     private static class CompareAndSetRunnerFactory implements RunnerFactory {
194         @Override
createRunner(TestTask[] tasks)195         public Thread createRunner(TestTask[] tasks) {
196             return new TaskRunnerWithCompareAndSet(tasks);
197         }
198     }
199 
200     private static class WeakCompareAndSetRunnerFactory implements RunnerFactory {
201         @Override
createRunner(TestTask[] tasks)202         public Thread createRunner(TestTask[] tasks) {
203             return new TaskRunnerWithWeakCompareAndSet(tasks);
204         }
205     }
206 
207     private static class TestTask {
208         private final Integer ord;
209         private final Consumer<Integer> action;
210 
TestTask(Integer ord, Consumer<Integer> action)211         TestTask(Integer ord, Consumer<Integer> action) {
212             this.ord = ord;
213             this.action = action;
214         }
215 
exec()216         public void exec() {
217             action.accept(ord);
218         }
219     }
220 
check(int actual, int expected, String msg, int iteration)221     private static void check(int actual, int expected, String msg, int iteration) {
222         if (actual != expected) {
223             System.err.println(String.format(
224                     "[iteration %d] %s : %d != %d", iteration, msg, actual, expected));
225             System.exit(1);
226         }
227     }
228 }
229