In the previous part of this mini-series, we have learned about some limitations of agents regarding memory and execution time. Although these restrictions are severe in their details, they are nothing new to developers on the phone. Memory usage caps also exist for normal applications, and we do have quite some time-based requirements there too. When you work with background agents, you're however also facing a new class of problems that is completely irrelevant and non-existing to normal app developers.
Part 3: Data Exchange
On Windows Phone, app developers were not able to benefit from real multi-tasking, where several applications can execute at the same time. Agents are not completely abandoning this concept, but somewhat ease that restriction by allowing you to run small amounts of code under stipulated conditions, in parallel to whatever app is in the foreground at the moment. One of the problematic side effects is that there's a chance your agent runs when the user's currently using your app, which has some unpleasant implications. Let's see how this needs to affect your software design, and why.
Usually you need some sort of mechanism to exchange information between your app and the background agent. For example, when your agent goes off to a web service to check for new data, you may want to enable users to configure what data is being checked for, or how often the agent actually does that. Obviously this needs to be done in the main application, because that's the only place you can present a user interface. In some cases you may also want to communicate the other way round, for example to inform the main application about something that happened in the agent the next time it is launched. Since agents and applications run in different processes and there's no built-in inter-process communication features, the logical consequence is to use a data store both the application and agent can access equally for that: isolated storage.
The Problem with Isolated Storage
A small recap: isolated storage is, like the name implies, an isolated and virtualized part of the available physical storage space on a device only your application can read from and write to – no other application installed on the device is able to do that:
With background agents this picture changes, because all of a sudden your storage can be accessed by multiple different processes at runtime:
Now imagine what happens when your application and the corresponding background agent run at the same time, and – worst case – access the shared isolated storage to exchange data and information. In extreme cases, this will result in data corruption or at least data inconsistencies, with all consequences: crashes, unexpected behavior, hard to track down bugs. What we need is a way to resolve this problem.
Note: this problem does not apply to Linq2Sql. It's safe to use this from both your app and the background agent without further precautionary measures.
Mutex to the Rescue
The problem of inter-process synchronization is nothing new; it's only a new issue on the phone since there's wasn't any real multi-tasking until now. This means that people have developed several solutions for these scenarios before, and of course the .NET Framework itself also provides ways to work safely in and with such a situation. The way Microsoft explicitly recommends (for example in this part of the MSDN documentation) is to use a mutex, which intentionally has been added for app developers in "Mango" for these reasons.
These synchronization objects are specifically suitable to being used by multiple processes, because ultimately they are maintained by and visible throughout the operating system (in the case of so-called named mutexes). If you're familiar with the threading concept of locks (monitors), then you can simply think of mutexes as a similar construct that can be used for threads across multiple processes. If you don't have any experience with these things, then simply think of a mutex as a way to guard access to a certain resource. Mutexes can be acquired by only one thread, and everybody else who also wants to access the resource then has to wait until the first one is finished with it.
If you want to learn more about all these concepts, you can find a nice online e-book about Threading in C# by Joe Albahari online for free; he also has details on mutexes.
A word of caution: Multi-threading and synchronization is a complex topic. Even when it seems simple in a certain situation, there's always some hidden danger or catch. In the case of mutexes, imagine for example a situation like this: one party acquires the mutex and then crashes unexpectedly because it failed to handle a certain error correctly, and as a consequence the mutex is never released (this is called an abandoned mutex). Silverlight on Windows Phone does a lot of work to shield you from some of the problems that usually arise with these objects (for example, it will deal with abandoned mutexes for you); however I hope you see what complex situation may result from using these synchronization constructs, and that it's more important than ever to have clean and robust code with them involved.
A pattern to use mutexes is the following:
var mutex = new Mutex(false, MutexName);
// do whatever needs synchronization,
// for example access a file in isolated storage
// error handling, if applicable
One noteworthy thing is the constructor arguments used here: the first one allows you to attempt acquisition of the mutex right away when you pass in true. This however is not recommended. If someone else has acquired the mutex before, then it will not be owned by the current thread as a result of the constructor call, and since there's no way to determine this situation, this will result in a lot of trouble. Use the pattern shown here that separates creation and the acquisition (WaitOne) into separate calls. More important things to consider when you're using mutexes are:
Successive threads that want to acquire the mutex will have to wait until the current operation has finished. This means that if you're performing an action that takes a long time (which using isolating storage often is) you're potentially blocking other threads and processes as long as it takes for you to finish that action. Imagine the following: your agent runs and successfully acquires the mutex, then operates on isolated storage files. If by coincidence your application has to do the same at the same time, and acquires the mutex from the UI thread, the whole application will turn unresponsive until your background agent has finished.
So above all you should minimize the time you hold the mutex at any cost. Do not perform unnecessary operations when you have acquired the mutex. Do as much work as possible before and after holding the mutex, and only synchronize what actually needs synchronization. Of course accessing isolated storage still is a slow operation and there's little you can do about that. In these cases:
- Use the overload of the WaitOne() method that takes the amount of milliseconds to wait for the mutex to be acquired. If acquisition fails it will return false, which gives you the chance to abort whatever you were trying to do, and re-try a few seconds later, for example.
- Move the code in question to a background thread or schedule it on the ThreadPool where it isn't critical when execution is blocked for a few seconds (like it is for the UI thread).
It is safe for one thread to acquire the mutex multiple times. This means that if you have two operations that need synchronization and that are guarded by the same mutex, calling WaitOne() again will not block the thread if it already has acquired the mutex before.
The important detail is that the runtime keeps track of the number of calls to WaitOne() for you; so again it's very important to write clean code and make sure that you call ReleaseMutex() the same number of times as WaitOne(), in all possible code execution paths.
As a general rule of thumb, you should try to avoid nested acquisitions if it's possible. The more complex you make the code that needs synchronization, the more likely it is that you will run into nasty threading bugs and constellations that you didn't take into account. Keep it simple.
Instead of creating an overly complex solution of synchronization between your application and the background agent, you should consider a simpler solution: is it really necessary that your agent is running at the same time like your app? Most likely the answer to that is no, so an alternative is to simply use a mutex to help you detect whether the app is currently running, and prevent execution of the agent at the same time completely.
The idea is to simply create an (empty) file in isolated storage whose presence signals to the agent that the app is currently running. If the agent sees that particular file, it simply terminates without actually performing its usual actions, and waits for the next scheduled execution. To write and read isolated storage to check for and create/delete the file, a mutex is used like shown above. The only logic you need to implement then is:
- In the Launching/Activation events of the phone application service, create the file.
- In the Closing/Deactivated and Unhandled Exception events, remove the file.
- In the agent, simply test if the file is present, or not.
This logic may be much easier to use if your synchronization demands are simply too complex to be implemented with reasonable effort.
"With great features comes great responsibility" – it's nice to finally have some sort of multi-tasking on the phone, even if it is tightly controlled and has a lot of limitations. However, this part of the series demonstrates that a new feature can also result in a whole new class of problems that you simply did not have to deal with before, at all. Although maybe chances are small that you'll often (ever?) run into the situation that your agent and app want to access the same resource at the same time, you should never use this as an argument to ignore the concepts shown here. When this situation comes, you'll likely have some real problems without proper preparation, to a point where corrupted or inconsistent data may prevent your application from launching completely.