 |
|
 |
It is now faster a lot faster and much more stable. It is still slower than ReaderWriterLockSlim, but I would say that the difference is well worth its price in manageability.
The data-consistency breaking locks by randomly grabbing whatever thread happens to be there and is causing the lock should go away soon. I am getting closer to having the order in which locks done being used to determine how deadlocks are solved. This will take some heavy testing and thinking, but I'm sure if locking is kept in the correct order, then when deadlocks are solved, it will be intuitive to know which thread is expected to come through first (keeping data consistent).
But this data-consistency issue is exactly the issue preemption causes when it switches threads in an unexpected place if two processes are accessing the same resource. If that happens, then one thread will only write part of what it needed, then another thread begins writing to that resource. And meanwhile, the first thread didn't finish, so that resource has corrupted data.
But the difference between deadlock prevention and preemption is you get to choose where the threads switch context.
By-the-way...Non-locking and Read/Upgrade/Write spin-locking is a lot more complicated than I had originally thought (hence the low rating). Maybe in the future it will support heuristic so that it doesn't just guarantee that one thread runs, but that one thread per processor runs.
|
|
|
|
 |
|
 |
I understand your motivation to create a simpler to use lock, but I disagree that you are solving a problem. You are, in fact, "breaking" a lock when a dead-lock occurs, considering that it is ok to do that and that "the programmer will need to deal with the consistency problem" when, in fact, a ReadLock is an way to say: While I am not done, no changes can happen. And, a WriteLock is a "if I am not finished, no one can see what I am doing".
Upgrading a lock, or creating a super-thread that is not stopped, after another thread got the lock is "I break your lock". This is even worst than dead-locking, and will only not cause problems in cases like:
lock(a)
lock(b)
SomeAction();
and
lock(b)
lock(a)
SomeAction();
Because there is no action between the first and second lock and, in fact, there is no need for two locks.
|
|
|
|
 |
|
 |
What about having exceptions thrown when deadlocks occur? Does this not solve an issue that ReaderWriterLockSlim has?
Deadlock detection, I guess, solves both the issue, deadlocks and data-consistency, as exceptions guarantee that code within the lock does not get ran. And any lock causing a deadlock will be unlocked all the way up to it being caught or the program exiting.
I am saying that I will change this version of my ReadWriteLocker to one where no parameter is needed. Throwing errors when deadlocks are found will be mandatory. Would this solve the issue you have brought up? And even if I find altering deadlocks useful in some situations, it won't exist.
The biggest issue this solves is allowing you to instantly know where deadlocks occur, and not have to guess by breaking the application to see if the ReaderWriterLockSlim has caused a deadlock. And upgrading and downgrading locks could still be done (as I believe Joe Duff's Weblog says deadlocking is the primary reason why only a single upgrade lock is allowed).
But on another note, is there any problems you can think of with upgrading and downgrading locks like I am doing (not considering deadlocks)? Deadlocks will result in exceptions being throw, telling you that locking is working incorrectly.
This is something I haven't researched much, so any ideas/insight would be really helpful. If there are problems here, I may consider using similar upgrade lock functionality, while throwing exceptions when I find deadlocks to occur.
|
|
|
|
 |
|
 |
About throwing exceptions, I agree it can be really useful.
Actually, I myself throw exceptions on dead-locks... but my "locks" only try to do such consistency in debug mode.
|
|
|
|
 |
|
 |
Excellent. I agree 100% that if you have a choice, that is the way things should be done. Unfortunately, when your in a team you don't control, you don't always have that choice.
I designed this locker to solve issues I was having with an ex-lead programmer (this forked rw-locker was in a personal project, so I lost some better code I had implemented there. but both had similar functionality). He later left (should have been fired), but he had introduced multi-threading in several places where the database could have handled the same functionality with no issues. And this was even before I joined the project, in some places.
As such, we had threads running on collections and various other modules without any regards to thread safety. Then he tried to solve those issues by locking wherever he felt or saw enumeration while modification errors occur. And this caused huge performance issues.
At the time we weren't still forced to use .NET 2.0, and .NET 4.0 wasn't even released. So this was my solution. And preventing deadlocks by using a super thread didn't break data-consistency, as I finally had enough control to only lock around tasks, and not within them.
But until you replied, I never had a clear definition in my head what happened, and how my locker was truly working.
Thanks again. My locker mostly if you find yourself in a situation with a out-of-your control rogue programmer using multi-threading unwisely.
But I'm really interested in your locker. Is it a codeproject or codeplex project? If so, I'm not quite sure why I'm continuing to re-inventing this wheel
|
|
|
|
 |
|
 |
It is part of my libraries, but it is relativelly new, so I think it is not in any of my published articles.
But I, in fact, try to detect dead-lock situations.
If you first lock object A, then object B.
And, another thread, at another moment, lock B, then A, then I throw an exception (as I said, only in debug mode) to show a dead-lock situation.
I don't really plan to write an article in the near future, but if you want, give me your e-mail and I can send it to you.
|
|
|
|
 |
|
 |
I was thinking on how to solve data inconsistency under this situation.
lock(a)
lock(b)
if (!lock.IsSuperThreadRunning)
{
SomeAction();
} else {
ThrowSomeError();
}
and
lock(b)
lock(a)
if (!lock.IsSuperThreadRunning)
{
SomeAction();
} else {
ThrowSomeError();
}
The block inside the if statement never gets ran. And it is easily distinguishable when deadlocks happen. The solution is still solved because when the super thread ends, regular locking resumes.
Because you are able to distinguish when the data-consistency contract may be violated and deal with it accordingly (if you code things correctly) data-inconsistency can never happen.
But this is not INTUITIVE! I am re-designing the locker to be policy based. This way you can call lock(b, policy) to choose whether you want lock(b) to throw an exception when deadlocks happen, allow super thread unlocking using the above example to keep data consistent while you are able to throw the exception, and for other things such as allowing or preventing locks from being upgraded or recursively upgraded.
|
|
|
|
 |
|
 |
If I understood well, you solved the problem of dead-locks making a "dead-locked" thread to run, and you also talk about upgrading locks...
But, only talking about the super thread, see the situation:
Thread 1:
Locks A.
Gets a value protected from lock A.
Tries to lock B (held by thread 2).
If it gets the lock, change the value protected from B.
Uses the value protected from lock A.
Thread 2:
Locks B.
Gets a value protected from lock B.
Tries to lock A (held by thread 1).
If it gets the lock, change the value protected from A.
Uses the value protected from lock B.
See the problem of the super thread?
If not, here it is:
Thread B is holding an "immutable" value protected from its lock (immutable because only he can change it). But, to solve the dead-lock, A is allowed to run, CHANGING the value of B. Now, B has a local variable of an "immutable" value that, in fact, has changed!
Considering two locks to then use the protected objects, your solution can work. But, lock, read/write some values, lock, read/write some other values, upgrading the locks and creating super-threads is not a solution.
|
|
|
|
 |
|
 |
Threading is always a little hard to comprehend, so I hope I read you correctly.
The super thread is global. It doesn't belong to any specific lock. When a super thread is set, no other threads can be set to the super thread while it is running.
This prevents Lock A from setting the super thread, then Lock B setting the super thread. And them flip-flopping without making any progress.
And while this super thread runs, it does not overwrite local variables, such as the local writing thread. This is so when regular locking resumes things will not be modified such that the lock thinks a hard lock has occurred and needs solved (problem with early versions).
To know when the super thread is finished, it is accompanied by an integer that is incremented and decremented until its count equals zero. This allows it to recursively finish.
The last lock that the super thread runs through that sets its accompanied integer to zero is the one that signals (via PulseAll) that the all other threads should jump out of sleeping mode and check and see if they are allowed to run, or if deadlock prevention via the super-thread needs done again.
To reply to your post, Thread B doesn't hold any values. But Lock 2 does have a reference to this super thread global variable. But the super thread variable is not "immutable," as any lock can change it. And if Thread B is assigned as the super thread, you are right in that only Thread B can cause it to change (Of course, at this point, it is the only thread running).
Otherwise, with no super thread, any thread can assign itself as the super-thread. But due to thread-safety, only one thread will be set. And no other threads will be allowed to be set until the assigned super thread "runs its course" and finishes.
I do still believe that this is a solution. It's not the best solution as temporary deadlocks can occur. This is because Thread A can be running somewhere, while a deadlock situation occurs between Thread B and C. One pretense is that Thread A must complete! After that occurs, the super thread algorithm will resolve Thread B and C. But this is a temporary deadlock, in that more parallelism could be occurring.
My attempts on solving these temporary deadlocks have all resulted in drastically reduced performance. And I'm pretty sure none of those attempts even worked.
I had convened myself for sometime that this because of temporary deadlocks being possible, this was not a solution. Took me a while to figure out that it is. Even if it isn't a very good one (which is better than most other read-write lockers that do no distributed deadlock prevention at all).
It is a very simple idea to make sure that at least one thread is always running (disregarding external code, as that requires solving the halting problem and applying that solution). Also, assigning the super thread to a random thread (first one to come in) isn't necessarily the smartest thing to do. But I think this problem is a lot like Hash Tables, in that finding a good heuristic which isn't perfect, but is acceptable, is really the only way this will be solved correctly.
This is an excellent point, and I will try to update this article with information I have said. And I'm not great at grammar, so I will sometime be getting help in improve the wording in the article .
You can try the Unit Test included in the project if you have at least Visual Studio 2008 .NET Pro. Even if this really isn't a solution to all deadlocks, like I believe it is, this unit test really is demanding. ReaderWriterLockSlim locks when upgrading a read lock to a write lock more than once. This at least does that (while preventing the deadlocks sometimes associated with that functionality).
Please reply back if this makes sense or not. Or you still believe deadlocking using this algorithm is possible.
modified on Friday, October 8, 2010 2:47 AM
|
|
|
|
 |
|
 |
No, I don't think you understood the situation.
I will try to simplify:
Thread A:
Lock 1.
Tries Lock 2.
Thread B:
Lock 2.
Tries Lock 1.
Dead lock.
How do you solve this?
If you allow thread A to run (effectivelly acquiring lock 2) you are also saying that lock B "temporarelly lost" its lock. So, all the data protected by its lock are no more protected.
If, instead thread B runs, then the data that thread A thought that was protected (and that's the immutable I talk) are no more protected, as B can be changing it.
|
|
|
|
 |
|
 |
When I allow thread A to run, lock B doesn't "temporarily loose" its lock, since it is still locking on thread B. It merely chooses thread A for write locking (well, super thread locking) until thread A completes. It does this for lock A, too. All locks temporarily choose lock A for write-locking and continues until thread A finishes past where the deadlock occurred.
As such, only thread A can run, maintaining that only 1 thread can run over the lock. This prevents two threads from accessing a resource at the same time, or no threads from ever accessing a locked resource by not running.
This locker, as of right now, has nothing to do with the order in which deadlocks are resolved.
"an immutable object is an object whose state cannot be modified after it is created." - Immutable object, wiki
If you are planning on causing deadlocks so objects within locks cannot be changed at all by any thread, this lockers purpose is to prevent that. I am assuming this because when an unresolved deadlock happens, the code within all locks are immutable to all threads (that code cannot cause any modifications).
But I don't assume you are crazy for wanting this. In fact, all you have to do is pass ReadWriteLocker.Behavior.ThrowOnHardLock to the constructor, and instead of running the code within the lock when a deadlock occurs, an exception is thrown.
But beware that this is still in progress, so my locker may not be in a sane state after a throw occurs. I just haven't had the time to test it and fix it. But if you go to Exceptions in the Debug menu, you can cause it to break when these exceptions happen, and then use the call stack to fix places where deadlocking happens.
|
|
|
|
 |
|
 |
I understand that only one thread runs inside the lock.
My concern is simple.
lock(someLock)
{
var a = getAVariable();
lock(someOtherLock) {
}
if (a != getAVariable())
throw new Exception("A variable was changed, when I HAD A LOCK TO AVOID IT.");
}
Then, the other thread:
lock(someOtherLock)
{
lock(someLock) {
changeAVariable();
}
}
I used "lock", but I mean using your lock.
The real problem is not about two threads running at the some time, but the "aVariable" to have changed after the first thread got its lock, when it can't be changed. (and that's the "immutable" I was talking about, not about real-immutable objects).
A simple lock case:
readLock(hashSet)
{
foreach(var item in hashSet)
{
}
}
|
|
|
|
 |
|
 |
I think I understand why we still disagree. I think it is related to the concept.
As I see, for you upgrading a lock is OK, as only ONE thread will execute at a time.
But, the lock is not only a way to tell "Here only one thread executes at a time"... it is an way to say: "Until I finish my job, no one will have access to it... even if I am sleeping".
So:
Lock A (got), then lock B (waiting).
Lock B (got), then lock A (waiting).
This is the common dead-lock. But, if you allow anyone to run in that situation, you are also considering:
Any attempt to obtain a lock also gives the other the opportunity to run when you wait for that lock.
But, then, my job is not finished. Anyone executing after I got my first lock will see a corrupted work.
Want a more human-world example?
I use source-safe and check-out file A. I have changed such file.
You use source-safe and check-out file B. You have changed such file.
I then use source-safe and want to check-out B, while you check-out A.
If one of us don't "undo our work" (ok, we can backup our work, but that's something like using a TryLock and dealing with it), we will consider that, for example:
I have a lock on file A. Which I already changed.
You have a lock on file B. Which you already changed.
And then, source-safes says (only to me): I will keep the other user waiting... so, get his file and modify it. You are doing this alone, he will not get that chance. And, when I try to modify the file:
"Hey... this B file is not compiling. How can I work on this???" (as I said, you didn't finished your work... but I am seeing your modifications... and "running alone").
|
|
|
|
 |
|
 |
So there is Thread A, Thread B, and Thread C (source-safe). Thread A and Thread B are deadlocked, as desired. But Thread C isn't doing any thread locking, but will do thread locking in the future to remove the deadlock imposed by Thread A and Thread B.
The simple solution is to make a new unrelated lock, and have the source-safe thread C do locking whenever you don't want deadlock prevention to occur. With this extra lock being done (not really doing anything, since it's single threaded instance), the deadlock situation won't be resolved until Thread C finishes locking.
Thread C could even lock for the duration of the application. But once Thread C completes, everything else will be guaranteed to run to completion. Thus solving the deadlocking issue present in other lockers.
As such, it seems like this is still a solution, even if a little more work has to be done on the coders side to get the expected results.
|
|
|
|
 |
|
 |
No... source-safe is not a thread... it's the "virtual environment" where locks can be acquired.
So, let's see another example:
Initialize List A with 1.000.000 items and List B, also with the same amount of items, but with different values (for example, listA, only positive integers, listB, only negative integers).
To keep names logic: listA, lockA, listB, lockB.
* Note: listA can be a hashSet if you want, but I think an slower algorithm can help cause the error.
Then, two threads:
Thread 1:
using(lockA.ReadLock())
{
foreach(int item in listA)
{
using(lockB.ReadLock())
{
if (!listB.Contains(item))
{
using(lockB.WriteLock())
{
listB.Add(item);
}
}
}
}
}
Thread 2: Only invert lockA and lockB, listA and listB.
To be honest, the ReadLock and the Contains is unnecessary, but keep it there to have a real-scenario where a lock will be upgraded.
Then, I got the result:
Thread 1 starts. It can even process some items, but I hope not all.
Thread 2 starts.
Now, we have:
lockA for sure in thread 1.
lockB for sure in thread 2.
Now, any write-lock, as they are crossed, will be a dead-lock. So, you free a thread to solve the lock but, in that case, the other thread (the one that was kept waiting) will throw an exception that the collection was modified, when it had a lock to guarantee that:
* Other thread can read the collection, but no other thread can change it.
So, how do you solve that?
|
|
|
|
 |
|
 |
Easily, by changing thread safety so it doesn't work within specific task (posted below, multiple functions that accomplish a single goal), or I can make a new collection which caches modifications until it is safe to apply them.
The collection in question that throws a "modification while enumeration" exception really doesn't have to throw that exception. It could instead be designed to silently apply changes (if possible, ignoring changes otherwise) and allow unexpected results. But it throws to notify you that unexpected results could occur, just as my read-write locker does when you pass it a parameter to throw when deadlocks occur.
|
|
|
|
 |
|
 |
Yes. This can be a problem when you consider collections. But that is because you are locking too finely within them. The resource in this case is really an subset of functions (a task) of that collection. Some tasks are contained in one function (like removing an item), and others involve more than one function (such as removing the last item in the collection--needing Count and RemoveAt). This is external to this read-write locker, so there is no way it can tell that this could be a problem.
For an example, lets look at Read-copy-update Linked List. It solves this issue without locking and is really an elegant approach to collections. And since Hash Tables can use linked-lists, so I'm counting RCU Hash Tables in the equation too.
With the RCU Linked List, you can insert, remove, and navigate through the linked list in any manner you like. But you can't access the list at any old position. This is one, because linked lists don't have the ability to jump to any desired position in O(1) time, and that accessing the list with a position requires you to know that position exists and is the correct item you desire. And that requires more than one function to accomplish a task.
Example:
public void RemoveAllItems()
{
using lock1.lock()
{
for (int i = 0; i < SomeCollection.Count; ++i)
{
using lock2.lock()
{
SomeCollection.RemoveAt(i);
} } } }
public void AddItems()
{
using lock1.lock()
{
for (int i = 0; i < 100; ++i)
{
using lock2.lock()
{
SomeCollection.Add(i);
} } } }
And two threads where running on these functions and deadlocked occurs at lock 2, this could very well cause issues when a random thread chosen to iterate over the respective block of code. But that is because locking is being done within a task/resource. And this is outside the scope of the issue that has been solved.
I have experienced this situation many times, so I hope this is the issue you are pointing out. I still believe I have a solution for solving deadlocks. But as for data consistency within these situations, that is up to the coder to solve.
And as such, you have your choice of what parameters to pass to the constructor to get the desired functionality you wish (deadlock detection where it throws an exception, or deadlock prevention where it chooses one and continues). This allows you to solve situations this locker cannot solve (because it requires knowing and analyzing code external to the locker).
|
|
|
|
 |
|
 |
Well... you solved the dead-lock problem allowing locks to be "breaked".
You can have a collection that solves the problem... but that's the same as having code that uses TryLock to deal with the problem.
The principle of ReaderWriterLockSlim is: When you acquire a read-lock (if the code is well written) you guarantee that the resource used will NOT change. You don't need to deal with "consistency problems", as there are none.
That's why a ReadLock can never be upgraded, but an Upgradeable lock can. The UpgradeableLock already blocks other UpgradeableLocks to be acquired at the same time, without blocking readers.
Your solution is: Any ReadLock can, if dead-locked, become a writer-lock (or, allow the thread to run, ignoring the dead-lock)... but, in that case, the purpose (avoiding consistency problems) is lost. And the worst: That's the hardest type of bug to detect.
|
|
|
|
 |
|
 |
I don't believe that the ReaderWriterLockSlim should simply deadlock when these situations occur. It should track and at least throw errors when these problems arise. The lack of finishing, or leaving out functionality and saying it is desired functionality is my #1 pet peeve against Microsoft. Simply allowing something stupid to happen like deadlocks is not intelligent to me, and is one reason why I believe Java is 5 to 10 years ahead of .NET (and may continue to be, depending on Oracle).
It's a good rebuttal saying that solving the deadlock issue while allowing locks to be broken is happening. But the fact is that they aren't broken. Locking is instead changed to a different type of locking when deadlocks are found. And I acknowledge that may lead to unexpected results.
But I believe I have covered this by not condoning using this until you know what you are doing. You should use ReadWriteLocker.Behavior.ThrowOnHardLock or ReadWriteLocker.Behavior.AssertOnHardLock to detect where hard locks occur. And from that, you can easily tell what functionality might be broken, and what isn't effected. And that is much easier than allowing deadlocks to occur.
I do have plans on making it unbios. Such as 3 read locks happen, 1 write lock, and another 4 read locks come in. The 3 read locks would be ran, then the next write lock, and then the next 4 read locks. Right now it has a bios towards write locks, I believe. But this is inherited from ReaderWriterLockAlt. This would be applied to the super thread, as well, making it consistent.
With the ReaderWriterLockSlim, the idea of write locking is tightly coupled with read locking. If you lock for writing, you are also locking for reading. My goal with allowing read locks within write locks is so that there is a distinction between the two (logically). If it is locked down to a single thread via write locking, using a read lock inside a write lock should have no effect except to make IsReading = true. At least, that is my intended goal in the future.
I will defiantly be updating this article. I didn't mean to leave out invaluable information I have discussed here, but that's what discussion is all about
|
|
|
|
 |
|
 |
This is my first time writing an article here at CodeProject. I had some issues with it being incomplete to begin with.
Any other ideas on what I can do? Or any other suggestions on what you thing about this project in general?
|
|
|
|
 |
|
 |
Quote"This article is far from finished. I have ran out of time". Well, don't publish half finished articles making bold statements without having the time to back them up!
|
|
|
|
 |
|
 |
Sorry. First article. How do I submit an unpublished article until I am finished?
|
|
|
|
 |
|
 |
Bold statement backed up by no facts or implementation details. Article should be complete before it is posted. Partial articles are a waste of everyone's time.
|
|
|
|
 |
|
 |
I don't intend for this to be a partial article for long. I had troubles at first finding out how to edit the article. But I really should have intended to have collections implemented before posting that in my title.
I have implemented the collections once, but it was in a closed source project.
|
|
|
|
 |
|
 |
There is no such thing as a lock that cannot deadlock. Every lock, irrelevant of design, can deadlock. That is, after all, the entire purpose of locks. Given any lock design you can always come up with a set of code that will deadlock it. The only type of lock that would provide even a remote challenge would be a lock that auto-unlocks after a given interval but even then you can easily deadlock it. Here's some code that is guaranteed to deadlock irrelevant of locking implementation: //Thread 1 read-lock A write-lock B //Thread 2 read-lock B write-lock A The problem is that a single lock doesn't cause deadlocks. If code is written properly then all locks will eventually be released. The main cause of deadlocks is lock hierarchies so unless you create an elaborate, global locking system you can't prevent hierarchies from forming. Even with a locking system you'd still have to somehow enforce hierarchies. Even worse is what the locking system would do when deadlocks would occur. An exception is about the only safe way to respond. You could use a TryEnter syntax but then you're relying on callers to check the return value. Even if you just limit yourself to a single lock the following code is guaranteed to deadlock as well. //Thread 1 read-lock A loop forever //Thread 2 read-lock A You could theoretically resolve this by auto-unlocking after a fixed time but what time is reasonable? Even more important is how the locker would know that the lock is no longer held. Having a lock released while accessing shared resources is no better than having no locking at all.
|
|
|
|
 |
|