Re:A Modern Guide to Threads
by IgorPartola on Friday December 02, 2011 @08:45AM
(#3303914)
I agree with TFA: actors model is the way to go if you can swing it. However, when you are implementing the actor model, you are limited by the throughput of each actor. If the service that needs to be performed by the actor is CPU bound (say file compression), your overall throughput will also suffer. In this case a work queue and a set of workers may be more efficient.
I disagree with the statement that programming with plain threads and locks is somehow archaic. No, it is simply programming at a lower level of abstraction. In some cases it is more efficient and your specifications may require it. Think of state-heavy applications, and the interface between the module modifying internal program state (which may be done using actors or STM) and the module that performs external IO. In this case, you may need a lock to get a consistent snapshot of the data from your program's state to export it to an external source and in some cases it is more efficient to do so by locking the structure in memory.
Finally, locks don't always grow your complexity exponentially. What grows your complexity is when you have many different types of locks. Let's say we have an address book database program that implements concurrency by using threads. It is simple and inefficient to have a single global write lock. On the other end of the spectrum you have the ability to lock any field of any record, or the whole record, or a set of records, or a set of fields of a set of records, or records that don't even exist yet (gap lock) or the whole database. In-between, you have the ability to place a lock on the entire database or on just one record. You have to make the tradeoff between your time as a programmer and the efficiency of the code.
STM is a cool tool, but does have performance penalties [1]. It may be appropriate in some cases, and inappropriate at others, just like the actor model.
Re:A Modern Guide to Threads
by obtu on Friday December 02, 2011 @09:27AM
(#3304041)
That lower level of abstraction shouldn't be exposed in all languages, though; languages with complex instructions and garbage collection tend to be better implemented with a GIL, and it is a good idea to discourage the use of threads when these features are present. Your example of snapshotting is well addressed by forking, which eliminates the need for locks in unrelated parts of the code.
Re:A Modern Guide to Threads
by IgorPartola on Friday December 02, 2011 @09:46AM
(#3304112)
Forking isn't always a good option. Imagine that your have a lot of state, and serializing it is inefficient. All of a sudden shared memory becomes a very attractive way to communicate between different modules of your program, as compared to serializing and pushing data over a pipe.
As for the GIL, while it may be convenient for organizing the codebase of the language, it is awful when it comes to using it for a heavily parallel workload. Yes, it prevents programmers from having to write code like:
with obj.lock:
obj.counter += 1
But it means that code that should by all means be lock free is now always blocking all threads. In other words, it helps really novice programmers write really simple single-user single use prototype-level code, but prevents concurrency in real-world production environments.
Locks should be exposed in most languages (except environments like JavaScript which are inherently single-threaded). It's just that (1) other models such as STM should be exposes as well and (2) other abstractions built on top of or related to locks (queues, events, conditions, etc). should be available as well.
Re:A Modern Guide to Threads
by rsanders on Friday December 02, 2011 @09:53PM
(#3306742)
What about JavaScript makes it inherently single-threaded? I would describe it as more of a counter-example. There's nothing preventing a multi-threaded JavaScript except for the implementor's choice to avoid it. When the need for parallel computation became strong enough, Web Workers were created with a shared-nothing, message-passing design.
I'm no fan of GILs, but I've been burned enough times with explicit locking that I respect any attempt to find a Third Way. And why provide a lock facility if the third way is a truly usable alternative?
Re:A Modern Guide to Threads
by IgorPartola on Saturday December 03, 2011 @08:07AM
(#3307653)
Implementer's choice is the only reason. What I am saying is that since the implementer chose to make it single-threaded, they should choose not to include unnecessary locking facilities.
While locking can sometimes be a pain to reason about (having many types of locks affecting access to one piece of data, etc.), on a lot of situations they are very useful. The GIL is exactly the opposite: easy to reason about, but rarely will it save you. Consider.
x = cache['foo']
y = bar(x)
cache['foo'] = y
Here a GIL will not help here: while lines 1 and 3 are atomic and the call to bar() may or may not be atomic, there is no guarantee that the whole set of operations is atomic. Thus you need an explicit locking mechanism. A GIL is a pattern that makes implementation of interpreters simple. It does not help in problems where you may be burned by locks.
Re:A Modern Guide to Threads
by regularfry on Friday December 02, 2011 @10:12AM
(#3304207)
Abstractions leak. By all means provide them, but don't assume that a sophisticated user won't complain if they can't get at the guts when the abstraction (or its implementation) gets in their way.
Re:A Modern Guide to Threads
by IgorPartola on Saturday December 03, 2011 @08:14AM
(#3307667)
Agreed. However I have never seen anyone complain about how a mutex, a semaphore, etc. were implemented under the hood. We are not talking about ORMs or artificial AI here.
On top of that, my point is that providing abstractions and the low-level interfaces is better than not providing either, or just one of the two. If a sophisticated user is not happy with how something is implemented they always have the option to implement an abstraction that eill work for them.
Basically, Win32 wants all threads to die out before the process exits, but the C runtime will exit out the process for you when your "main" thread dies.
I disagree with the statement that programming with plain threads and locks is somehow archaic. No, it is simply programming at a lower level of abstraction. In some cases it is more efficient and your specifications may require it. Think of state-heavy applications, and the interface between the module modifying internal program state (which may be done using actors or STM) and the module that performs external IO. In this case, you may need a lock to get a consistent snapshot of the data from your program's state to export it to an external source and in some cases it is more efficient to do so by locking the structure in memory.
Finally, locks don't always grow your complexity exponentially. What grows your complexity is when you have many different types of locks. Let's say we have an address book database program that implements concurrency by using threads. It is simple and inefficient to have a single global write lock. On the other end of the spectrum you have the ability to lock any field of any record, or the whole record, or a set of records, or a set of fields of a set of records, or records that don't even exist yet (gap lock) or the whole database. In-between, you have the ability to place a lock on the entire database or on just one record. You have to make the tradeoff between your time as a programmer and the efficiency of the code.
STM is a cool tool, but does have performance penalties [1]. It may be appropriate in some cases, and inappropriate at others, just like the actor model.
[1] http://en.wikipedia.org/wiki/Software_transactional_memory#P...
As for the GIL, while it may be convenient for organizing the codebase of the language, it is awful when it comes to using it for a heavily parallel workload. Yes, it prevents programmers from having to write code like:
But it means that code that should by all means be lock free is now always blocking all threads. In other words, it helps really novice programmers write really simple single-user single use prototype-level code, but prevents concurrency in real-world production environments.Locks should be exposed in most languages (except environments like JavaScript which are inherently single-threaded). It's just that (1) other models such as STM should be exposes as well and (2) other abstractions built on top of or related to locks (queues, events, conditions, etc). should be available as well.
I'm no fan of GILs, but I've been burned enough times with explicit locking that I respect any attempt to find a Third Way. And why provide a lock facility if the third way is a truly usable alternative?
While locking can sometimes be a pain to reason about (having many types of locks affecting access to one piece of data, etc.), on a lot of situations they are very useful. The GIL is exactly the opposite: easy to reason about, but rarely will it save you. Consider.
Here a GIL will not help here: while lines 1 and 3 are atomic and the call to bar() may or may not be atomic, there is no guarantee that the whole set of operations is atomic. Thus you need an explicit locking mechanism. A GIL is a pattern that makes implementation of interpreters simple. It does not help in problems where you may be burned by locks.On top of that, my point is that providing abstractions and the low-level interfaces is better than not providing either, or just one of the two. If a sophisticated user is not happy with how something is implemented they always have the option to implement an abstraction that eill work for them.
This is true on Unix/Linux. I believe this is not true on Windows. (I'm not a Windows dev, so feel free to enlighten me if I am wrong.)
Everything else in the article seemed pretty OS-independent.
http://msdn.microsoft.com/en-us/library/h339syd0.aspx
EDIT: It seems that in win32 the process does indeed die when the last thread exits. See http://msdn.microsoft.com/en-us/library/windows/desktop/ms68...
http://blogs.msdn.com/b/oldnewthing/archive/2010/08/27/10054...
Basically, Win32 wants all threads to die out before the process exits, but the C runtime will exit out the process for you when your "main" thread dies.