Hello, I have an issue with my C# code that calls an external procedure written in x64 ASM.
[DllImport("...", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr ApplyFilterToImageFragmentAsm(IntPtr bitmapBytes, int bitmapBytesLength, int bitmapWidth, int startIndex, int endIndex, IntPtr filteredFragment);
The project I am working on is a High Pass Image Filter (HP1) that is being put on a 24-bit bitmap image. I have an implementation of that algorithm written in C#, C++ and x64 ASM. The idea of this project is to run these synchronously and asynchronously, and compare the execution times based on the language used and the amount of threads specified.
When I call the C# or C++ methods asynchronously, everything works fine, the returned result is correct, there are no exceptions. The same is true when I call the ASM procedure synchronously - there are no exceptions, ever.
However, I am running into multiple issues when trying to run the procedure asynchronously, using tasks. What I am doing is creating Task objects in a loop, adding them to a list (to keep track of them), then awaiting all of them and synchronizing the results, something like this:
var listOfTasks = new List<Task<IntPtr>>();
for (int i = 0; i < threadCount; i++)
{
var task = Task.Run(() => ApplyFilterToImageFragmentAsm(bitmapBytesIntPtr, bitmapBytes.Length, bitmapWidth, startIndex, endIndex, filteredFragmentIntPtr));
listOfTasks.Add(task);
}
await Task.WhenAll(listOfTasks).ConfigureAwait(false);
When I run this code with threadCount higher than 1, I get the following problems:
1. Sometimes, the program just crashes without an exception.
2. Most of the times, I get an AccessViolationException - sometimes for an address that looks "legitimate" (eg. very close to the address that was passed as parameter), and sometimes for an address that is all zeroes or F's (keep in mind **this never happens when only using a single Task to execute the method**).
3. Some of the times I got an exception of type ExecutionEngineException, which (according to the documentation) is obsolete and not thrown by the runtime anymore, without any additional information.
4. Sometimes, there is no exception, but the returned result is wrong (eg. the image is distorted, the filter is only applied to some parts of it, etc.).
5. And some of the time, the algorithm works fine and returns the correct result, even for a higher number of Tasks like 2, 4 or 8.
I suspect the problem is related to the ASM code itself, in how the registers, stack and variables are shared among the same thread, which makes the code crash the program when multiple procedures are executed on the same thread (instead of separate threads).
My question is - **how can I make the ASM code handle multithreading properly, or how can I ensure the C# code executes different method calls on different threads?**
Important note - when I make the following modification, the code works correctly 100% of the time.
for (int i = 0; i < threadCount; i++)
{
var task = Task.Run(() => ApplyFilterToImageFragmentAsm(bitmapBytesIntPtr, bitmapBytes.Length, bitmapWidth, startIndex, endIndex, filteredFragmentIntPtr));
listOfTasks.Add(task);
task.Wait();
}
Therefore, I am almost certain the problem is caused exactly by how multithreading is handled - however, I need the procedure to run in parallel multiple times, so awaiting single calls is not an option for me. Neither is using Task.Delay to delay specific calls, since it would increase the execution time when run with multiple tasks, and that's against the point of this project. (besides, delaying doesn't make the code work 100% of the time anyway)
I can share more information (and the source code itself) if needed. Cheers.
[UPDATE]
Here's the rest of the code that does the Task creation and handling, without the part that synchronizes the results together (because the exception occurs before that part anyway). I added some comments to better explain what is going on:
var listOfTasks = new List<Task<IntPtr>>();
int index = 0;
int bytesPerPart = bitmapBytes.Length / threadCount;
bytesPerPart -= bytesPerPart % 3;
for (int i = 0; i < threadCount; i++)
{
int startIndex = index;
int endIndex = startIndex + bytesPerPart - 1;
if (i == threadCount - 1)
{
endIndex = bitmapBytes.Length - 1;
}
index = endIndex + 1;
byte[] bitmapCopy = new byte[bitmapBytes.Length];
for (int j = 0; j < bitmapBytes.Length; j++)
{
bitmapCopy[j] = bitmapBytes[j];
}
byte[] filteredFragment = new byte[endIndex - startIndex + 1];
for (int x = 0; x < endIndex - startIndex + 1; x++)
{
filteredFragment[x] = bitmapBytes[startIndex + x];
}
unsafe
{
fixed (byte* pointerToByteArray = &(bitmapBytes[0]))
fixed (byte* pointerToFilteredFragmentArray = &(filteredFragment[0]))
{
var bitmapBytesIntPtr = new IntPtr(pointerToByteArray);
var filteredFragmentIntPtr = new IntPtr(pointerToFilteredFragmentArray);
var task = Task.Run(() => ApplyFilterToImageFragmentAsm(bitmapBytesIntPtr, bitmapBytes.Length, bitmapWidth, startIndex, endIndex, filteredFragmentIntPtr));
listOfTasks.Add(task);
}
}
}
await Task.WhenAll(listOfTasks).ConfigureAwait(false);
What I have tried:
1. Awaiting each Task individually - this makes the code work 100% of the time, but goes against the idea of this project.
2. Creating new Threads instead of Tasks - this caused even more issues, I could not get the code to work at all because of constant exceptions or the methods returning zeroes.
3. Using Task.Delay for individual Tasks - this doesn't make the code work, and goes against the idea of this project again.