Tasks, actors and garbage collection
Petar
Petar
Tue Apr 20 16:21:39 UTC 2021
On Tuesday, 20 April 2021 at 09:52:07 UTC, Ola Fosheim Grøstad
wrote:
> As computer memory grows, naive scan and sweep garbage
> collection becomes more and more a burden.
>
> Also, languages have not really come up with a satisfactory way
> to simplify multi-threaded programming, except to split the
> workload into many single-threaded tasks that are run in
> parallel.
>
> It seems to me that the obvious way to retain the easy of use
> that garbage collection provides without impeding performance
> is to limit the memory to scan, and preferably do the scanning
> when nobody is using the memory.
>
> The actor model seems to be a good fit. Or call it a task, if
> you wish. If each actor/task has it's own GC pool then there is
> less memory to scan, and you can do the scanning when the
> actor/task is waiting on I/O or scheduling. So you would get
> less intrusive scanning pauses. It would also fit well with
> async-await/futures.
>
> Another benefit is that if an actor is deleted before it is
> scanned, then no scanning is necessary at all. It can simply be
> released (assuming destructor-free classes are allocated in a
> separate area). This is of great benefit to web-services, they
> can simply implement a request-handler as an actor/task.
>
> The downside is that you need a non-GC mechanism for dealing
> with inter-actor/task communication. Such as reference
> counting, however that should be quite ok, as you would expect
> the time-consuming stuff to happen within an actor/task as well
> as complex allocation patterns.
>
> Is this a direction D is able to move in or is a new language
> needed?
A few years ago, when [`std.experimental.allocator`][0] was still
hot out of the oven, I considered that this would one of primary
innovations that it would enable.
The basic idea is that since allocators are composable
first-class objects, you can pass them to any function and that
way you can override and customize its memory allocation policy,
without resorting to global variables.
(The package does provide convenience [thread-local][1] and
[global variables][2], but IMO that's an anti-pattern, as if you
prefer the simplicity, you can either use the GC (as always), or
`MAllocator` directly. IMO, if you're reaching for
`std.experimental.allocator`, you do so, in order to gain more
control over the memory management. Also knowing whether
`theAllocator` points to `GCAllocator`, or an actually separate
thread-local allocator, can be critical for ensuring that code is
lock-free. You either know what you're doing, or the code is not
performance critical, so it doesn't matter, and you should be
using the GC anyway.)
By passing the allocator as an object, you allow it to be used
safely from `pure` functions. (If `pure` functions were to
somehow be allowed to use those global allocator variables, you
could have some ugly consequences. For example, a pure function
can be preempted in the middle of its execution, only to have the
global allocator replaced under its feet, thereby leaving all the
memory allocated from the previous allocator dangling.)
Pure code (even in the relaxed D sense) is great for parallelism,
as a scheduler can essentially assume that it's both lock-free
and wait-free - it doesn't need to interact with any other
thread/fiber/task to make progress.
Having multiple per thread/fiber/actor/task GC heaps fits
naturally in the model you propose. There could be a new
LocalGCAllocator, which the runtime / framework can simply pass
to the actor on its creation. There two main challenges:
1. Ensuring code doesn't brake the assumptions of the actor model
by e.g. sharing memory between threads in an uncontrolled manner.
This can be addressed in a variety of ways:
* The framework's build-system can prevent you from importing
code that doesn't fit its model
* The framework can run a non-optional linter as part of the
build process, which would ensure that you don't have:
* `@system` or `@trusted` code
* `extern` function declarations (otherwise you could
define `@safe pure int printf(scope const char* format, scope
const ...);`)
* reference capabilities like [Pony][3]'s
* other type-system or language built-in static analysis
2. Making it ergonomic and easy to use, as is using the GC.
Essentially having all language and library features that
currently require the GC use `LocalGCAllocator` automagically.
I think this can be done in several steps:
* Finish transitioning druntime's compiler interface from
unchecked "magic" extern(C) functions to regular D (template)
functions
* Add `context` as the last parameter to each of druntime
function that may need to allocate memory set it's default value
to the global GC context. This is a pure refactoring, no change
in behavior.
* Add Scala `implicit` parameters [⁴][4] [⁵][5] [⁶][6] [⁷][7]
[⁸][8] to the language and mark the `context` parameters as
`implicit`
[0]: https://dlang.org/phobos/std_experimental_allocator.html
[1]:
https://dlang.org/phobos/std_experimental_allocator.html#theAllocator
[2]:
https://dlang.org/phobos/std_experimental_allocator.html#.processAllocator
[3]:
https://tutorial.ponylang.io/reference-capabilities/reference-capabilities.html
[4]:
https://scala-lang.org/files/archive/spec/2.13/07-implicits.html#implicit-parameters
[5]: https://docs.scala-lang.org/tour/implicit-parameters.html
[6]:
https://docs.scala-lang.org/tutorials/FAQ/finding-implicits.html
[7]:
https://stackoverflow.com/questions/10375633/understanding-implicit-in-scala
[8]: https://dzone.com/articles/scala-implicits-presentations
More information about the Digitalmars-d
mailing list