0

I'm trying to make an application with GUI (WinForms) that reads data from an FTDI chip via USB and doesn't cause the UI to freeze while reading.

The application window is no longer responsive. The buttons I added to the window don't work, nor does minimize, maximize, close with a cross.... nothing related to the window works. But I can see in the application debugging window in Visual Studio that data is being received.

The data reading is based on an Event from the FTDI driver: ftHandle.SetEventNotification(FTDI.FT_EVENTS.FT_EVENT_RXCHAR, receivedDataEvent);.

It would be appropriate for the reception of this data to work in a separate thread, so as not to block the GUI. I have read that in 90% of cases the best way to handle threads is Tasks. Unfortunately, the following code snippet reads the data, but the UI blocks during this time. How to solve this problem?

This code is executed once, right after the FTDI is initialized to work:

var receivedDataEvent = new AutoResetEvent(false);

ftHandle.SetEventNotification(FTDI.FT_EVENTS.FT_EVENT_RXCHAR, receivedDataEvent);

// Create a new task to receive data
Task dataReceiveTask = new Task(async () =>
{
    while (true)
    {
        // Wait for the FT_EVENT_RXCHAR event
        receivedDataEvent.WaitOne();

        await Task.Run(() =>
        {
            ReadDataAvailableInFtdiBuffer();
        });                    
    }
});

// Start the data receive task
dataReceiveTask.Start();

And the ReadDataAvailableInFtdiBuffer(); is:

void ReadDataAvailableInFtdiBuffer()
{
    ftStatus = ftHandle.GetRxBytesAvailable(ref numBytesAvailable);
    if (numBytesAvailable < 1)
        return;
    System.Diagnostics.Debug.WriteLine(ftStatus + "  bytes available: " + numBytesAvailable.ToString());

    byte[] bytes = new byte[numBytesAvailable];
    UInt32 numBytesRead = 0;
    ftHandle.Read(bytes, numBytesAvailable, ref numBytesRead);

    // --------------------------------------------------------------------------------------------
    // Calling Received Data Event with passing data to the event subscriber (USB)
    // OnFtdiBytesReceived?.Invoke(this, new FtdiBytesReceivedEventArgs(bytes, numBytesAvailable));
    // --------------------------------------------------------------------------------------------
}

EDIT: using Task.Run as suggested in comments:

var receivedDataEvent = new AutoResetEvent(false);          
ftHandle.SetEventNotification(FTDI.FT_EVENTS.FT_EVENT_RXCHAR, receivedDataEvent);

// Create a new task to receive data
Task dataReceiveTask = Task.Run(() =>
{
    while (true)
    {
         // Wait for the FT_EVENT_RXCHAR event
         receivedDataEvent.WaitOne();
         ReadDataAvailableInFtdiBuffer();                                
    }
});

But the UI behaviour is the same.

EDIT2: SetEventNotification function description

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
bLAZ
  • 1,689
  • 4
  • 19
  • 31
  • This won't really fix your problem, but you have a task that is just sitting idle waiting for the `receivedDataEvent` and then as soon as that triggers you are scheduling a new Task to be run on potentially a different thread instead of just using the thread that you were already using waiting for the event? It just seems like it would make more sense to just run `ReadDataAvailableInFtdiBuffer` without the `await Task.Run` – TJ Rockefeller Dec 28 '22 at 15:39
  • Thanks! Actually what you write makes more sense. I tried it, but unfortunately the UI remains "dead" while receiving data. – bLAZ Dec 28 '22 at 15:44
  • 1
    In C#, `Tasks` are hot. As soon as you call `Task dataReceiveTask = ...`, the `Task` is already running, or is at least scheduled for execution and will begin running soon. Replace `dataReceiveTask.Start();` with `await dataReceiveTask;` and see if that works. EDIT: actually, consider putting the `dataReceiveTask` in its own asynchronous method, then simply calling it, instead of declaring a `new Task()`; – Patrick Tucci Dec 28 '22 at 15:45
  • This also is not an ideal way to schedule what appears to be a long-lived background service. Consider putting this in its own class. Additionally, consider implementing some disposal logic that cancels the infinite `while` on exit. – Patrick Tucci Dec 28 '22 at 15:47
  • I'm kind of vague on using `Start` for tasks because it says that it will start the task using the current task scheduler https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.start?view=net-7.0, which in this case may be the UI scheduler, so your `dataReceiveTask` may be running on the UI thread? Have you tried using Task.Run because that should use the default scheduler which will be the thread pool scheduler instead of the UI scheduler. – TJ Rockefeller Dec 28 '22 at 15:50
  • @PatrickTucci thanks. I am aware that I can apply Cancellation Token to Tasks to cancel this while. I will add it as soon as I can make a working UI. – bLAZ Dec 28 '22 at 15:58
  • @TJRockefeller I just read about this difference in running Task between Start and Run and indeed, as you write, because of the scheduler it is better to use Run. Unfortunately, I have no idea how to verify in Visual Studio if a function runs in a separate thread and if so, in which one. – bLAZ Dec 28 '22 at 16:01
  • The following may be of interest: [Task-based asynchronous pattern (TAP) in .NET: Introduction and overview](https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap),[TPL and traditional .NET asynchronous programming](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/tpl-and-traditional-async-programming) and [Task Parallel Library (TPL)](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl) – Tu deschizi eu inchid Dec 28 '22 at 16:08
  • Where is this code located? Is it in the Form_Load event handler of the main form? – Theodor Zoulias Dec 28 '22 at 16:14
  • 1
    Why is `AutoResetEvent` used? That's what blocks, in code that wasn't posted. Not the background task. Are you using a USB Serial port using an FTDI chip? Why not use the `SerialPort` class then? There's no reason to try and use either the Win32 API or a custom API. You're paying for a serial port, which means it should just work as a serial port. If the vendor has a bug, it's their job to fix it – Panagiotis Kanavos Dec 28 '22 at 16:16
  • I say `if the vendor has a bug they should fix it` because that's exactly what happened about ... 18 years ago with another FTDI-based serial port. It didn't automatically handle RTS/CTS the way it should due to a driver bug. The vendor was persuaded to fix this because even the MSDN code example didn't work – Panagiotis Kanavos Dec 28 '22 at 16:20
  • @TheodorZoulias thanks for edits. It is not in the Form_Load handler. In the constructor of the main Form, I have an object created and its method regarding the initialization of this object is also called there. In this initialization, another object (USB) is created and initialized and there the FTDI chip is configured for operation and this Task is created. So there is more or less this type of hierarchy: Form -> Object1 -> USBhandler -> FTDI – bLAZ Dec 28 '22 at 16:22
  • @PanagiotisKanavos I'm not using it as serial port, but in fast Fifo mode. And it looks like I don't understand `AutoResetEvent ` too much. – bLAZ Dec 28 '22 at 16:25
  • 1
    IMHO your question is too complicated, and important details about the API that you are using are missing, which reduces greatly the chances of getting a useful answer. It would be great if you could provide the signature of all the APIs that you are using, that are relevant to the UI freezing problem. Providing also the documentation about the usage of the APIs, in case it is not obvious, would be helpful. You could also include an example of how your code interacts with the UI, like a `TextBox`, a `ProgressBar` etc. – Theodor Zoulias Dec 28 '22 at 16:34
  • You can try running `dataReceiveTask` using `Task.Run` and see if it still blocks, and if it doesn't, then that's likely your problem. If you want to post debug messages to see what thread something is running on you can use `Thread.CurrentThread.ManagedThreadId` along with some other properties on thread like `IsThreadPoolThread` – TJ Rockefeller Dec 28 '22 at 16:36
  • @bLAZ what library are you using? Or did you use P/Invoke over a C-style API? `AutoResetEvent` is a synchronization primitive that blocks until some other thread signals it. The `Auto` part means the event gets reset after the other thread gets unblocked. There's `ManualResetEvent` which requires explicit resetting. – Panagiotis Kanavos Dec 28 '22 at 17:11
  • @TheodorZoulias I use FTDI D2XX https://www.ftdichip.com/Support/Documents/ProgramGuides/D2XX_Programmer%27s_Guide(FT_000071).pdf I also added printscreen with the API function description. As I understand it, this library must somehow do `receivedDataEvent.Set()` and then in my code `receivedDataEvent.WaitOne();` it comes through. Then my function `ReadDataAvailableInFtdiBuffer();` is run and after it is executed in this `while` there is again a wait for `receivedDataEvent` because it was automatically reset. – bLAZ Dec 28 '22 at 17:18
  • @bLAZ nothing in the code you posted should block, but it is really ugly code, even for the early 2000s. I suspect the code comes from an FTDI manual. What do you mean by saying the UI is blocked? Does the application freeze, ie you can't even click buttons? Or is nothing retrieved? The AutoResetEvent may not be getting signaled – Panagiotis Kanavos Dec 28 '22 at 17:19
  • @bLAZ that's the same chip I used back in 2004. The manual explicitly says there's a Virtual COM port. Have you tried using it? That will tell you that the device works to begin with. You can try to "go fast" once that works, assuming there's any need to do so. – Panagiotis Kanavos Dec 28 '22 at 17:21
  • @bLAZ what is `ftHandle` and what is `ftHandle.SetEventNotification`'s signature? You can't pass an AutoResetEvent directly to native code. You need to pass the [SafeWaitHandle](https://learn.microsoft.com/en-us/dotnet/api/system.threading.waithandle.safewaithandle?view=net-7.0#system-threading-waithandle-safewaithandle) property. The [SafeWaitHandle](https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles.safewaithandle?view=net-7.0) type should be used as the handle type for any native method – Panagiotis Kanavos Dec 28 '22 at 17:30
  • The application window is no longer responsive. The buttons I added to the window don't work, nor does minimize, maximize, close with a cross.... nothing related to the window works. But I can see in the application debugging window in Visual Studio that data is being received. If I have the code there to write to a file then the data goes to the file. There I even display now this `ManagedThreadId` and `IsThreadPoolThread` value in this `ReadDataAvailableInFtdiBuffer()` function. (`ReadDataAvailableInFtdiBuffer Thread ManagedThreadId: 6 IsThreadPoolThread: True` – bLAZ Dec 28 '22 at 17:35
  • `ftHandle` is the handle with which I've opened and configured FTDI chip. `ftStatus = ftHandle.OpenBySerialNumber(allowedDevice);`, etc. I added `ftHandle.SetEventNotification` signature in the second EDIT of my post. Pleas check it. – bLAZ Dec 28 '22 at 17:39
  • All in all, here in the post at the very bottom is something I'm trying to do: https://stackoverflow.com/questions/17319672/ftdi-chip-c-sharp-programming-how-to-read-all-buffer – bLAZ Dec 28 '22 at 17:41
  • `async void buttonClicked() { /* Only event handlers can be "async void" */ await Task.Run(() => { ... }); }` – Pieterjan Dec 28 '22 at 18:20
  • 1
    bLAZ [your comment](https://stackoverflow.com/questions/74942051/how-to-run-task-which-is-not-freezing-the-ui#comment132252000_74942051) where you describe the symptoms is quite revealing, so I added part of it in the question. My guess is that your problem is related to this question: [BackgroundWorker still freezes UI](https://stackoverflow.com/questions/22205015/backgroundworker-still-freezes-ui). The UI freezes when the `Control.Invoke` is called with high frequency. – Theodor Zoulias Dec 28 '22 at 19:12
  • The following may be of interest: https://devblogs.microsoft.com/dotnet/configureawait-faq/ – Tu deschizi eu inchid Dec 28 '22 at 19:42
  • Something else that you could try is to run your program without the debugger attached (with Ctrl+F5). Sometimes the problems just disappear when the debugger is not trying to help you. – Theodor Zoulias Dec 28 '22 at 20:42
  • What FTDI NuGet package are you using? – Enigmativity Dec 28 '22 at 23:37
  • Guys, I made the fix that @ΩmegaMan suggested, because actually I should not write to a variable from another thread. Anyway, I found the solution to the problem. And I'm ashamed to admit it. In this cascade of objects and their methods of mine, at the beginning when I started the application to test it I inserted such a dumb countdown timer to receive data only for a minute. And I did it using `Threed.Sleep()`... I didn't look at this place in the code later and focused completely on running this `Task` correctly, thinking that there is the problem there all the time. – bLAZ Dec 30 '22 at 16:26
  • 1
    I feel so stupid that for several hours I didn't know how to write this to you at all, but finally it has to be admitted. At least in the occasion I read a lot about `Tasks` and learned a lot, this series describes it very accessibly: https://www.pluralsight.com/guides/csharp-async-await-keywords-getting-started But I am so extremely uncomfortable.... I apologize and thank you for your time in educating me. – bLAZ Dec 30 '22 at 16:26
  • 1
    It's OK. Since you figured out the cause of the problem, you could consider closing the question for the reason "Not reproducible or was caused by a typo". – Theodor Zoulias Jan 03 '23 at 15:29

1 Answers1

1

. I have read that in 90% of cases the best way to handle threads is Tasks.

The other advice is to never write to the screen, or any variables which were not, or are not, in the same thread as the thread/task. In other words, the gui thread is different from the running thread. Attempting to write to data to the screen or or other memory locations is problematic.

In your case, you want to pass the data, or status to a specific variable which is "locked" and cannot be written to/accessed by either thread concurrently.

I suspect the issue is here

ftStatus = ftHandle.GetRxBytesAvailable(ref numBytesAvailable);

and that the ftStatus was created on a different thread. You will need to figure out a way to safely write or push messages to the status operation in a different way.


I ran into something similar and blogged about it ten years ago

OmegaMan's Musings » C# WPF: Linq Fails in BackgroundWorker DoWork Event

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122