Jump to content

About This File

Nosis' Locks: Arbitrary Read/Write Locks

"Protects your code against CTDs better than Trojan protects against STDs"

v0.3.0

 

Description

NosLocks gives you an generic read/write locking mechanism to use as you see fit. It abstracts out the GotoState() paradigm of Papyrus effectively allowing a single script to have multiple states at the same time (via semaphore barriers). This library provides classic read/write locking capabilities: many readers and zero writers, or one writer and zero readers.

 

 

WARNING

Semaphore barriers are dangerous if you aren't careful with them. Only you can prevent dead-locks by avoiding modding while intoxicated!

 

 

BETA Notice (No Longer a Warning)

v0.3.0 is a mid-beta release. It's barely been tested under real world conditions but has been heavily self-tested at this point. The API will likely change a bit as well. I'm putting it out there mostly so that other modders can try it out and give feedback/make requests before I move it to a more late beta/stable state and lock the API.

 

It's core has been proven enough at this point that I encourage you to start using it for any projects that you don't plan on releasing in the next few weeks (I expect to declare it late beta/stable by then).

My plan is simple. Once it's stable the API gets locked and, other than an unexpected bug fix or two, freeze development of it entirely. So now's the time to make requests, suggestions, etc.

 

 

Why Would I Need This?

If you're a modder then it will help you elegantly protect your code from race conditions, particularly with complex multi-state code that "yields the floor" at inopportune moments.

 

If you're not a modder it might be because another mod is using this (doubtful right now). You can stop reading here unless your curiosity is just boiling over.

 

 

Dependencies

v0.3.0:

Maybe SKSE? If you want great debug information then it's a must have. Otherwise I believe nothing should be broken if you don't have it. If someone could test WITHOUT SKSE (try running the debug dump and see what's in your logs) and get back to me, I'd greatly appriciate it.

 

v0.2.x:

None. Zip. Zilch. Not even Skyrim.esm. Nothing.

 

 

Upgrading From 0.2.x

If you had any allocated locks in your save file then a clean save is required due to a bug that's now been fixed.

 

 

Upgrading From 0.1

Clean save. Start over. Your code will be broken anyhow due to API changes.

 

 

What's New (from 0.2.x to 0.3.0)

Bug fixes (two major), support for ActiveMagicEffect as owner, and a bunch of debugging methods/capabilities.

In detail, the following was done...

 

 

Bug fixes/internal stuff:

  • At some point yesterday I introduced a major bug on lock allocation. Not sure if it was in 0.2.x but an upgrade is STRONGLY recommended.
  • Example code/documentation fixes.
  • Garbage collection was borked. Fixed. Works exactly as expected now.
  • Added better barrier protection within the manager itself.
  • Fixed pointless log spam on NosLock.Free().
  • I must have stopped mid-thought because property AllocCount was not changed from Allocated. Even this doc was inconsistent. Fixed (it's now AllocCount).

ActiveMagicEffect support:

  • ActiveMagicEffect as lock owners is now implemented (AllocLockForAME and kin added).

Debug support:

  • Automatic reporting with an "error" MessageBox when all locks are gone.
  • Automatic silent reporting when there's only 28 left.
  • A bit better Debug.Tracing overall.
  • Added NosLocks.DumpAllocations() which generates a detailed report of all allocated locks and their owners to the papyrus log.
  • Added the ability to fire off DumpAllocations() from the console at will.

API additions to NosLocks:

  • NosLocks::AllocLockForAME() [global]
  • NosLocks.GetLockForAME()
  • NosLocks.DumpAllocations()
  • NosLocks.IsDeadBeef()
  • NosLocks.IsAllocated()
  • NosLocks.IsAliasOwned()
  • NosLocks.IsAMEOwned()
  • NosLocks.IsFormOwned()
  • NosLocks.IsUnowned()
  • NosLocks.GetOwningAlias()
  • NosLocks.GetOwningAME()
  • NosLocks.GetOwningForm()

API additions to NosLock:

  • Int NosLock.TimesLocked
  • Int NosLock.TimesWriteLocked
  • Float NosLock.Lifetime
  • Float NosLock.TimeSpentLocked
  • Float NosLock.TimeSpentWriteLocked

 

 

 

What's New (from 0.1 to 0.2.x)

A lot. Heavy API refactoring. No bug fixes because I didn't find any :). Two major API usage changes:

  • Locks are now "owned" so that they may be garbage collected when deadbeef.
  • You no longer have to manually get the manager. Everything you need is scoped globally.

In detail the following has changed...

 

  • Documentation: I used Upgrade() everwhere instead of UpgradeLock()... doh!
  • NosLocks.ActiveCount has been changed to NosLocks.Allocated
  • Garbage collection! deadbeef locks are now garbage collected every 2 minutes
  • The self tests now provide almost 100% code coverage (getting close to stable here).
  • Debug.TraceStack() used all over the place for warnings and errors.
  • NosLocks.AllocLock() and NosLocks.FreeLock() have been removed in favor of the following...
    • NosLocks::AllocUnownedLock() [global]
    • NosLocks::AllocLockForAlias() [global]
    • NosLocks::AllocLockForForm() [global]
    • NosLocks::FreeLock() [global]
    • NosLocks.GetUnownedLock()
    • NosLocks.GetLockForAlias()
    • NosLocks.GetLockForForm()
    • NosLocks.ReleaseLock()

    [*]In additon, the following has been added to the manager (NosLocks)...

    • String NosLocks.Version [read-only]
    • Float NosLocks.VersionFloat [read-only]
    • Int NosLocks.VersionMajor [read-only]
    • Int NosLocks.VersionMinor [read-only]
    • Float NosLocks.GCFrequency [read-only]
    • NosLocks.GarbageCollect()
    • NosLocks::IsInstalled() [global]
    • NosLocks::GetManager() [global]
    • NosLocks::GetVersion() [global]
    • NosLocks::GetVersionFloat() [global]
    • NosLocks::GetVersionMajor() [global]
    • NosLocks::GetVersionMinor() [global]

    [*]NosLock.Release() has been renamed to NosLock.Unlock() to be more consistent.

    [*]The following has been added to the individual locks (NosLock)...

    • Int NosLock.Readers [read-only]
    • Int NosLock.PeakReaders [read-only]
    • NosLock.IsLocked()
    • NosLock.IsReadLocked()
    • NosLock.IsWriteLocked()
    • NosLock.Free()

    [*]NosLock.Yield() has become NosLock::Yield() [global]

 

 

 

Reference Guide

NosLocks:

 

Int Property AllocCount; Count of currently allocated locks in the wild. [read-only]Float Property GCFrequency; How often the garbage collector runs. (120.0) [read-only]String Property Version; The version of NosLocks installed. ("0.3.0") [read-only]Float Property VersionFloat; The version of NosLocks installed. (0.3) [read-only]Int Property VersionMajor; The major version of NosLocks installed. (0) [read-only]Int Property VersionMinor; The major version of NosLocks installed. (3) [read-only]Bool Function IsDeadBeef(NosLock lock); Returns True when the specified owned lock is detected as leaked.; * This will cause log spam if the lock was actually leakedBool Function IsAllocated(NosLock lock); Returns True when the specified lock is allocated.Bool Function IsAliasOwned(NosLock lock); Returns True when the specified lock is allocated and owned by an Alias.Bool Function IsAMEOwned(NosLock lock); Returns True when the specified lock is allocated and owned by an; ActiveMagicEffect.Bool Function IsAMEOwned(NosLock lock); Returns True when the specified lock is allocated and owned by a Form.Alias Function GetOwningAlias(NosLock lock); Returns the owning Alias for a lock, or None.ActiveMagicEffect Function GetOwningAME(NosLock lock); Returns the owning ActiveMagicEffect for a lock, or None.Form Function GetOwningForm(NosLock lock); Returns the owning Form for a lock, or None.NosLock Function GetUnownedLock(); Returns an unowned lock for use. If all 128 are taken, returns None.; * Owned locks are strongly preferred over unowned locks.; * You'll probably be more interested in AllocUnownedLock()NosLock Function GetLockForAlias(Alias owner); Returns an owned lock for use. If all 128 are taken, returns None.; * You'll probably be more interested in AllocLockForAlias()NosLock Function GetLockForAME(ActiveMagicEffect owner); Returns an owned lock for use. If all 128 are taken, returns None.; * You'll probably be more interested in AllocLockForAlias()NosLock Function GetLockForForm(Form owner); Returns an owned lock for use. If all 128 are taken, returns None.; * You'll probably be more interested in AllocLockForForm()Bool Function ReleaseLock(NosLock lock); Gives a lock that is no longer needed back to the manager. Returns True; if the lock was actually previously allocated, or False otherwise.; * You'll probably be more interested in NosLock.Free(); WARNING: It will attempt to gain a write lock on that lock. If you free;		  a lock that's locked by the calling thread, you will cause a;		  dead-lock to happen.Function GarbageCollect(); Forces a garbage cellection cycle to try and deallocate deadbeef locks.Function DumpAllocations(); Generates a detailed report (SKSE is recommended) of all allocated locks in; the Papyrus log.Bool Function IsInstalled() global; Returns True when NosLocks.esp is loadedNosLocks Function GetManager() global; Returns the manager instance or None if NosLocks.esp isn't loadedString Function GetVersion() global; Returns the version of NosLocks installed. ("0.3.0"); Returns "" when NosLocks.esp is not loadedFloat Function GetVersionFloat() global; Returns the version of NosLocks installed. (0.3); Returns 0.0 when NosLocks.esp is not loadedInt Function GetVersionMajor() global; Returns the major version of NosLocks installed. (0); Returns 0 when NosLocks.esp is not loadedInt Function GetVersionMinor() global; Returns the minor version of NosLocks installed. (3); Returns 0 when NosLocks.esp is not loadedNosLock Function AllocUnownedLock() global; Returns an unowned lock for use. If all 128 are taken or NosLocks.esp isn't; installed, returns None.; * Owned locks are strongly preferred over unowned locks.NosLock Function AllocLockForAlias(Alias owner) global; Returns an owned lock for use. If all 128 are taken or NosLocks.esp isn't; installed, returns None.NosLock Function AllocLockForAME(ActiveMagicEffect owner) global; Returns an owned lock for use. If all 128 are taken or NosLocks.esp isn't; installed, returns None.NosLock Function AllocLockForForm(Form owner) global; Returns an owned lock for use. If all 128 are taken or NosLocks.esp isn't; installed, returns None.Bool Function FreeLock(NosLock lock) global; Gives a lock that is no longer needed back to the manager. Returns True; if the lock was actually previously allocated, or False otherwise.; * You'll probably be more interested in NosLock.Free(); WARNING: It will attempt to gain a write lock on that lock. If you free;		  a lock that's locked by the calling thread, you will cause a;		  dead-lock to happen.

 

 

NosLock:

 

Int Property Readers; The current number of concurrent readers for the lock. [read-only]Int Property PeakReaders; The peak number of concurrent readers for the lock. [read-only]Int Property TimesLocked; The total number of times a lock request of any kind has been made.; [read-only]Int Property TimesWriteLocked; The total number of times a write-lock request of any kind has been made.; [read-only]Float Property Lifetime; Total amount of game-time (not real-time) the lock has been allocated.; [read-only]; * Skyrim doesn't provide a steady monotonic clock in reference to the save,; * so this property has limited usefulness.Float Property TimeSpentLocked; Total amount of game-time (not real-time) the lock has been in a locked; state of any kind. [read-only]; * Skyrim doesn't provide a steady monotonic clock in reference to the save,; * so this property has limited usefulness.Float Property TimeSpentWriteLocked; Total amount of game-time (not real-time) the lock has been in a write-; locked state. [read-only]; * Skyrim doesn't provide a steady monotonic clock in reference to the save,; * so this property has limited usefulness.Bool Function IsLocked(); Returns True if there are any readers or a writer.Bool Function IsReadLocked(); Returns True if there are any readers.Bool Function IsWriteLocked(); Returns True if there is a writer.Function ReadLock(Int writeLockID=0); Obtains a read-lock. Blocks until obtained. Optionally recurses a current; write-lock.Int Function WriteLock(Int writeLockID=0); Obtains a write-lock. Blocks until obtained. Optionally recurses a current; write-lock. Returns the current write-lock ID for use elsewhereBool Function TryReadLock(Int writeLockID=0); Attempts to gain a read-lock. Returns True if a read-lock was obtained, or; False if a read-lock couldn't be obtained (would have blocked).; Optionally recurses a current write-lock.Int Function TryWriteLock(Int writeLockID=0); Attempts to gain a write-lock. Returns the write lock ID on success, and; 0 when a write-lock couldn't be obtained (would have blocked).; Optionally recurses a current write-lock.Int Function UpgradeLock(); Upgrades a read-lock to a write-lock. Returns the write-lock's ID.Bool Function Unlock(); Releases the current lock. Returns True if there are no longer any readers; or writers.NosLock Function Free(); Frees the lock, giving it back to the manager. Always returns None so that; you may use it like myLock = myLock.Free().; WARNING: It will attempt to gain a write lock on that lock. If you free;		  a lock that's locked by the calling thread, you will cause a;		  dead-lock to happen.Function Yield() global; Simply yields the current thread by calling Utility.WaitMenuMode with a; tiny duration.

 

 

Console Commands:

 

setpqv NosLocks DebugDump 1; Fires off NosLocks.DumpAllocations(); * Provides a notification.setpqv NosLocks GC 1; Fires off NosLocks.GarbageCollect(); * Silent. Check the log to see if it cleaned up any deadbeef locks (silent; * if it didn't).setpqv NosLocks RunSelfTest [duration in seconds]; Runs a self-test with 4 threads each competing with each other for the; duration specified. A good duration is generally anything greater than 15,; but may be longer depending on a variety of conditions. Try 30. Running for; hours results in unbelievably high confidence that the barrier mechanism; completely works as expect.; * When the test is completed, a report will be given with a status of,; * failure, inconclusive, or success. Inconclusive just means that you should; * try again, probably with a longer duration this time.; * If the test never finishes, it means a dead-lock occured .; WARNING: Loading from a save that was made during a running self-test will;		  result in unexpected behavior.; WARNING: Loading from a save that was made after a self-test will result in;		  unexpected behavior.; WARNING: The self-test does not reset itself after finishing. Run once per;		  session and don't save.

 

 

Quick-Start Example

The below demonstrates NosLocks in a nutshell...

NosLock MyLockEvent OnInit()   MyLock = NosLocks.AllocLockForForm(Self)EndEventInt Function GetSomething(Actor whatever, Int lockID=0)   MyLock.ReadLock(lockID)   Int res = gottenFromSomeProtectedData   MyLock.Unlock()   Return resEndFunctionFunction SetSomething(Actor whatever, Int lockID=0)   MyLock.ReadLock(lockID)   If (!GetSomething(whatever, lockID))	  lockID = MyLock.UpgradeLock()	  If (!GetSomething(whatever, lockID)) ; check again		 manipulateSomeProtectedData	  EndIf   EndIf   MyLock.Unlock()EndFunction

.

 

In-Depth Example

 

 

There are 128 available locks for ALL mods to use (not just yours). To request a lock you need to get one from the manager. After you have a lock, you can read lock it, upgrade a read lock to a write lock, write lock it, or recursively read/write lock it from a write lock.

 

Let's say that you have a cloak spell tracking near by actors, and then a main management quest that allows other stuff to access that list. It might look like the following...

;NB: You wouldn't want to _ACTUALLY_ do it this way simply because polling;GetDistance() is painfully slow, but this gives you an idea of how you might;use the locker.Actor[] ActorsInt ActorCountNosLock MyLock = NoneEvent OnInit()   Actors = new Actor[64]   ActorCount = 0   ; Get the lock manager (this can obviously be done via a property as well)   If (!NosLocks.IsInstalled())	  Debug.MessageBox("FATAL: NosLocks.esp is not installed :(")	  Return   EndIf   ; Get a locker... it's not locked until you lock it   MyLock = NosLocks.AllocLockForForm(Self)   If (!MyLock)	  Debug.MessageBox("FATAL: Couldn't get a lock :(")	  Return   EndIf   RegisterForSingleUpdate(0.5)EndEvent; Now that you have a lock, you can put it to use.Int Function FindActor(Actor who, Int lockID=0)   MyLock.ReadLock(lockID)   Int i = 0   While (i != ActorCount) && (Actors[i] != who)	  i += 1   EndWhile   If (i == ActorCount)	  i = -1   EndIf   MyLock.Unlock()   Return iEndFunctionBool Function AddActor(Actor who, Int lockID=0)   ; This could use a read lock that gets upgraded as needed... see OnUpdate()   lockID = MyLock.WriteLock(lockID)   If (ActorCount == 64)	  MyLock.Unlock()	  Return False   EndIf   If (FindActor(who, lockID) != -1)	  MyLock.Unlock()	  Return False   EndIf   Actors[ActorCount] = who   ActorCount += 1   MyLock.Unlock()   Return TrueEndFunctionBool Function RemoveActor(Actor who, Int lockID=0)   ; This could use a read lock that gets upgraded as needed... see OnUpdate()   lockID = MyLock.WriteLock(lockID)   If (ActorCount == 0)	  MyLock.Unlock()	  Return False   EndIf   Int i = FindActor(who, lockID)   If (i == -1)	  MyLock.Unlock()	  Return False   EndIf   Int j = ActorCount - 1   While (i != j)	  Actors[i] = Actors[i + 1]	  i += 1   EndWhile   ActorCount -= 1   Actors[ActorCount] = None   MyLock.Unlock()   Return TrueEndFunctionEvent OnUpdate()   Int lockID = 0   MyLock.ReadLock()   Int i = 0   Actor player = Game.GetPlayer()   Bool needUpgrade = False   While (i != ActorCount) && (!needUpgrade)	  If (Actors[i].GetDistance(player) >= 1000.0)		 needUpgrade = True	  EndIf	  i += 1   EndWhile   If (needUpgrade)	  lockID = MyLock.UpgradeLock()	  ; Now we have a write lock so we can safely mutate the array of	  ; actors. Note that I didn't start where I left off. See "A Note About	  ; Upgrades".	  i = 0	  While (i != ActorCount)		 If (Actors[i].GetDistance(player) >= 1000.0)		    RemoveActor(Actors[i], lockID)		 Else		    i += 1		 EndIf	  EndWhile   EndIf   MyLock.Unlock()   RegisterForSingleUpdate(0.5)EndEvent; Finally, you should clean up after yourselfFunction WhenUninstallingMyMod()   MyLock = MyLock.Free()EndFunction

Your magic effect that registers/unregisters those actors might look something like...

MyTrackingLib Property Tracker AutoEvent OnEffectStart(Actor target, Actor caster)   Tracker.AddActor(target)EndEventEvent OnEffectFinish(Actor target, Actor caster)   Tracker.RemoveActor(target)EndEvent

Simple enough... it just keeps track of up to 64 actors and purges those that are further than 1000.0 units periodically (again, there are more efficient ways to accomplish this, and there's obviously important code missing, but I'm just being illustrative).

 

So why the locks in this example? If you had done this (or something similar) without them, at some point Skyrim would lock up in an infinite loop. This is because when GetDistance() was being called, a magic effect might have mutated the array of actors via AddActor() or RemoveActor(). Only when a script is running is it thread safe... not just because it's in the call stack.

 

Something you might have noticed as well is as soon as I got a write lock I started passing around it's ID. This allows for recursive locking. Without it the code would have dead-locked on the next (inner) lock request.

 

 

 

 

Owned vs. Unowned Locks

In 0.1 all locks were unowned. Starting with 0.2 it's now strongly preferred that you allocate owned locks. If the script using the lock is a Form use AllocLockForForm(). If it's an Alias use AllocLockForAlias(). If it's an ActiveMagicEffect use AllocLockForAME(). Explict lock freeing is still preferred of course, but now the locks can be garbage collected when someone uninstalls your mod.

 

A Note About Lock Upgrades

If you are unfamiliar with read/write locks (but understand the concept of mutexing), keep something in mind: you can not make assumptions about the state of your data after the upgrade. The upgrade is not an atomic operation. During the upgrade another writer might have come along and changed your protected data. Basically...

 

 

Don't do this...

myLock.ReadLock(); something and whatever are protectedIf (something == 0)   myLock.UpgradeLock()   ; mutate whatever assuming something is still 0. It might not be.EndIfmyLock.Unlock()

Instead, do this...

myLock.ReadLock(); something and whatever are protected from writesIf (something == 0)   myLock.UpgradeLock()   If (something == 0)	  ; mutate whatever because you know you're the only one touching the	  ; data _AND_ it still meets our conditions   EndIfEndIfmyLock.Unlock()

 

 

 

 

Write Lock IDs

Two things...

 

First, 0 is the only invalid write lock ID, so don't expect 0.

 

Second, they should be passed around in a fully reentrant manner. Don't globalize them (it's tempting... I know) or you might find yourself scratching your head about why you're dead-locking when it looks like you shouldn't be.

 

 

Lock Leaking

Release those locks back to the manager if they have a limited life time! Be kind. There's 128 of them but that runs out quickly if they aren't released but new ones keep getting acquired. Examples:

  • If you're using a lock in a magic effect, NosLocks.FreeLock(MyLock) on the effect finish event!
  • If you have a lock for a quest that only is used when the quest is running, release it when the quest stops!
  • If you have "permanent" locks and start an "uninstall" procedure, give them back or they will walk in limbo for eternity!

If you don't remember to release then owned locks will be garbage collected periodically (not optimum of course), but unowned locks will be lost forever! Leaking unowned locks may eventually result in the player's save game being 100% deadbeef.

 

 

Pre-emptive FAQ

Please read before asking questions/asserting positions as they might already be addressed.

 

 

Q: What's mutexing, locking, semaphores, dead-locking, etc?

A: Let me google that for you. Seriously though (do google it as well) there's an entry over at creationkit.com all about thread access and papyrus you should check out. Some of the barrier methods given as example are... questionable, but overall it's a good view of how Skyrim is dealing with concurrancy.

 

Q: How do I just use NosLocks for a simple mutex?

A: Just write lock. It's a mutex.

 

Q: Why not just use GotoState()?

A: Imagine the above example is much more complex. A script can only be in a single state at a time. While you can recreate this concept from scratch for your script, that means you can't use states for more high-level stuff. If your code is simple, then read-write locks are probably overkill to begin with. If that's the case, then yes, just use states (it's a tad faster if nothing else). KISS. The right tool for the right job. Et cetera.

 

Q: How do you know this works?

A: Short answer, I don't. Until I can read Skyrim's source code to verify GotoState()/GetState() are protected by a semaphore of some sort I can't be 100% positive race conditions don't exist. I've done a bunch of testing of the mutexing though and, conservatively, I'd say I'm over 95% sure this works. Regardless, it's definitely better than no protection at all.

 

Q: How do I get to a clean-save point after using your library in my mod?

A: Release all the locks and give the locks back to the manager. You probably want to set them to None as well to avoid log noise on the next load. That's it. Nothing special.

 

Q: How many readers can I have at one time?

A: As much as can fit in 31-bits of an integer (~2 billion). What? Are you writing a high performance HTTP server in Papyrus or something? Crazy person!

 

Q: Isn't locking slow?

A: There's a misconception that locking is "slow". It's not unless there's contention for the lock. Locking is nearly "free" when there's no contention. Read/write locking helps with that even more because multiple readers can obtain the lock for "free". It's only costly if there's contention involving a writer, and only then really costly if there's opportunity costs from a lock upgrade. The real question is, "Isn't Papyrus slow?" Yes. Yes it is.

 

Q: What happens if a 3rd-party script calls one of my methods and manually/accidentally sets the lock ID argument?

A: Why in the world would that ever happen? Of course it will happen :). If it does, don't fret. It will only screw things up if that ID just happens to be the correct one (a 1 in 4 billion chance). If it's the wrong ID NosLocks will just act as if it was the default 0 (not the current write lock) behaving completely normal for all intents and purposes.

 

Q: Starvation?

A: It's certainly a possibility. For general usage I doubt it will ever show it's ugly face but it's something to think about in your lock-flow design. If it happens, it will be a stack overflow as it's absolute worst-case manifestation (I have no idea what that will look like). If you get a stack overflow I'd go back and analyze what's starving the lock over and over again faster than another requester can actually grab it. Overall, when you design your code flow I would pay more attention to the possibility of reader starvation rather than writer starvation simply because mutations in papyrus--in practice--tend to be slow by nature compared to simply looking something up.

 

Q: 128 Quests??? WTF?!?!?

A: Blame Bethesda. Actually, I probably could have done it a different way (like 1 quest with 128 aliases) as the locks just need their own property scopes, but this works so whatever. Just ignore them or simply link NosLocks runtime with GetFormFromFile(0x801, "NosLocks.esp"). I'm new to Skyrim modding and probably everyone reading this is screaming, "You could have done <insert method here> you idiot!"

 

Q: I used your library and now Skyrim is locking up! Help!

A: Not actually a question. It's probably not my library (but may be of course). You're probably dead-locking because you forgot to pass around the writer lock id or something like that (double-check your call flow). If it is my code, please point out the issue in the code and I'll be happy to fix it if it's fixable, or scrap the project, apologize, and walk away in shame if it's not.

 

 

 

Please try it out (not on a stable branch of your project) and let me know what you think. Particularly if there's issues or feature/API request changes.

 

~ nosis


Other Files from nosis


×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue. For more information, see our Privacy Policy & Terms of Use