Skip to content

Commit

Permalink
job system optimizations - removed global lock
Browse files Browse the repository at this point in the history
  • Loading branch information
nem0 committed Oct 14, 2024
1 parent d199fff commit dfba690
Show file tree
Hide file tree
Showing 12 changed files with 399 additions and 410 deletions.
63 changes: 35 additions & 28 deletions docs/job_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,50 +29,57 @@ jobs::run(&data, sum, nullptr);
## Signal
**Signals** are one of the synchronization primitives of the Job System. They can be used to wait for two types of events:
**Signals** are the primary synchronization primitive in the Job System. A signal can be either **red** or **green**. A red signal blocks all `wait()` callers until it turns green. The four basic operations on signals are:
* `jobs::turnRed` - Turns the signal red. This can be called at any time and is a no-op if the signal is already red.
* `jobs::turnGreen` - Turns the signal green, scheduling all waiting fibers to execute. This can be called at any time and is a no-op if the signal is already green.
* `jobs::wait` - Waits until the signal turns green. If the signal is already green, it continues. While waiting, the job system runs other jobs.
* `jobs::waitAndTurnRed` - Waits until the signal turns green and then atomically turns it red. If the signal is already green, it is equivalent to `jobs::turnRed`. If multiple fibers are `waitAndTurnRed`-ing on the same signal and it turns green, only one fiber proceeds with execution.
While **waiting** on a signal, a **worker thread** will attempt to **execute another job** from the queue. If no jobs are available, the thread will go to sleep to conserve resources. Signals are not copyable. It means the following code is invalid:
```cpp
jobs::Signal other_signal = signal;
```

### Counters

Counters are a specialized type of signal that maintain a numeric value. They are considered green when this value is zero and red when it is non-zero. Counters are used to wait until one or more jobs are finished. There are two operations on counters:

* `jobs::run` - can increment value of a provided counter.
* `jobs::wait` - waits until the signal turns green.

### Job finished
```cpp
jobs::Signal signal;
jobs::Counter counter;
// run some job
jobs::run(data, function, &signal);
jobs::run(data, function, &counter);

// wait till the job is finished
jobs::wait(&signal);
// no need to cleanup `signal`
jobs::wait(&counter);
// no need to cleanup `counter`
```
While **waiting** on a signal, a **worker thread** will attempt to **execute another job** from the queue. If no jobs are available, the thread will go to sleep to conserve resources. The Job System retains pointers to signals until the associated jobs are completed. It is the **user's responsibility** to ensure that the **signal remains valid** until the job is finished. Consequently, signals **cannot be assigned** to other signals, making the following code invalid:
The Job System retains pointers to counters until the associated jobs are completed. It is the **user's responsibility** to ensure that the **counter remains valid** until the job is finished. Consequently, counters **cannot be assigned** to other counter, making the following code invalid:
```cpp
jobs::Signal other_signal = signal; // invalid
jobs::Counter other_counter = counter; // invalid
void foo() {
jobs::Signal signal;
jobs::run(&data, fn, &signal);
// `signal` gets destroyed here, but there's no guarantee the job is finished, so this is invalid
jobs::Counter counter;
jobs::run(&data, fn, &counter);
// `counter` gets destroyed here, but there's no guarantee the job is finished, so this is invalid
}
```

Multiple jobs can use the same signal:
Multiple jobs can use the same counter:

```cpp
jobs::Signal signal;
jobs::Counter counter;
// run two jobs
jobs::run(&dataA, fnA, &signal);
jobs::run(&dataB, fnB, &signal);
jobs::run(&dataA, fnA, &counter);
jobs::run(&dataB, fnB, &counter);
// wait till both jobs are finished
jobs::wait(&signal);
```
### User triggered signals
Signals can also be used to trigger events based on user requests. It is **strongly advised not to mix** user-triggered signals with signals used as job counters. User-triggered signal can be in one of two states: green or red. Waiting on **green** signal is a **does not block**. Waiting on **red signal blocks** the caller until the signal turns green.
```cpp
jobs::Signal is_ready; // signal is green by default
jobs::setRed(&is_ready); // signal is red, any called of wait(&is_ready) is blocked
...
jobs::setRed(&is_ready); // this unblocks anybody waiting on `is_ready`
// from this point, wait(&is_ready) does not block the callers
jobs::wait(&counter);
```
## Mutex
Expand Down
6 changes: 5 additions & 1 deletion src/core/atomic.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@ struct LUMIX_CORE_API AtomicI64 {
i64 dec();
i64 add(i64 v);
i64 subtract(i64 v);
i64 exchange(i64 new_value);
i64 setBits(i64 v);
i64 clearBits(i64 v);
bool bitTestAndSet(u32 bit_position);

bool compareExchange(i64 exchange, i64 comperand);
private:
volatile i64 value;
};

LUMIX_CORE_API void* exchangePtr(void* volatile* value, void* exchange);
LUMIX_CORE_API bool compareExchangePtr(void*volatile* value, void* exchange, void* comperand);
LUMIX_CORE_API void memoryBarrier();
LUMIX_CORE_API void cpuRelax(); // rep nop, can be used in busy wait to conserve resources

} // namespace Lumix
Loading

0 comments on commit dfba690

Please sign in to comment.