Click here to Skip to main content
15,881,881 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
Hello, I have an issue with my C# code that calls an external procedure written in x64 ASM.

C#
[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:

C#
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.

C#
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:


C#
var listOfTasks = new List<Task<IntPtr>>();

int index = 0;
int bytesPerPart = bitmapBytes.Length / threadCount;	// bitmapBytes is the input array of type byte[] (without bitmap header)
// each value represents one color of one pixel in this order: R, G, B, ...

bytesPerPart -= bytesPerPart % 3;	// Calculating the amount of pixels per function call, and making sure it's divisible by 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];	// Making a copy of the input array, so that the same memory isn't accessed by multiple threads by the same time

	for (int j = 0; j < bitmapBytes.Length; j++)
	{
		bitmapCopy[j] = bitmapBytes[j];
	}

	byte[] filteredFragment = new byte[endIndex - startIndex + 1];	// Output array, also passed as parameter to the procedure

	for (int x = 0; x < endIndex - startIndex + 1; x++)
	{
		filteredFragment[x] = bitmapBytes[startIndex + x];
	}

	unsafe
	{
		fixed (byte* pointerToByteArray = &(bitmapBytes[0]))		// Pointer to input array
		fixed (byte* pointerToFilteredFragmentArray = &(filteredFragment[0]))	// Pointer to output array
		{
			var bitmapBytesIntPtr = new IntPtr(pointerToByteArray);	// Converting from byte* to IntPtr
			var filteredFragmentIntPtr = new IntPtr(pointerToFilteredFragmentArray);	// Converting from byte* to IntPtr

			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.
Posted
Updated 10-Dec-21 4:15am
v2
Comments
Richard Deeming 10-Dec-21 9:34am    
Almost certainly something to do with the way you're getting the pointer to the bytes - there's a chance the CLR could move the object whilst your task is running in the background, which would make the pointer invalid.

If you update your question to show the missing part of your for loop, it might be more obvious where the problem is.
Stukeley 10-Dec-21 10:02am    
Thank you for your answer. I updated my question with the relevant part of my function that creates the task, and the for loop.
Richard Deeming 10-Dec-21 10:04am    
That'll do it - the code exits the fixed block before the task has finished, which means your array can be moved around whilst the unmanaged code is trying to access it.
Stukeley 10-Dec-21 10:10am    
What would you recommend? How could I keep the pointers fixed for the entire duration of the Task execution?
Richard Deeming 10-Dec-21 10:15am    
See solution 1. :)

1 solution

As I suspected, the problem is that your unmanaged code is running outside of the fixed blocks, so the CLR is free to move the memory around. The pointer you pass in will be pointing to the wrong thing.

Since you can't await inside a fixed block, you'll need to move those blocks to another method, which will be the target of your Task.Run:
C#
private static void ApplyFilterToImageFragment(byte[] bitmapBytes, int bitmapWidth, int startIndex, int endIndex, byte[] filteredFragment)
{
	unsafe
	{
		fixed (byte* pointerToByteArray = &(bitmapBytes[0]))		// Pointer to input array
		fixed (byte* pointerToFilteredFragmentArray = &(filteredFragment[0]))	// Pointer to output array
		{
			var bitmapBytesIntPtr = new IntPtr(pointerToByteArray);	// Converting from byte* to IntPtr
			var filteredFragmentIntPtr = new IntPtr(pointerToFilteredFragmentArray); // Converting from byte* to IntPtr
            ApplyFilterToImageFragmentAsm(bitmapBytesIntPtr, bitmapBytes.Length, bitmapWidth, startIndex, endIndex, filteredFragmentIntPtr);
		}
	}
}
C#
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];
    Array.Copy(bitmapBytes, 0, bitmapCopy, 0, bitmapBytes.Length);

	byte[] filteredFragment = new byte[endIndex - startIndex + 1];
    Array.Copy(bitmapBytes, startIndex, filteredFragment, 0, filteredFragment.Length);
    
    var task = Task.Run(() => ApplyFilterToImageFragment(bitmapCopy, bitmapWidth, startIndex, endIndex, filteredFragment));
    listOfTasks.Add(task);
}

await Task.WhenAll(listOfTasks).ConfigureAwait(false);
 
Share this answer
 
Comments
Stukeley 10-Dec-21 11:01am    
Thank you for your time! This did indeed fix the issue with the exceptions.
The algorithm now runs to the end every time.
However, what's weird still is that nearly every time, the returned result is not correct - the output image is distorted, or the filter is not applied to the entirety of it.
I have checked using the debugger, and the arrays are copied properly, and the tasks are initialized (and later reassembled) in the correct order.
When I use task.Wait() in the loop, the output once again is correct 100% of the time.
I think there might still be some problems on the ASM side, but I need to do some more testing to make sure of that.

Thank you very much for the answer!

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900