1 module hunt.concurrency.TaskPool; 2 3 import hunt.concurrency.SimpleQueue; 4 import hunt.logging.ConsoleLogger; 5 import hunt.system.Memory; 6 import hunt.util.Common; 7 8 import core.thread; 9 import core.atomic; 10 import core.sync.condition; 11 import core.sync.mutex; 12 13 import std.traits; 14 15 private enum TaskStatus : ubyte { 16 ready, 17 processing, 18 done 19 } 20 21 /* Atomics code. These forward to core.atomic, but are written like this 22 for two reasons: 23 24 1. They used to actually contain ASM code and I don' want to have to change 25 to directly calling core.atomic in a zillion different places. 26 27 2. core.atomic has some misc. issues that make my use cases difficult 28 without wrapping it. If I didn't wrap it, casts would be required 29 basically everywhere. 30 */ 31 private void atomicSetUbyte(T)(ref T stuff, T newVal) 32 if (__traits(isIntegral, T) && is(T : ubyte)) { 33 //core.atomic.cas(cast(shared) &stuff, stuff, newVal); 34 atomicStore(*(cast(shared)&stuff), newVal); 35 } 36 37 private ubyte atomicReadUbyte(T)(ref T val) 38 if (__traits(isIntegral, T) && is(T : ubyte)) { 39 return atomicLoad(*(cast(shared)&val)); 40 } 41 42 // This gets rid of the need for a lot of annoying casts in other parts of the 43 // code, when enums are involved. 44 private bool atomicCasUbyte(T)(ref T stuff, T testVal, T newVal) 45 if (__traits(isIntegral, T) && is(T : ubyte)) { 46 return core.atomic.cas(cast(shared)&stuff, testVal, newVal); 47 } 48 49 50 /** 51 * 52 */ 53 class AbstractTask : Runnable { 54 55 Throwable exception; 56 ubyte taskStatus = TaskStatus.ready; 57 58 final void run() { 59 atomicSetUbyte(taskStatus, TaskStatus.processing); 60 try { 61 onRun(); 62 } catch (Throwable e) { 63 exception = e; 64 debug warning(e.msg); 65 } 66 67 atomicSetUbyte(taskStatus, TaskStatus.done); 68 } 69 70 abstract protected void onRun(); 71 72 bool done() @property { 73 if (atomicReadUbyte(taskStatus) == TaskStatus.done) { 74 if (exception) { 75 throw exception; 76 } 77 return true; 78 } 79 return false; 80 } 81 } 82 83 /** 84 */ 85 class Task(alias fun, Args...) : AbstractTask { 86 Args _args; 87 88 static if (Args.length > 0) { 89 this(Args args) { 90 _args = args; 91 } 92 } else { 93 this() { 94 } 95 } 96 97 /** 98 The return type of the function called by this `Task`. This can be 99 `void`. 100 */ 101 alias ReturnType = typeof(fun(_args)); 102 103 static if (!is(ReturnType == void)) { 104 static if (is(typeof(&fun(_args)))) { 105 // Ref return. 106 ReturnType* returnVal; 107 108 ref ReturnType fixRef(ReturnType* val) { 109 return *val; 110 } 111 112 } else { 113 ReturnType returnVal; 114 115 ref ReturnType fixRef(ref ReturnType val) { 116 return val; 117 } 118 } 119 } 120 121 private static void impl(AbstractTask myTask) { 122 auto myCastedTask = cast(typeof(this)) myTask; 123 static if (is(ReturnType == void)) { 124 fun(myCastedTask._args); 125 } else static if (is(typeof(addressOf(fun(myCastedTask._args))))) { 126 myCastedTask.returnVal = addressOf(fun(myCastedTask._args)); 127 } else { 128 myCastedTask.returnVal = fun(myCastedTask._args); 129 } 130 } 131 132 protected override void onRun() { 133 impl(this); 134 } 135 } 136 137 T* addressOf(T)(ref T val) { 138 return &val; 139 } 140 141 auto makeTask(alias fun, Args...)(Args args) { 142 return new Task!(fun, Args)(args); 143 } 144 145 auto makeTask(F, Args...)(F delegateOrFp, Args args) 146 if (is(typeof(delegateOrFp(args)))) // && !isSafeTask!F 147 { 148 return new Task!(run, F, Args)(delegateOrFp, args); 149 } 150 151 // Calls `fpOrDelegate` with `args`. This is an 152 // adapter that makes `Task` work with delegates, function pointers and 153 // functors instead of just aliases. 154 ReturnType!F run(F, Args...)(F fpOrDelegate, ref Args args) { 155 return fpOrDelegate(args); 156 } 157 158 /* 159 This class serves two purposes: 160 161 1. It distinguishes std.parallelism threads from other threads so that 162 the std.parallelism daemon threads can be terminated. 163 164 2. It adds a reference to the pool that the thread is a member of, 165 which is also necessary to allow the daemon threads to be properly 166 terminated. 167 */ 168 final class ParallelismThread : Thread { 169 this(void delegate() dg) { 170 super(dg); 171 taskQueue = new NonBlockingQueue!(AbstractTask)(); 172 } 173 174 TaskPool pool; 175 NonBlockingQueue!(AbstractTask) taskQueue; 176 } 177 178 /** 179 */ 180 enum PoolState : ubyte { 181 running, 182 finishing, 183 stopNow 184 } 185 186 /** 187 */ 188 class TaskPool { 189 190 private ParallelismThread[] pool; 191 private PoolState status = PoolState.running; 192 193 // The instanceStartIndex of the next instance that will be created. 194 // __gshared size_t nextInstanceIndex = 1; 195 196 // The index of the first thread in this instance. 197 // immutable size_t instanceStartIndex; 198 199 // The index of the current thread. 200 static size_t threadIndex; 201 202 // The index that the next thread to be initialized in this pool will have. 203 shared size_t nextThreadIndex; 204 205 Condition workerCondition; 206 Condition waiterCondition; 207 Mutex queueMutex; 208 Mutex waiterMutex; // For waiterCondition 209 210 bool isSingleTask = false; 211 212 /** 213 Default constructor that initializes a `TaskPool` with 214 `totalCPUs` - 1 worker threads. The minus 1 is included because the 215 main thread will also be available to do work. 216 217 Note: On single-core machines, the primitives provided by `TaskPool` 218 operate transparently in single-threaded mode. 219 */ 220 this() { 221 this(totalCPUs - 1); 222 } 223 224 /** 225 Allows for custom number of worker threads. 226 */ 227 this(size_t nWorkers) { 228 if (nWorkers == 0) 229 nWorkers = 1; 230 231 queueMutex = new Mutex(this); 232 waiterMutex = new Mutex(); 233 workerCondition = new Condition(queueMutex); 234 waiterCondition = new Condition(waiterMutex); 235 nextThreadIndex = 0; 236 237 pool = new ParallelismThread[nWorkers]; 238 foreach (ref poolThread; pool) { 239 poolThread = new ParallelismThread(&startWorkLoop); 240 poolThread.pool = this; 241 poolThread.start(); 242 } 243 } 244 245 bool isDaemon() @property @trusted { 246 return pool[0].isDaemon; 247 } 248 249 /// Ditto 250 void isDaemon(bool newVal) @property @trusted { 251 foreach (thread; pool) { 252 thread.isDaemon = newVal; 253 } 254 } 255 256 // This function performs initialization for each thread that affects 257 // thread local storage and therefore must be done from within the 258 // worker thread. It then calls executeWorkLoop(). 259 private void startWorkLoop() { 260 // Initialize thread index. 261 size_t index = atomicOp!("+=")(nextThreadIndex, 1); 262 threadIndex = index - 1; 263 264 executeWorkLoop(); 265 } 266 267 // This is the main work loop that worker threads spend their time in 268 // until they terminate. It's also entered by non-worker threads when 269 // finish() is called with the blocking variable set to true. 270 private void executeWorkLoop() { 271 while (atomicReadUbyte(status) != PoolState.stopNow) { 272 AbstractTask task = pool[threadIndex].taskQueue.dequeue(); 273 if (task is null) { 274 if (atomicReadUbyte(status) == PoolState.finishing) { 275 atomicSetUbyte(status, PoolState.stopNow); 276 return; 277 } 278 } else { 279 doJob(task); 280 } 281 } 282 } 283 284 private void doJob(AbstractTask job) { 285 // assert(job.taskStatus == TaskStatus.processing); 286 287 // scope (exit) { 288 // // if (!isSingleTask) 289 // { 290 // waiterLock(); 291 // scope (exit) 292 // waiterUnlock(); 293 // notifyWaiters(); 294 // } 295 // } 296 job.run(); 297 } 298 299 private void waiterLock() { 300 if (!isSingleTask) 301 waiterMutex.lock(); 302 } 303 304 private void waiterUnlock() { 305 if (!isSingleTask) 306 waiterMutex.unlock(); 307 } 308 309 private void wait() { 310 if (!isSingleTask) 311 workerCondition.wait(); 312 } 313 314 private void notify() { 315 if (!isSingleTask) 316 workerCondition.notify(); 317 } 318 319 private void notifyAll() { 320 if (!isSingleTask) 321 workerCondition.notifyAll(); 322 } 323 324 private void waitUntilCompletion() { 325 waiterCondition.wait(); 326 } 327 328 private void notifyWaiters() { 329 if (!isSingleTask) 330 waiterCondition.notifyAll(); 331 } 332 333 void stop() @trusted { 334 // queueLock(); 335 // scope(exit) queueUnlock(); 336 atomicSetUbyte(status, PoolState.stopNow); 337 notifyAll(); 338 } 339 340 void finish(bool blocking = false) @trusted { 341 { 342 // queueLock(); 343 // scope(exit) queueUnlock(); 344 atomicCasUbyte(status, PoolState.running, PoolState.finishing); 345 notifyAll(); 346 } 347 if (blocking) { 348 // Use this thread as a worker until everything is finished. 349 // stopWorkLoop(); 350 // taskQueue.wakeup(); 351 executeWorkLoop(); 352 353 foreach (t; pool) { 354 // Maybe there should be something here to prevent a thread 355 // from calling join() on itself if this function is called 356 // from a worker thread in the same pool, but: 357 // 358 // 1. Using an if statement to skip join() would result in 359 // finish() returning without all tasks being finished. 360 // 361 // 2. If an exception were thrown, it would bubble up to the 362 // Task from which finish() was called and likely be 363 // swallowed. 364 t.join(); 365 } 366 } 367 } 368 369 void put(int factor, AbstractTask task) { 370 int nWorkers = cast(int)pool.length; 371 if(factor<0) factor = -factor; 372 int i = factor % nWorkers; 373 // tracef("factor=%d, index=%d", factor, i); 374 pool[i].taskQueue.enqueue(task); 375 } 376 }