As described in my other recent post, I'm trying to write a C# program that sends a message on a serial port and waits for a reply which should come within 10 or 15 milliseconds, after which another message could be sent. If no reply comes, an ack timeout timer event initiates the sending of another message. There is also a timer event that puts messages in the queue of messages to be sent (function add_message_to_queue below). Hence there are three different threads (possibly on different CPU cores) that can call SendNextMessage.
Is the (semi-pseudo) code below correct from a "memory visibility" point of view? e.g. suppose the reply_received function runs and clears the wait_ack flag because there's no new message to send, when the ack_timeout function runs (on a different thread), will it see the true value of the wait_ack flag or could it see a stale value that just happens to already be in the level one cache associated with that thread. If so, do I have to use InterlockedExchange or similar to get around this? If the code is correct as it stands, is it because the C# lock function involves a memory barrier - and if so, how does that solve the "stale cache" problem?
Secondly, how can I solve the following problem. Suppose that a late reply arrives at the same time as the ack_timeout occurs and the late reply acquires sendMessageLocker just before the ack timeout. This means that the ack timeout thread is sitting waiting for the sendMessageLocker lock and as soon as it gets it, it will generate a spurious ack timeout and possibly send another message before the reply to the first message has had a chance to arrive. I can probably force the ack timeout time to be longer than any possible late arrival but I would like the code to handle the situation if possible.
Is there any easier way to do this that avoids the tricky problems above? e.g. I could make SendNextMessage a thread of its own that gets woken up by the other three threads which no longer need to lock sendMessageLocker.
Is the (semi-pseudo) code below correct from a "memory visibility" point of view? e.g. suppose the reply_received function runs and clears the wait_ack flag because there's no new message to send, when the ack_timeout function runs (on a different thread), will it see the true value of the wait_ack flag or could it see a stale value that just happens to already be in the level one cache associated with that thread. If so, do I have to use InterlockedExchange or similar to get around this? If the code is correct as it stands, is it because the C# lock function involves a memory barrier - and if so, how does that solve the "stale cache" problem?
Secondly, how can I solve the following problem. Suppose that a late reply arrives at the same time as the ack_timeout occurs and the late reply acquires sendMessageLocker just before the ack timeout. This means that the ack timeout thread is sitting waiting for the sendMessageLocker lock and as soon as it gets it, it will generate a spurious ack timeout and possibly send another message before the reply to the first message has had a chance to arrive. I can probably force the ack timeout time to be longer than any possible late arrival but I would like the code to handle the situation if possible.
Is there any easier way to do this that avoids the tricky problems above? e.g. I could make SendNextMessage a thread of its own that gets woken up by the other three threads which no longer need to lock sendMessageLocker.
Code:
private void reply_received()
{
lock (sendMessageLocker)
{
reply_received_flag = true;
SendNextMessage();
}
}
private void ack_timeout()
{
lock (sendMessageLocker)
{
if (wait_ack)
{
ack_timeout_occurred = true;
SendNextMessage();
increment_ack_timeout_counter();
}
}
}
private void add_message_to_queue()
{
// ... add to queue
lock (sendMessageLocker)
{
SendNextMessage();
}
}
private void SendNextMessage()
{
// the caller must lock
//lock (sendMessageLocker)
//{
if (reply_received_flag || ack_timeout_occurred || !wait_ack)
{
reply_received_flag = false;
restart_ack_timeout_timer();
ack_timeout_occurred = false;
wait_ack = false;
if (anything_to_send)
{
restart_ack_timeout_timer();
send_message();
wait_ack = true;
}
}
else if (wait_ack)
{
// somehow check that the ack timeout timer event didn't get lost
}
//}
}