HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
withScopedParallelism.h
Go to the documentation of this file.
1 //
2 // Copyright 2021 Pixar
3 //
4 // Licensed under the terms set forth in the LICENSE.txt file available at
5 // https://openusd.org/license.
6 //
7 #ifndef PXR_BASE_WORK_WITH_SCOPED_PARALLELISM_H
8 #define PXR_BASE_WORK_WITH_SCOPED_PARALLELISM_H
9 
10 ///\file work/withScopedParallelism.h
11 
12 #include "pxr/pxr.h"
13 #include "pxr/base/work/api.h"
15 #include "pxr/base/tf/pyLock.h"
16 
17 #include <tbb/task_arena.h>
18 
19 #include <utility>
20 
22 
23 /// Invoke \p fn, ensuring that all wait operations on concurrent constructs
24 /// invoked by the calling thread only take tasks created within the scope of \p
25 /// fn 's execution.
26 ///
27 /// Ordinarily when a thread invokes a wait operation on a concurrent construct
28 /// (e.g. the explicit WorkDispatcher::Wait(), or the implicit wait in loops
29 /// like WorkParallelForEach()) it joins the pool of worker threads and executes
30 /// tasks to help complete the work. This is good, since the calling thread
31 /// does useful work instead of busy waiting or sleeping until the work has
32 /// completed. However, this can be problematic depending on the calling
33 /// context, and which tasks the waiting thread executes.
34 ///
35 /// For example, consider the following example: a demand-populated resource
36 /// cache.
37 ///
38 /// \code
39 /// ResourceHandle
40 /// GetResource(ResourceKey key) {
41 /// // Attempt to lookup/insert an entry for \p key. If we insert the
42 /// // element, then populate the resource.
43 /// ResourceAccessorAndLock accessor;
44 /// if (_resources.FindOrCreate(key, &accessor)) {
45 /// // No previous entry, so populate the resource.
46 /// WorkDispatcher wd;
47 /// wd.Run( /* resource population task 1 */);
48 /// wd.Run( /* resource population task 2 */);
49 /// wd.Run( /* resource population task 3 */);
50 /// WorkParallelForN( /* parallel population code */);
51 /// wd.Wait();
52 /// /* Store resource data. */
53 /// }
54 /// return *accessor.first;
55 /// }
56 /// \endcode
57 ///
58 /// Here when a caller has requested the resource for \p key for the first time,
59 /// we do the work to populate the resource while holding a lock on that
60 /// resource entry in the cache. The problem is that when the calling thread
61 /// waits for work to complete, if it picks up tasks unrelated to this context
62 /// and those tasks attempt to call GetResource() with the same key, the process
63 /// will deadlock.
64 ///
65 /// This can be fixed by using WorkWithScopedParallelism() to ensure that the
66 /// calling thread's wait operations only take tasks that were created during
67 /// the scope of the population work:
68 ///
69 /// \code
70 /// ResourceHandle
71 /// GetResource(ResourceKey key) {
72 /// // Attempt to lookup/insert an entry for \p key. If we insert the
73 /// // element, then populate the resource.
74 /// ResourceAccessorAndLock accessor;
75 /// if (_resources.FindOrCreate(key, &accessor)) {
76 /// // No previous entry, so populate the resource.
77 /// WorkWithScopedParallelism([&accessor]() {
78 /// WorkDispatcher wd;
79 /// wd.Run( /* resource population task 1 */);
80 /// wd.Run( /* resource population task 2 */);
81 /// wd.Run( /* resource population task 3 */);
82 /// WorkParallelForN( /* parallel population code */);
83 /// }
84 /// /* Store resource data. */
85 /// }
86 /// return *accessor.first;
87 /// }
88 /// \endcode
89 ///
90 /// This limits parallelism by only a small degree. It's only the waiting
91 /// thread that restricts the tasks it can take to the protected scope: all
92 /// other worker threads continue unhindered.
93 ///
94 /// If Python support is enabled and \p dropPythonGIL is true, this function
95 /// ensures the GIL is released before invoking \p fn. If this function
96 /// released the GIL, it reacquires it before returning.
97 ///
98 template <class Fn>
99 auto
100 WorkWithScopedParallelism(Fn &&fn, bool dropPythonGIL=true)
101 {
102  if (dropPythonGIL) {
104  return tbb::this_task_arena::isolate(std::forward<Fn>(fn));
105  }
106  else {
107  return tbb::this_task_arena::isolate(std::forward<Fn>(fn));
108  }
109 }
110 
111 /// Similar to WorkWithScopedParallelism(), but pass a WorkDispatcher instance
112 /// to \p fn for its use during the scoped parallelism. Accordingly, \p fn must
113 /// accept a WorkDispatcher lvalue reference argument. After \p fn returns but
114 /// before the scoped parallelism ends, call WorkDispatcher::Wait() on the
115 /// dispatcher instance. The \p dropPythonGIL argument has the same meaning as
116 /// it does for WorkWithScopedParallelism().
117 template <class Fn>
118 auto
119 WorkWithScopedDispatcher(Fn &&fn, bool dropPythonGIL=true)
120 {
121  return WorkWithScopedParallelism([&fn]() {
122  WorkDispatcher dispatcher;
123  return std::forward<Fn>(fn)(dispatcher);
124  // dispatcher's destructor invokes Wait() here.
125  }, dropPythonGIL);
126 }
127 
129 
130 #endif // PXR_BASE_WORK_WITH_SCOPED_PARALLELISM_H
131 
PXR_NAMESPACE_OPEN_SCOPE auto WorkWithScopedParallelism(Fn &&fn, bool dropPythonGIL=true)
auto WorkWithScopedDispatcher(Fn &&fn, bool dropPythonGIL=true)
#define TF_PY_ALLOW_THREADS_IN_SCOPE()
Definition: pyLock.h:181
PXR_NAMESPACE_CLOSE_SCOPE PXR_NAMESPACE_OPEN_SCOPE
Definition: path.h:1425
#define PXR_NAMESPACE_CLOSE_SCOPE
Definition: pxr.h:74