I am using .Net version 4.6.1 and VS2015 Update 3.
I am facing a weird issue as a piece of code is working fine when compiled in Debug mode, however, it fails with NullReferenceException when compiled in Release mode.
I have an async method which uses TaskCompletionSource to wait for task completion and I am again retrying after 5 seconds if response is not received. I have reproduced this error in the simplified sample code given below.
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TaskCompletionIssue
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
#if DEBUG
this.Text = "Running in Debug Mode";
#else
this.Text = "Running in Release Mode";
#endif
}
private async void btnStartTest_Click(object sender, EventArgs e)
{
try
{
this.Cursor = Cursors.WaitCursor;
await sendRequest("Test");
MessageBox.Show("Test Completed Successfully");
}
finally
{
this.Cursor = Cursors.Default;
}
}
private static TimeSpan secondsToWaitBeforeRetryingRequest = TimeSpan.FromSeconds(5);
private static TimeSpan secondsToWaitForResponse = TimeSpan.FromSeconds(180);
internal static readonly ConcurrentDictionary<Guid, TaskCompletionSource<object>> ClientResponses = new ConcurrentDictionary<Guid, TaskCompletionSource<object>>();
private static Thread t1 = null;
public async static Task<object> sendRequest(String req)
{
var tcs = new TaskCompletionSource<object>();
Guid requestId = Guid.NewGuid();
ClientResponses.TryAdd(requestId, tcs);
try
{
DateTime startTime = DateTime.Now;
while (true)
{
//Call method to send request, It doesn't block the thread
SendRequestForProcessing(requestId, req);
if (tcs == null)
{
MessageBox.Show("tcs is null");
}
var task = tcs.Task;
//Wait for the client to respond
if (await Task.WhenAny(task, Task.Delay(secondsToWaitBeforeRetryingRequest)) == task)
{
return await task;
}
else
{
if ((DateTime.Now - startTime).TotalSeconds > secondsToWaitForResponse.TotalSeconds)
{
throw new TimeoutException("Could not detect response within " + secondsToWaitForResponse.TotalSeconds.ToString() + " secs.");
}
else
{
//Let's try again, Previous call might be lost due to network issue
}
}
}
}
finally
{
// Remove the tcs from the dictionary so that we don't leak memory
ClientResponses.TryRemove(requestId, out tcs);
}
}
private static void SendRequestForProcessing(Guid requestId, string req)
{
//Not doing anything with request as this is just a sample program
if (t1 == null || !t1.IsAlive)
{
t1 = new Thread(receivedResponse);
t1.Name = "Test";
t1.IsBackground = true;
t1.Start(requestId);
}
}
public static void receivedResponse(object id)
{
TaskCompletionSource<object> tcs;
Guid requestId = (Guid)id;
if (ClientResponses.TryGetValue(requestId, out tcs))
{
//Some static wait in sample program
Thread.Sleep(TimeSpan.FromSeconds(15));
// Trigger the task continuation
tcs.TrySetResult("Test Success");
}
else
{
throw new Exception($"Request not found for id {requestId.ToString()}");
}
}
}
}
In the above code sample, I have shown a message if variable 'tcs' becomes null. I get the error message when the code is compiled in Release mode; nonetheless, it works fine in Debug mode.
Furthermore, To fix this issue, I have simply moved the below line of code outside the try block and everything is working fine.
var task = tcs.Task;
It appears some kind of .Net bug to me.
Can anyone please help me to understand this awkward behavior?
Edit 1:
Well, this issue is kind of hard to believe, thus, I have created a working sample project reproducing this issue. Please download it from the below link and compile the code in both Debug and Release mode.
After the compilation, please run the executable file in both the modes one by one.
In Debug mode, the test should complete after 15 seconds, however, it will show a message that 'tcs' variable is null in release mode and on pressing Ok button it will throw a NullReferenceException.