xref: /oneTBB/test/tbb/test_scheduler_mix.cpp (revision 952845af)
1 /*
2     Copyright (c) 2021 Intel Corporation
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 #define TBB_PREVIEW_WAITING_FOR_WORKERS 1
18 #define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1
19 
20 #include "common/config.h"
21 
22 #include <oneapi/tbb/task_arena.h>
23 #include <oneapi/tbb/concurrent_vector.h>
24 #include <oneapi/tbb/rw_mutex.h>
25 #include <oneapi/tbb/task_group.h>
26 #include <oneapi/tbb/parallel_for.h>
27 
28 #include <oneapi/tbb/global_control.h>
29 
30 #include "common/test.h"
31 #include "common/utils.h"
32 #include "common/utils_concurrency_limit.h"
33 #include "common/spin_barrier.h"
34 
35 #include <stdlib.h> // C11/POSIX aligned_alloc
36 #include <random>
37 
38 
39 //! \file test_scheduler_mix.cpp
40 //! \brief Test for [scheduler.task_arena scheduler.task_scheduler_observer] specification
41 
42 const std::uint64_t maxNumActions = 1 * 100 * 1000;
43 static std::atomic<std::uint64_t> globalNumActions{};
44 
45 //using Random = utils::FastRandom<>;
46 class Random {
47     struct State {
48         std::random_device rd;
49         std::mt19937 gen;
50         std::uniform_int_distribution<> dist;
51 
52         State() : gen(rd()), dist(0, std::numeric_limits<unsigned short>::max()) {}
53 
54         int get() {
55             return dist(gen);
56         }
57     };
58     static thread_local State* mState;
59     tbb::concurrent_vector<State*> mStateList;
60 public:
61     ~Random() {
62         for (auto s : mStateList) {
63             delete s;
64         }
65     }
66 
67     int get() {
68         auto& s = mState;
69         if (!s) {
70             s = new State;
71             mStateList.push_back(s);
72         }
73         return s->get();
74     }
75 };
76 
77 thread_local Random::State* Random::mState = nullptr;
78 
79 
80 void* aligned_malloc(std::size_t alignment, std::size_t size) {
81 #if _WIN32
82     return _aligned_malloc(size, alignment);
83 #elif __unix__ || __APPLE__
84     void* ptr{};
85     int res = posix_memalign(&ptr, alignment, size);
86     CHECK(res == 0);
87     return ptr;
88 #else
89     return aligned_alloc(alignment, size);
90 #endif
91 }
92 
93 void aligned_free(void* ptr) {
94 #if _WIN32
95     _aligned_free(ptr);
96 #else
97     free(ptr);
98 #endif
99 }
100 
101 template <typename T, std::size_t Alignment>
102 class PtrRWMutex {
103     static const std::size_t maxThreads = (Alignment >> 1) - 1;
104     static const std::uintptr_t READER_MASK = maxThreads;       // 7F..
105     static const std::uintptr_t LOCKED = Alignment - 1;         // FF..
106     static const std::uintptr_t LOCKED_MASK = LOCKED;           // FF..
107     static const std::uintptr_t LOCK_PENDING = READER_MASK + 1; // 80..
108 
109     std::atomic<std::uintptr_t> mState;
110 
111     T* pointer() {
112         return reinterpret_cast<T*>(state() & ~LOCKED_MASK);
113     }
114 
115     std::uintptr_t state() {
116         return mState.load(std::memory_order_relaxed);
117     }
118 
119 public:
120     class ScopedLock {
121     public:
122         constexpr ScopedLock() : mMutex(nullptr), mIsWriter(false) {}
123         //! Acquire lock on given mutex.
124         ScopedLock(PtrRWMutex& m, bool write = true) : mMutex(nullptr) {
125             CHECK_FAST(write == true);
126             acquire(m);
127         }
128         //! Release lock (if lock is held).
129         ~ScopedLock() {
130             if (mMutex) {
131                 release();
132             }
133         }
134         //! No Copy
135         ScopedLock(const ScopedLock&) = delete;
136         ScopedLock& operator=(const ScopedLock&) = delete;
137 
138         //! Acquire lock on given mutex.
139         void acquire(PtrRWMutex& m) {
140             CHECK_FAST(mMutex == nullptr);
141             mIsWriter = true;
142             mMutex = &m;
143             mMutex->lock();
144         }
145 
146         //! Try acquire lock on given mutex.
147         bool tryAcquire(PtrRWMutex& m, bool write = true) {
148             bool succeed = write ? m.tryLock() : m.tryLockShared();
149             if (succeed) {
150                 mMutex = &m;
151                 mIsWriter = write;
152             }
153             return succeed;
154         }
155 
156         void clear() {
157             CHECK_FAST(mMutex != nullptr);
158             CHECK_FAST(mIsWriter);
159             PtrRWMutex* m = mMutex;
160             mMutex = nullptr;
161             m->clear();
162         }
163 
164         //! Release lock.
165         void release() {
166             CHECK_FAST(mMutex != nullptr);
167             PtrRWMutex* m = mMutex;
168             mMutex = nullptr;
169 
170             if (mIsWriter) {
171                 m->unlock();
172             }
173             else {
174                 m->unlockShared();
175             }
176         }
177     protected:
178         PtrRWMutex* mMutex{};
179         bool mIsWriter{};
180     };
181 
182     bool trySet(T* ptr) {
183         auto p = reinterpret_cast<std::uintptr_t>(ptr);
184         CHECK_FAST((p & (Alignment - 1)) == 0);
185         if (!state()) {
186             std::uintptr_t expected = 0;
187             if (mState.compare_exchange_strong(expected, p)) {
188                 return true;
189             }
190         }
191         return false;
192     }
193 
194     void clear() {
195         CHECK_FAST((state() & LOCKED_MASK) == LOCKED);
196         mState.store(0, std::memory_order_relaxed);
197     }
198 
199     bool tryLock() {
200         auto v = state();
201         if (v == 0) {
202             return false;
203         }
204         CHECK_FAST((v & LOCKED_MASK) == LOCKED || (v & READER_MASK) < maxThreads);
205         if ((v & READER_MASK) == 0) {
206             if (mState.compare_exchange_strong(v, v | LOCKED)) {
207                 return true;
208             }
209         }
210         return false;
211     }
212 
213     bool tryLockShared() {
214         auto v = state();
215         if (v == 0) {
216             return false;
217         }
218         CHECK_FAST((v & LOCKED_MASK) == LOCKED || (v & READER_MASK) < maxThreads);
219         if ((v & LOCKED_MASK) != LOCKED && (v & LOCK_PENDING) == 0) {
220             if (mState.compare_exchange_strong(v, v + 1)) {
221                 return true;
222             }
223         }
224         return false;
225     }
226 
227     void lock() {
228         auto v = state();
229         mState.compare_exchange_strong(v, v | LOCK_PENDING);
230         while (!tryLock()) {
231             utils::yield();
232         }
233     }
234 
235     void unlock() {
236         auto v = state();
237         CHECK_FAST((v & LOCKED_MASK) == LOCKED);
238         mState.store(v & ~LOCKED, std::memory_order_release);
239     }
240 
241     void unlockShared() {
242         auto v = state();
243         CHECK_FAST((v & LOCKED_MASK) != LOCKED);
244         CHECK_FAST((v & READER_MASK) > 0);
245         mState -= 1;
246     }
247 
248     operator bool() const {
249         return pointer() != 0;
250     }
251 
252     T* get() {
253         return pointer();
254     }
255 };
256 
257 class Statistics {
258 public:
259     enum ACTION {
260         ArenaCreate,
261         ArenaDestroy,
262         ArenaAcquire,
263         skippedArenaCreate,
264         skippedArenaDestroy,
265         skippedArenaAcquire,
266         ParallelAlgorithm,
267         ArenaEnqueue,
268         ArenaExecute,
269         numActions
270     };
271 
272     static const char* const mStatNames[numActions];
273 private:
274     struct StatType {
275         StatType() : mCounters() {}
276         std::array<std::uint64_t, numActions> mCounters;
277     };
278 
279     tbb::concurrent_vector<StatType*> mStatsList;
280     static thread_local StatType* mStats;
281 
282     StatType& get() {
283         auto& s = mStats;
284         if (!s) {
285             s = new StatType;
286             mStatsList.push_back(s);
287         }
288         return *s;
289     }
290 public:
291     ~Statistics() {
292         for (auto s : mStatsList) {
293             delete s;
294         }
295     }
296 
297     void notify(ACTION a) {
298         ++get().mCounters[a];
299     }
300 
301     void report() {
302         StatType summary;
303         for (auto& s : mStatsList) {
304             for (int i = 0; i < numActions; ++i) {
305                 summary.mCounters[i] += s->mCounters[i];
306             }
307         }
308         std::cout << std::endl << "Statistics:" << std::endl;
309         std::cout << "Total actions: " << globalNumActions << std::endl;
310         for (int i = 0; i < numActions; ++i) {
311             std::cout << mStatNames[i] << ": " << summary.mCounters[i] << std::endl;
312         }
313     }
314 };
315 
316 
317 const char* const Statistics::mStatNames[Statistics::numActions] = {
318     "Arena create", "Arena destroy", "Arena acquire",
319     "Skipped arena create", "Skipped arena destroy", "Skipped arena acquire",
320     "Parallel algorithm", "Arena enqueue", "Arena execute"
321 };
322 thread_local Statistics::StatType* Statistics::mStats;
323 
324 static Statistics gStats;
325 
326 class LifetimeTracker {
327 public:
328     LifetimeTracker() = default;
329 
330     class Guard {
331     public:
332         Guard(LifetimeTracker* obj) {
333             if (!(obj->mReferences.load(std::memory_order_relaxed) & SHUTDOWN_FLAG)) {
334                 if (obj->mReferences.fetch_add(REFERENCE_FLAG) & SHUTDOWN_FLAG) {
335                     obj->mReferences.fetch_sub(REFERENCE_FLAG);
336                 } else {
337                     mObj = obj;
338                 }
339             }
340         }
341 
342         Guard(Guard&& ing) : mObj(ing.mObj) {
343             ing.mObj = nullptr;
344         }
345 
346         ~Guard() {
347             if (mObj != nullptr) {
348                 mObj->mReferences.fetch_sub(REFERENCE_FLAG);
349             }
350         }
351 
352         bool continue_execution() {
353             return mObj != nullptr;
354         }
355 
356     private:
357         LifetimeTracker* mObj{nullptr};
358     };
359 
360     Guard makeGuard() {
361         return Guard(this);
362     }
363 
364     void signalShutdown() {
365         mReferences.fetch_add(SHUTDOWN_FLAG);
366     }
367 
368     void waitCompletion() {
369         utils::SpinWaitUntilEq(mReferences, SHUTDOWN_FLAG);
370     }
371 
372 private:
373     friend class Guard;
374     static constexpr std::uintptr_t SHUTDOWN_FLAG = 1;
375     static constexpr std::uintptr_t REFERENCE_FLAG = 1 << 1;
376     std::atomic<std::uintptr_t> mReferences{};
377 };
378 
379 class ArenaTable {
380     static const std::size_t maxArenas = 64;
381     static const std::size_t maxThreads = 1 << 9;
382     static const std::size_t arenaAligment = maxThreads << 1;
383 
384     using ArenaPtrRWMutex = PtrRWMutex<tbb::task_arena, arenaAligment>;
385     std::array<ArenaPtrRWMutex, maxArenas> mArenaTable;
386 
387     struct ThreadState {
388         bool lockedArenas[maxArenas]{};
389         int arenaIdxStack[maxArenas];
390         int level{};
391     };
392 
393     LifetimeTracker mLifetimeTracker{};
394     static thread_local ThreadState mThreadState;
395 
396     template <typename F>
397     auto find_arena(std::size_t start, F f) -> decltype(f(std::declval<ArenaPtrRWMutex&>(), std::size_t{})) {
398         for (std::size_t idx = start, i = 0; i < maxArenas; ++i, idx = (idx + 1) % maxArenas) {
399             auto res = f(mArenaTable[idx], idx);
400             if (res) {
401                 return res;
402             }
403         }
404         return {};
405     }
406 
407 public:
408     using ScopedLock = ArenaPtrRWMutex::ScopedLock;
409 
410     void create(Random& rnd) {
411         auto guard = mLifetimeTracker.makeGuard();
412         if (guard.continue_execution()) {
413             int num_threads = rnd.get() % utils::get_platform_max_threads() + 1;
414             unsigned int num_reserved = rnd.get() % num_threads;
415             tbb::task_arena::priority priorities[] = { tbb::task_arena::priority::low , tbb::task_arena::priority::normal, tbb::task_arena::priority::high };
416             tbb::task_arena::priority priority = priorities[rnd.get() % 3];
417 
418             tbb::task_arena* a = new (aligned_malloc(arenaAligment, arenaAligment)) tbb::task_arena{ num_threads , num_reserved , priority };
419 
420             if (!find_arena(rnd.get() % maxArenas, [a](ArenaPtrRWMutex& arena, std::size_t) -> bool {
421                     if (arena.trySet(a)) {
422                         return true;
423                     }
424                     return false;
425                 }))
426             {
427                 gStats.notify(Statistics::skippedArenaCreate);
428                 a->~task_arena();
429                 aligned_free(a);
430             }
431         }
432     }
433 
434     void destroy(Random& rnd) {
435         auto guard = mLifetimeTracker.makeGuard();
436         if (guard.continue_execution()) {
437             auto& ts = mThreadState;
438             if (!find_arena(rnd.get() % maxArenas, [&ts](ArenaPtrRWMutex& arena, std::size_t idx) {
439                     if (!ts.lockedArenas[idx]) {
440                         ScopedLock lock;
441                         if (lock.tryAcquire(arena, true)) {
442                             auto a = arena.get();
443                             lock.clear();
444                             a->~task_arena();
445                             aligned_free(a);
446                             return true;
447                         }
448                     }
449                     return false;
450                 }))
451             {
452                 gStats.notify(Statistics::skippedArenaDestroy);
453             }
454         }
455     }
456 
457     void shutdown() {
458         mLifetimeTracker.signalShutdown();
459         mLifetimeTracker.waitCompletion();
460         find_arena(0, [](ArenaPtrRWMutex& arena, std::size_t) {
461             if (arena.get()) {
462                 ScopedLock lock{ arena, true };
463                 auto a = arena.get();
464                 lock.clear();
465                 a->~task_arena();
466                 aligned_free(a);
467             }
468             return false;
469         });
470     }
471 
472     std::pair<tbb::task_arena*, std::size_t> acquire(Random& rnd, ScopedLock& lock) {
473         auto guard = mLifetimeTracker.makeGuard();
474 
475         tbb::task_arena* a{nullptr};
476         std::size_t resIdx{};
477         if (guard.continue_execution()) {
478             auto& ts = mThreadState;
479             a = find_arena(rnd.get() % maxArenas,
480                 [&ts, &lock, &resIdx](ArenaPtrRWMutex& arena, std::size_t idx) -> tbb::task_arena* {
481                     if (!ts.lockedArenas[idx]) {
482                         if (lock.tryAcquire(arena, false)) {
483                             ts.lockedArenas[idx] = true;
484                             ts.arenaIdxStack[ts.level++] = int(idx);
485                             resIdx = idx;
486                             return arena.get();
487                         }
488                     }
489                     return nullptr;
490                 });
491             if (!a) {
492                 gStats.notify(Statistics::skippedArenaAcquire);
493             }
494         }
495         return { a, resIdx };
496     }
497 
498     void release(ScopedLock& lock) {
499         auto& ts = mThreadState;
500         CHECK_FAST(ts.level > 0);
501         auto idx = ts.arenaIdxStack[--ts.level];
502         CHECK_FAST(ts.lockedArenas[idx]);
503         ts.lockedArenas[idx] = false;
504         lock.release();
505     }
506 };
507 
508 thread_local ArenaTable::ThreadState ArenaTable::mThreadState;
509 
510 static ArenaTable arenaTable;
511 static Random threadRandom;
512 
513 enum ACTIONS {
514     arena_create,
515     arena_destroy,
516     arena_action,
517     parallel_algorithm,
518     // TODO:
519     // observer_attach,
520     // observer_detach,
521     // flow_graph,
522     // task_group,
523     // resumable_tasks,
524 
525     num_actions
526 };
527 
528 void global_actor();
529 
530 template <ACTIONS action>
531 struct actor;
532 
533 template <>
534 struct actor<arena_create> {
535     static void do_it(Random& r) {
536         arenaTable.create(r);
537     }
538 };
539 
540 template <>
541 struct actor<arena_destroy> {
542     static void do_it(Random& r) {
543         arenaTable.destroy(r);
544     }
545 };
546 
547 template <>
548 struct actor<arena_action> {
549     static void do_it(Random& r) {
550         static thread_local std::size_t arenaLevel = 0;
551         ArenaTable::ScopedLock lock;
552         auto entry = arenaTable.acquire(r, lock);
553         if (entry.first) {
554             enum arena_actions {
555                 arena_execute,
556                 arena_enqueue,
557                 num_arena_actions
558             };
559             auto process = r.get() % 2;
560             auto body = [process] {
561                 if (process) {
562                     tbb::detail::d1::wait_context wctx{ 1 };
563                     tbb::task_group_context ctx;
564                     tbb::this_task_arena::enqueue([&wctx] { wctx.release(); });
565                     tbb::detail::d1::wait(wctx, ctx);
566                 } else {
567                     global_actor();
568                 }
569             };
570             switch (r.get() % (16*num_arena_actions)) {
571             case arena_execute:
572                 if (entry.second > arenaLevel) {
573                     gStats.notify(Statistics::ArenaExecute);
574                     auto oldArenaLevel = arenaLevel;
575                     arenaLevel = entry.second;
576                     entry.first->execute(body);
577                     arenaLevel = oldArenaLevel;
578                     break;
579                 }
580                 utils_fallthrough;
581             case arena_enqueue:
582                 utils_fallthrough;
583             default:
584                 gStats.notify(Statistics::ArenaEnqueue);
585                 entry.first->enqueue([] { global_actor(); });
586                 break;
587             }
588             arenaTable.release(lock);
589         }
590     }
591 };
592 
593 template <>
594 struct actor<parallel_algorithm> {
595     static void do_it(Random& rnd) {
596         enum PARTITIONERS {
597             simpl_part,
598             auto_part,
599             aff_part,
600             static_part,
601             num_parts
602         };
603         int sz = rnd.get() % 10000;
604         auto doGlbAction = rnd.get() % 1000 == 42;
605         auto body = [doGlbAction, sz](int i) {
606             if (i == sz / 2 && doGlbAction) {
607                 global_actor();
608             }
609         };
610 
611         switch (rnd.get() % num_parts) {
612         case simpl_part:
613             tbb::parallel_for(0, sz, body, tbb::simple_partitioner{}); break;
614         case auto_part:
615             tbb::parallel_for(0, sz, body, tbb::auto_partitioner{}); break;
616         case aff_part:
617         {
618             tbb::affinity_partitioner aff;
619             tbb::parallel_for(0, sz, body, aff); break;
620         }
621         case static_part:
622             tbb::parallel_for(0, sz, body, tbb::static_partitioner{}); break;
623         }
624     }
625 };
626 
627 void global_actor() {
628     static thread_local std::uint64_t localNumActions{};
629 
630     while (globalNumActions < maxNumActions) {
631         auto& rnd = threadRandom;
632         switch (rnd.get() % num_actions) {
633         case arena_create:  gStats.notify(Statistics::ArenaCreate); actor<arena_create>::do_it(rnd);  break;
634         case arena_destroy: gStats.notify(Statistics::ArenaDestroy); actor<arena_destroy>::do_it(rnd); break;
635         case arena_action:  gStats.notify(Statistics::ArenaAcquire); actor<arena_action>::do_it(rnd);  break;
636         case parallel_algorithm: gStats.notify(Statistics::ParallelAlgorithm); actor<parallel_algorithm>::do_it(rnd);  break;
637         }
638 
639         if (++localNumActions == 100) {
640             localNumActions = 0;
641             globalNumActions += 100;
642 
643             // TODO: Enable statistics
644             // static std::mutex mutex;
645             // std::lock_guard<std::mutex> lock{ mutex };
646             // std::cout << globalNumActions << "\r" << std::flush;
647         }
648     }
649 }
650 
651 #if TBB_USE_EXCEPTIONS
652 //! \brief \ref stress
653 TEST_CASE("Stress test with mixing functionality") {
654     // TODO add thread recreation
655     // TODO: Enable statistics
656     // tbb::task_scheduler_handle handle = tbb::task_scheduler_handle::get();
657 
658     const std::size_t numExtraThreads = 16;
659     utils::SpinBarrier startBarrier{numExtraThreads};
660     utils::NativeParallelFor(numExtraThreads, [&startBarrier](std::size_t) {
661         startBarrier.wait();
662         global_actor();
663     });
664 
665     arenaTable.shutdown();
666 
667     // tbb::finalize(handle, std::nothrow_t{});
668 
669     // gStats.report();
670 }
671 #endif
672