Nabil
08/09/2019, 8:44 AMLong
so it can be passed later to make some invocations via the C-interop (the Long
value is reinterpret_cast<CppObj*>(pointer)
to whatever the original C/C++ object is), usually we want to deallocate explicitly this pointer when the object get GC’ed via the reference counter.
I’m aware of the existing memory management approaches (memScope/arena etc.) but in this scenario we can not wrap every instance of the Kotlin class inside a memScope/arena, these are instances passed to the user of the API so we don’t control their lifecycles, this is why deallocating the C++ resources should be tied to the collection of the original Kotlin instance (to draw a parallel, this is the pattern used in Java with Phantom reference & a referenceQueue to close native pointers, see CrazyBob presentation Arkadii Ivanov
08/09/2019, 8:58 AMArtyom Degtyarev [JB]
08/09/2019, 9:16 AMArkadii Ivanov
08/09/2019, 9:27 AMArkadii Ivanov
08/09/2019, 9:47 AMsvyatoslav.scherbina
08/09/2019, 11:06 AM.destroy()
should make all subsequent resource usage attempts fail in a predictable explicit way.
If you actually have multiple independent users of the resource in your code, then consider implementing manual-RC-style management for the resource.
I really would like some sort of deinit section.Speaking of how
deinit
section helps to make your code more safe.
Getting segmentation fault in Swift:
var topLevel: Any? = nil
class Test {
deinit {
topLevel = self
}
}
Test()
print(topLevel)
Pure Swift, without native resources or even “unsafe” language concepts like !
.Arkadii Ivanov
08/09/2019, 11:13 AMArkadii Ivanov
08/09/2019, 11:14 AMNabil
08/09/2019, 11:22 AMWeakReference
to build an equivalent of a “ReferenceQueue”, which will be populated by a weak ref to newly created objects, then regularly traverse this queue to check for GC’ed element (by calling instance.ref.get() == null
we can then safely close the native resource associated with it. However this requires accessing/mutating this queue in a worker thread which is not possible I think with the threading model of K/N?svyatoslav.scherbina
08/09/2019, 11:22 AMSince you can’t call destroy while someone is inside mutex (acquired it).Exactly. So with manual RC approach you can make “someone” a resource user too, incrementing the RC before acquiring the mutex and decrementing after releasing the mutex.
Your example is so weirdI believe that it is possible to make an example which will not be “so weird” but will still be unsafe.
deinit
-like approach is fundamentally dangerous and broken, that’s why I’m trying to demonstrate.
Then remove !! please, because someone can use it incorrectly.There is a huge difference between
!!
(which can throw easily findable error with obvious immediate cause) and segmentation fault caused by the unobvious and hardly discoverable fact that you can’t use some operations in deinit
.svyatoslav.scherbina
08/09/2019, 11:28 AMI think we all agree that the usage of finalizers could be problematic (these are the same issues present in Java - see Effective Java item 7) hence the presence of the Reference API that solves the aforementioned use case gracefully.Reference-like API is somewhat more safe than Swift-like
deinit
.
However implementing such an API has an impact to memory manager, and that of Kotlin/Native is still being actively developed and heavily reworked quite often. So implementing reference-like API now would be harmful at least to the evolution of Kotlin/Native memory manager and concurrency model.Arkadii Ivanov
08/09/2019, 11:30 AMArkadii Ivanov
08/09/2019, 11:31 AMNabil
08/09/2019, 11:34 AMsvyatoslav.scherbina
08/09/2019, 11:39 AMSo is not it a good idea to provide a mechanism to automatically release allocated resources at the end of life cycle (without client interaction)?This is disputable. See e.g. the links above.
So you will have less segmentation faults due to inappropriate usage of your class?If you get segmentation faults due to inappropriate usage of your class, then your class is probably implemented wrong since it fails to properly abstract an unsafe resource.
svyatoslav.scherbina
08/09/2019, 11:40 AMOne workaround I thought about could be to use the the introducedIf you resources are shared in terms of K/N threading model (e.g. native handles or frozen wrappers for them), then you can implement a shared queue and process it in the worker.to build an equivalent of a “ReferenceQueue”, which will be populated by a weak ref to newly created objects, then regularly traverse this queue to check for GC’ed element (by callingWeakReference
we can then safely close the native resource associated with it. However this requires accessing/mutating this queue in a worker thread which is not possible I think with the threading model of K/N?instance.ref.get() == null
Nabil
08/09/2019, 11:42 AMsvyatoslav.scherbina
08/09/2019, 11:47 AMAtomicReference
.Arkadii Ivanov
08/09/2019, 12:00 PMclass MyRepository {
private val arena = Arena()
private val mutex = arena.alloc<pthread_mutex_t>()
init {
pthread_mutex_init(mutex.ptr, null)
}
fun destroy() {
arena.clear()
}
// Can be called from different threads concurrently
fun put(value: String) {
pthread_mutex_lock(mutex.ptr)
try {
// Put the value somewhere
} finally {
pthread_mutex_unlock(mutex.ptr)
}
}
}
svyatoslav.scherbina
08/09/2019, 12:24 PMclass MyRepository {
private val arena = Arena()
private val mutex = arena.alloc<pthread_mutex_t>()
private val refCount = AtomicInt(1)
private val destroyed = AtomicInt(0)
private fun decrementRefCount() {
if (refCount.addAndGet(-1) == 0) {
arena.clear()
}
}
init {
pthread_mutex_init(mutex.ptr, null)
}
fun destroy() {
if (!destroyed.compareAndSet(0, 1)) error("Already destroyed")
decrementRefCount()
}
// Can be called from different threads concurrently
fun put(value: String) {
do {
val currentRefCount = refCount.value
if (currentRefCount <= 0) error("Already destroyed")
} while (!refCount.compareAndSet(currentRefCount, currentRefCount + 1))
pthread_mutex_lock(mutex.ptr)
try {
// Put the value somewhere
} finally {
pthread_mutex_unlock(mutex.ptr)
decrementRefCount()
}
}
}
Have not tested. May vary depending on what behaviour you need for destroy-while-in-use etc.
Btw, your code lacks pthread_mutex_destroy
.Arkadii Ivanov
08/09/2019, 12:47 PMput
method it will possibly release the resources. At the same time another thread may execute put
and will crash with Already disposed
error. But I did something similar, there were two additional methods like incRefCount
and decRefCount
and they were called from client side. That's why I dislike it, it should not be clients' responsibility.svyatoslav.scherbina
08/09/2019, 12:51 PMhere are race conditions in the provided code, if one thread leavesThis can happen only after someone calledmethod it will possibly release the resources. At the same time another thread may executeput
and will crash withput
errorAlready disposed
destroy
. Throwing an error if the resource is used after destroy is not a race condition, it is the correct behaviour in this case. That’s what manual resource management is.
Also your original complain was about segmentation faults. Can this code cause a segmentation fault?
But I did something similar, there were two additional methods likeWell, in my approach the clients aren’t responsible for reference counting here.andincRefCount
and they were called from client side. That’s why I dislike it, it should not be clients’ responsibility.decRefCount