Today we are going to look at a relatively rare case of synchronization. There are no appropriate C# synchronization primitives out of the box.
In order to understand the case we’re going to talk about you can imagine the following case: “Your code makes a call to the third-party library’s method and you have to wait that method until the end. That method performs a sequence of operations and at each step it will invoke a callback. In addition, by the agreement that method takes a timeout which is a max amount of time allocated for each step. After a callback is invoked, timeout will be reset. If you passed in 60 seconds as a timeout, then that third-party’s method will take 60 seconds for each step (if there are three steps, then 3*60 sec. is a maximum time amount may be spent on the overall execution).”
Consider the example where the Client is waiting for the Processor spinning around using UpdateableSpin synchronization primitive:
[code language=”csharp”]
class Program {
private static void Main() {
for (int i = 0; i < 5; i++) {
var client = new Client();
client.Do();
Console.WriteLine("The end.");
Console.WriteLine("————-");
}
Console.ReadLine();
}
}
class Client {
private readonly UpdateableSpin spin;
private readonly Processor processor;
public Client() {
processor = new Processor();
processor.ResponseReceived += response => {
spin.UpdateTimeout();
Console.WriteLine(response);
};
processor.ProcessingFinished += () => spin.Set();
spin = new UpdateableSpin(shouldWait: true);
}
public void Do() {
var timeoutRandomizer = new Random((int) DateTime.Now.Ticks);
var timeout = timeoutRandomizer.Next(2, 5);
Console.WriteLine("Start processing.");
processor.AsyncProcess(timeout);
Console.WriteLine("Waiting.");
//just in case give third-party code a little bit more time
const int offset = 5;
int maxTimeoutToWait = timeout + offset;
spin.Reset();
if (!spin.Wait(TimeSpan.FromSeconds(maxTimeoutToWait), spinDuration: 5)) {
Console.WriteLine("Timed out!");
}
else {
Console.WriteLine("Finished in time.");
}
}
}
class Processor {
private readonly Random taskDurationRandomizer;
public delegate void Response(string response);
public delegate void Finished();
public event Response ResponseReceived;
public event Finished ProcessingFinished;
public Processor() {
taskDurationRandomizer = new Random();
}
public void AsyncProcess(int timeout) {
Task.Factory.StartNew(() => {
bool willNeverResponse = timeout%2 != 0;
const int stepsNumber = 3;
for (int i = 0; i < stepsNumber; i++) {
DoStep(timeout);
//oops, something went wrong, third-party will not respond
if (willNeverResponse && i == stepsNumber – 1) {
Thread.Sleep(int.MaxValue);
}
ResponseReceived(string.Format("Important results of step {0}", i));
}
ProcessingFinished();
});
}
private void DoStep(int timeout) {
var duration = taskDurationRandomizer.Next(timeout);
//do actual work
Thread.Sleep(duration);
}
}
public class UpdateableSpin {
private readonly object lockObj = new object();
private bool shouldWait;
private long taskExecutionStartingTime;
public UpdateableSpin(bool shouldWait) {
this.shouldWait = shouldWait;
}
public bool Wait(TimeSpan executionTimeout, int spinDuration = 0) {
UpdateTimeout();
while (true) {
lock (lockObj) {
if (!shouldWait) {
return true;
}
if ((DateTime.UtcNow.Ticks – taskExecutionStartingTime > executionTimeout.Ticks)) {
return false;
}
}
Thread.Sleep(spinDuration);
}
}
public void UpdateTimeout() {
lock (lockObj) {
taskExecutionStartingTime = DateTime.UtcNow.Ticks;
}
}
public void Reset() {
lock (lockObj) {
shouldWait = true;
}
}
public void Set() {
lock (lockObj) {
shouldWait = false;
}
}
}
[/code]
Yes, I know that spinning for a long time (more than 1 millisec, not to say that we use lock-statements there) sounds a little bit silly, cause the actual benefit of spinning is in avoiding locks which incur costly context-switching. But it seems we can’t do anything about that in this particular case. Spins are very rarely used constructs, for example in LinkedList supporting multithreading. In our example we could use CPU-intensive Thread.SpinWait() instead of sleeping in order to gain extremely precise timing, but as I already mentioned this is rarely the case we should think about when talking about spinning more than one millisec or even more (not to say about seconds). But, of course this example can be modified in a way to provide very precise timing.