In my previous blog entry, I went through techniques to create a thread safe class. However, I also pointed out that correctly implementing thread safety in your code is only part of solving the problem. The first and most important step in creating a thread safe application is to lay down the threading model for your application. There are multiple ways of insuring thread safety, and it is critical to make the right choices early in the process because a lot of the code you write will depend on these choices. I would even say that it is more important to get these choices stabilized than most other aspects of the design of your application. Multiple threads complicate your application significantly, but if they are necessary to your application (e.g. responsiveness of your Swing application), you should not forego them first, to add them later. Adding thread safety to existing code, is a lot more error prone than starting from scratch with thread safety in mind. In this entry I will go through a few ways to handle threading in your application.
First, we are going to make the assumption that you have divided you application into components. These components can so basic as model and presentation components, but you most probably grouped your design elements into larger blocks of functionality with well defined responsibilities. We’ll also assume in our definition of a component that they have a certain size and several access points. For example, a Swing component is not a component in this sense, a component handling all communication with an external data source would qualify. Usually, only components are worth considering when putting together the threading model of your application. What you want to define is what happens at the boundaries of your component, what happens inside the component should in essence be irrelevant to your application (This is not true when it comes to resource management, but for now we will keep that assumption). There are three basic ways to manage threading on the boundaries of your application, all other ways I’ve seen so far could be expressed in terms of these.
- Thread Confinement.
- External synchronization mechanisms.
- Internal synchronization.
Thread confinement is probably the easiest to implement, and should be one of the first things to cross your mind. The principle of thread confinement is that you only allow predefined threads to access your component. The Swing framework is an obvious example of thread confinement; you are only allowed to access Swing classes from the Event Dispatch Thread. Implementing this is fairly easy and the only synchronization point you have to worry is when other threads need to access the component threads to post a request. When I say component threads, it is to imply that unlike Swing you are not forced to have only one thread. For example, you can divide your component internally and define which component thread is allowed to access which part, then depending on the nature of the request from the client you would dispatch it to the appropriate thread. This tends to make things a lot easier when coding your component because you do not have to worry so much about synchronization on each part of your code, because you know exactly which thread is going to access what. This might also perform pretty well because there is no thread contention either. Of course, this is more or less reverting to a sequential execution model with all the limitations that this implies. The most obvious ones being possible resource starvation and poor scalability, if one client post a big job on your component thread, then all the other clients might be starved out and your component becomes the point of contention of the entire system. Another problem arises because the integrity of your component depends on well behaved clients, if a client decides to call elements from their own thread; there is probably little you can do about it short of putting checks on all access points of your component (hint: use aspects). Using thread contention makes things easier when coding your component and it does make sense to use when your component is limited by other resources anyway. For example, a printer driver can never go faster than the printer, so why bother having a whole bunch of threads accessing it when only one can actually print.
An external synchronization mechanism is a little bit like thread confinement, but you don’t define specific threads to be the only ones able to enter your code. In this case, you provide the clients with a lock they can take prior to accessing your component. For example, if you have a component providing data to all other components, imagine the data is structured as a database and you have data tables, you could provide a lock() method on a table object. Any client wanting to access that table would have to first get the lock() on the table and release when done. This would ensure that only one thread accesses this table concurrently. However, this might not be enough. What if a client wants to operate on three tables and it needs exclusive access to all of them. In that case, you could introduce a lock manager that could have a lockTables() method. This is probably the most useful feature of an external synchronization mechanism; it allows your clients to synchronize access to several resources in your component in a single operation. In some cases, this might be your only option because the client is the only one with the knowledge of what it is going to access, and you need to ensure data consistency across all accesses. This also (potentially) gives a finer grained resource management than thread confinement, the client actually tells you what it is going to access potentially giving you the possibility to optimize your mutual exclusion strategy. Plus implementation wise you can start with a lock manager, that is just a global lock and if that gives sufficient performance stop there. The downside is that you force the clients to first remember to take a lock, then to tell you what they are going to access. This can be error prone, what if the client forgets to lock everything it needs or someone changes the code to access a new resource but forgets to update the locking call?
An external synchronization mechanism that you expose to your clients does not have to actually lock anything; you could try and implement the hipped transactional memory or some transactional scheme. What that means is that several client threads are allowed in the component, and they all do what they need to do and when done they try to commit. Committing will:
1. Check that no other thread changed any of the data accessed by the committing thread.
2. Make the changes from the committing thread visible to other clients.
If step 1 fails, an option is to tell the client to rewind and try again, if it so desires. This gives an all or nothing semantics much like databases which can be very useful in some instances. This should also give a very good throughput because no thread is ever pre-empted from working by any locks (except when it needs to commit). However, if too many threads are walking on each others feet, you will end up doing a lot of work twice or actually never manage to get it done, leading to starvation for some clients. Another advantage is that the client does not need to tell in advance what it is going to access; it just needs to mark the limits of the transaction.
Using internal synchronization, you do not expose any synchronization mechanism to the clients and allow any number of threads to concurrently make calls to the component. It is then up to you to synchronize the internals of your component to ensure correctness. This is the easiest for the clients since they do not have to worry about any threading issues. However, internally in your component you’ll have to synchronize everything that several threads can touch. One example of this is the first few versions of the AWT framework, which actually allowed any threads to call it. This was however short lived, because the developers quickly found out that this was way too hard to get right and they moved to a thread confinement design instead. While this approach can offer many advantages in terms of scalability and ease of use for the clients, experience tells that it should be limited to fairly simple components. Once a component has reached a certain size, the task becomes overwhelming. For the client, the main problem with this approach is that it cannot assure consistency across multiple calls to the component like the external lock could.
So what to choose for your components? Well, there really isn’t any simple answer to that. What you should choose for a component is not only influenced by the component itself, but also by the application as a whole. You need to make sure that no component will be a bottleneck when going live. Threading allows for better scalability, but they also complicate things and if your thread spends more time synchronizing than working then the advantages are effectively nullified. Each technique for threading presented here has advantages and disadvantages that need to be balanced. Also there are limitations, like consistency across multiple calls. When it comes to it, the most important thing might be, to actually have a plan; you won’t get it a 100% right from the start, but it is a lot easier to make changes to a predefined well known model, than to a de facto model that arose from the implementation. As for all software, keep it simple and don’t be a feature/performance freak, if you don’t need it now don’t implement it, if you think a single thread can do it, keep it to that.
No comments:
Post a Comment