Summary
The C++11 support on Android when using g++ 4.8 is pretty good. The important concurrency features that were missing from g++ 4.6 are now available in the Standard Template Library.
Testing libgnustl_shared.so
After examining the LLVM libc++ library on Android, I wanted to check which C++11 features are currently available using g++ 4.8 and the GNU Standard Template Library (libstdc++). g++ 4.8 improved support for C++11 significantly (4.6 vs. 4.8), so the question was whether that support also made it into the Android-specific compilers. With the LLVM libc++ library, a lot of very specific platform code such as support for atomic operations was stubbed out, which would cascade down into problems with the mutual exclusion mechanisms and threads.
Based on a review of an existing codebase, the features most interesting to me were contained in the following headers: atomic
, chrono
, condition_variable
(condition_variable
), memory
(shared_ptr
, unique_ptr
), mutex
(lock_guard
, mutex
, unique_lock
), thread
(thread
). Also of interest was compiler support for lambdas and auto
keyword support.
So let’s see what’s available, by modifying the android-ndk-r9/samples/test-libstdc++
native executable to utilize a number of C++11 features to perform the same race condition-prone task. Namely, the program generates a random number of threads which each attempt to simultaneously increment some global variable by one, one hundred times.
C++ gives you a few ways to solve the problem, and I’ve tried each of them here. The logic of the incrementation isn’t exactly identical, and in some cases incredibly inefficient (the conditional_variable
case basically has all threads waiting for a single thread to complete, before they can acquire a shared resource mutex and begin executing). In any case, this is more a proof of library completeness than a proof of algorithmic efficiency.
The test source code looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
|
// New in C++ 11.
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <random>
#include <thread>
// Regular STL.
#include <list>
#include <string>
// C-compatibility.
#include <cassert>
#include <cerrno>
#include <cstdio>
#include <cstddef>
// Constants.
const int INCREMENT = 100;
// Utility functions (std::random)
std::random_device randomDevice;
std::default_random_engine randomEngine(randomDevice());
int diceRoll(int sides = 20)
{
std::uniform_int_distribution<int> distribution(1, sides);
int roll = distribution(randomEngine);
printf(" -- std::random rolled: %d\n", roll);
return roll;
}
// std::atomic
std::atomic<int> atomicInt(0);
void stdAtomicThreadFn(void)
{
// Try creating a bunch of threads, each one interested
// only in incrementing the global atomicInt.
// If it's truly atomic, there shouldn't be any race
// conditions and the final int value should be an
// exact multiple of this thread's loop.
for (int i = 0; i < INCREMENT; ++i)
{
++atomicInt;
}
}
// std::mutex + std::condition_variable + std::unique_lock + lambda tests
int nonAtomicInt = 0;
bool nonAtomicThreadsReady = false;
std::mutex nonAtomicIntMutex;
std::condition_variable nonAtomicIntCv;
void cvMutexThreadFn(void)
{
// Use the mutex and signalling capabilities in the
// Standard Library to safely increment the nonAtomicInt.
// Start the thread but wait on a condition variable
// before starting the nonAtomicInt incrementation.
std::unique_lock<std::mutex> lock(nonAtomicIntMutex);
nonAtomicIntCv.wait(lock, [] { return nonAtomicThreadsReady; });
// This runs after the nonAtomicIntMutex lock is reacquired,
// therefore only a single thread at a time is modifying
// nonAtomicInt.
for (int i = 0; i < INCREMENT; ++i)
{
nonAtomicInt++;
}
}
// std::lock_guard + std::mutex tests
int lockGuardInt = 0;
std::mutex lockGuardIntMutex;
void incrementLockGuardInt(void)
{
std::lock_guard<std::mutex> lock(lockGuardIntMutex);
lockGuardInt++;
}
// std::unique_ptr, std::shared_ptr, and std::make_shared test class.
class PtrTest
{
private:
std::string ptrType;
public:
PtrTest(std::string const& ptrType) : ptrType(ptrType) { printf(" -- PtrTest(): %s\n", ptrType.c_str()); };
~PtrTest() { printf(" -- ~PtrTest(): %s\n", ptrType.c_str()); };
};
void testA(void)
{
printf("std::atomic + std::random + std::thread + auto + ranged-for test\n");
std::list<std::thread> atomicIntThreads;
const int atomicIntThreadCount = diceRoll();
// Create a number of threads, all incrementing atomicInt.
for (int i = 0; i < atomicIntThreadCount; ++i)
{
atomicIntThreads.push_back(std::thread(&stdAtomicThreadFn));
}
// Join those threads.
for (auto & oneThread : atomicIntThreads)
{
oneThread.join();
}
assert(atomicInt == atomicIntThreadCount * INCREMENT);
printf(" -- atomicInt final value: %d\n", atomicInt.load());
}
void testB(void)
{
printf("std::condition_variable + std::mutex + std::unique_lock + std::random + std::thread + lambda test\n");
std::list<std::thread> nonAtomicIntThreads;
const int nonAtomicIntThreadCount = diceRoll();
// Create a number of threads, all incrementing nonAtomicInt.
for (int i = 0; i < nonAtomicIntThreadCount; ++i)
{
nonAtomicIntThreads.push_back(std::thread(&cvMutexThreadFn));
}
// Start those threads.
nonAtomicThreadsReady = true;
nonAtomicIntCv.notify_all();
// Join those threads.
for (auto & oneNonAtomicThread : nonAtomicIntThreads)
{
oneNonAtomicThread.join();
}
assert(nonAtomicInt == nonAtomicIntThreadCount * INCREMENT);
printf(" -- nonAtomicInt final value: %d\n", nonAtomicInt);
}
void testC(void)
{
printf("std::lock_guard + std::mutex + std::random + std::thread + lambda test\n");
std::list<std::thread> lockGuardThreads;
const int lockGuardThreadCount = diceRoll();
// Create a number of threads, all incrementing lockGuardInt.
for (int i = 0; i < lockGuardThreadCount; ++i)
{
lockGuardThreads.push_back(std::thread([] {
for (int j = 0; j < INCREMENT; ++j) incrementLockGuardInt();
}));
}
// Join those threads.
for (auto & oneLockGuardThread : lockGuardThreads)
{
oneLockGuardThread.join();
}
assert(lockGuardInt == lockGuardThreadCount * INCREMENT);
printf(" -- lockGuardInt final value: %d\n", lockGuardInt);
}
void testD(void)
{
printf("std::unique_ptr + std::shared_ptr + std::make_shared tests\n");
std::unique_ptr<PtrTest> uP(new PtrTest("std::unique_ptr"));
std::shared_ptr<PtrTest> sP = std::make_shared<PtrTest>("std::shared_ptr");
}
int main(void)
{
testA();
testB();
testC();
testD();
return 0;
}
|
I added an Application.mk
file to the jni
folder. Debugging is activated, so that the assert() macro isn’t disabled.
APP_ABI := armeabi APP_STL := gnustl_shared APP_CFLAGS := --std=c++11 APP_OPTIM := debug NDK_TOOLCHAIN_VERSION := 4.8
Building works fine:
$ ndk-build Compile++ thumb : test-libstl /sources/cxx-stl/gnu-libstdc++/4.8/libs/armeabi/ Executable : test-libstl Install : test-libstl => libs/armeabi/test-libstl Install : libgnustl_shared.so => libs/armeabi/libgnustl_shared.so $ adb push libs/armeabi/libgnustl_shared.so /data/tmp 366 KB/s (804484 bytes in 2.143s) $ adb push libs/armeabi/test-libstl /data/tmp 284 KB/s (50536 bytes in 0.173s)
After pushing the test-libstl
executable to the device, make sure the /data/tmp
folder is added to the LD_LIBRARY_PATH
:
localhost tmp # LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/data/tmp
Then just run the executable, and you should see:
localhost tmp # ./test-libstl std::atomic + std::random + std::thread + auto + ranged-for test -- std::random rolled: 11 -- atomicInt final value: 1100 std::condition_variable + std::mutex + std::unique_lock + std::random + std::thread + lambda test -- std::random rolled: 15 -- nonAtomicInt final value: 1500 std::lock_guard + std::mutex + std::random + std::thread + lambda test -- std::random rolled: 7 -- lockGuardInt final value: 700 std::unique_ptr + std::shared_ptr + std::make_shared tests -- PtrTest(): std::unique_ptr -- PtrTest(): std::shared_ptr -- ~PtrTest(): std::shared_ptr -- ~PtrTest(): std::unique_ptr
To run the tests, I used a rooted phone running Android 2.3.7.
In real deployments where you’re building a loadable shared-library using the Java Native Interface, you’ll need to follow the instructions from the NDK’s CPLUSPLUS-SUPPORT.html
file and call System.loadLibrary
on libgnustl_shared.so
before loading the eventual shared library form of your app. (Managing standalone executable lifecycles tends to be a pain in the ass inside of Android apps and services, at least based on the experiences we had at my previous company.)
Overall, I’m pretty positive about the support for concurrency in g++ 4.8 on Android, and am thrilled to see that it may now be possible to write a single modern C++ codebase across all of the major desktop and mobile platforms.