2

I'm working on a software product that runs intensive operations on the main thread. Running them on a separate thread is not supported by design and won't be changed.

At the same time we need to handle mouse movements coming from UI. In one case mouse cursor freezes because the main thread is being busy with computations.

Seems a good case for introducing asynchronous operation: run computations asynchronously in a separate thread while main thread is still handling mouse movements. But as I said before it is not supported in the current design.

Recently I came across an idea to run two tasks asynchronously in one thread. Meaning that thread context is switched between two tasks and each task is partially executed for a quantum of time until each of them gets finished.

Is this possible in C++? The version of the language (11 or 14) does not matter.
The software uses WinApi and standard message queue to receive mouse events.

Tried to look at Microsoft PPL but from my understanding the lib does not help in this case.
Thanks everyone for help.

whhandrey
  • 23
  • 4
  • 1
    By definition, one execution thread is one task. End of story. C++20 introduces some zombie mutations called "coroutines", that would be one option; although I pity the soul who ventures to go there... – Sam Varshavchik Apr 13 '20 at 12:27
  • 1
    Regarding the 'tintensive operations', can you clarify what you mean by `"Running them on a separate thread is not supported by design"? – G.M. Apr 13 '20 at 12:30
  • 1
    You could manually process the message queue: https://stackoverflow.com/questions/1415095/doevents-equivalent-for-c but really not doing processing on the main thread is the only sensible answer – Alan Birtles Apr 13 '20 at 12:31
  • Check out coroutines and fibers. – geza Apr 13 '20 at 12:40
  • *"run two tasks asynchronously in one thread. Meaning that thread context is switched between two tasks"* - We call that *"synchronous"*. – IInspectable Apr 13 '20 at 13:26
  • @G.M.: I tried to run these computation in s separate thread (using std::async) and the module just threw exception complaining that it was run in a separate thread. I asked responsible for that module person and the answer was that it is not supported by the current design. – whhandrey Apr 13 '20 at 13:28
  • What sort of exception? There is nothing in C++ that allows code to run on one thread but prevent it to run on another. This is a custom exception, manually thrown by the code after it checked some condition. This could be due to an uninitialized COM apartment. In that case, the issue can be solved (without having the foreign code get involved or changed). – IInspectable Apr 13 '20 at 13:41
  • @IInspectable: Yes, this is a custom exception. It's thrown if mainThreadId != runningThreadId. Don't know why this has been done. – whhandrey Apr 13 '20 at 13:47
  • There is no such thing as a *"main thread"* in a Windows process, so there is no way for the external code to query for that information. How does whatever *"mainThreadId"* means gets passed to this external code? There's little reason for a thread to run on any given thread. There are really just 2 reasons: A COM object that needs to run on a thread in a particular apartment, or code that mutates UI state. A switch to a specific thread (or context) can easily and cleanly be implemented for either use case. – IInspectable Apr 13 '20 at 14:00
  • If the library vendor won't support running on a thread other than the gui thread, then little hope that they will support coop multi tasking etc. – David Heffernan Apr 15 '20 at 07:13

5 Answers5

1

What you are looking for is cooperative multi-tasking. This is possible on a single thread. You can take a look at coroutines, e.g. in boost or the standard library (since C++20).

You can also roll your own, stripped down version. The key ingerdients are:

  • Each task needs to store its context (e.g. parameters) itself
  • Each task needs a way to suspend and resume operations. It decides on its own when to suspend.
  • You might need some form of scheduler that keeps track of all the tasks and run them frequently. You might want to design it in a way that the GUI main loop calls into your scheduler which runs for approximately 30-50 ms at most by passing the available time budget to each of the tasks it keeps track of.

This is quite feasible if threads are not an option at all.

Markus Mayr
  • 4,038
  • 1
  • 20
  • 42
1

Boost.Coroutine, Boost.Context, and Boost.Asio all support single thread concurrency at some level or another. Coroutines are cooperative, reentrant, interruptible, resumable functions. Context is user land context switching. Asio executors can schedule many different tasks to run on one thread. For your case, I think you can take your pick as to what you're comfortable putting into your application.

EDIT

Boost.Fiber implements mini thread-like "fibers" on top of the Context library.

Zuodian Hu
  • 979
  • 4
  • 9
0

Here is how I would implement my own run to completion cooperative multitasking:

enum class eStep
{
    START,
    STEP1,
    STEP2,
    DONE
};

struct sLongFuncContext
{
    //whatver is meaning full to go from one step to the next
};

eStep long_func_split_in_steps(eStep aStep,sLongFuncContext &aContext)
{
    eStep next;
    switch (aStep)
    {
        case eStep::START:
        // execute first part of func, save context
        next = eStep::STEP1;
        break;

        case eStep::STEP1:
         // execute 2nd part of func, save context
        next = eStep::STEP2;
        break;

        case eStep::STEP2:
        next = eStep::DONE;
        break;
        // repeat 

    };
    return (next);
}

int main()
{
    eStep step = eStep::START;
    sLongFuncContext context;
    while (step != eStep::DONE)
    {
        // do a part of the long function
        step = long_func_split_in_steps(step,context);

        // handle mouse events
        // ...
    }



    return 0;
}
Jean-Marc Volle
  • 3,113
  • 1
  • 16
  • 20
  • funny ! this is how c++ coroutines are implemented in general however the compiler has more cool things to do – dev65 Apr 13 '20 at 13:35
0

Since you are targeting windows but doesn't have access to c++ 20 coroutines (using old compiler) you can use winapi Fibers which is like heavy coroutines .

It's documented here : Fibers Win32 apps

And this is an example of using it :

#include <windows.h>
#include <tchar.h>
#include <stdio.h>

VOID
__stdcall
ReadFiberFunc(LPVOID lpParameter);

VOID
__stdcall
WriteFiberFunc(LPVOID lpParameter);

void DisplayFiberInfo(void);

typedef struct
{
   DWORD dwParameter;          // DWORD parameter to fiber (unused)
   DWORD dwFiberResultCode;    // GetLastError() result code
   HANDLE hFile;               // handle to operate on
   DWORD dwBytesProcessed;     // number of bytes processed
} FIBERDATASTRUCT, *PFIBERDATASTRUCT, *LPFIBERDATASTRUCT;

#define RTN_OK 0
#define RTN_USAGE 1
#define RTN_ERROR 13

#define BUFFER_SIZE 32768   // read/write buffer size
#define FIBER_COUNT 3       // max fibers (including primary)

#define PRIMARY_FIBER 0 // array index to primary fiber
#define READ_FIBER 1    // array index to read fiber
#define WRITE_FIBER 2   // array index to write fiber

LPVOID g_lpFiber[FIBER_COUNT];
LPBYTE g_lpBuffer;
DWORD g_dwBytesRead;

int __cdecl _tmain(int argc, TCHAR *argv[])
{
   LPFIBERDATASTRUCT fs;

   if (argc != 3)
   {
      printf("Usage: %s <SourceFile> <DestinationFile>\n", argv[0]);
      return RTN_USAGE;
   }

   //
   // Allocate storage for our fiber data structures
   //
   fs = (LPFIBERDATASTRUCT) HeapAlloc(
                              GetProcessHeap(), 0,
                              sizeof(FIBERDATASTRUCT) * FIBER_COUNT);

   if (fs == NULL)
   {
      printf("HeapAlloc error (%d)\n", GetLastError());
      return RTN_ERROR;
   }

   //
   // Allocate storage for the read/write buffer
   //
   g_lpBuffer = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, BUFFER_SIZE);
   if (g_lpBuffer == NULL)
   {
      printf("HeapAlloc error (%d)\n", GetLastError());
      return RTN_ERROR;
   }

   //
   // Open the source file
   //
   fs[READ_FIBER].hFile = CreateFile(
                                    argv[1],
                                    GENERIC_READ,
                                    FILE_SHARE_READ,
                                    NULL,
                                    OPEN_EXISTING,
                                    FILE_FLAG_SEQUENTIAL_SCAN,
                                    NULL
                                    );

   if (fs[READ_FIBER].hFile == INVALID_HANDLE_VALUE)
   {
      printf("CreateFile error (%d)\n", GetLastError());
      return RTN_ERROR;
   }

   //
   // Open the destination file
   //
   fs[WRITE_FIBER].hFile = CreateFile(
                                     argv[2],
                                     GENERIC_WRITE,
                                     0,
                                     NULL,
                                     CREATE_NEW,
                                     FILE_FLAG_SEQUENTIAL_SCAN,
                                     NULL
                                     );

   if (fs[WRITE_FIBER].hFile == INVALID_HANDLE_VALUE)
   {
      printf("CreateFile error (%d)\n", GetLastError());
      return RTN_ERROR;
   }

   //
   // Convert thread to a fiber, to allow scheduling other fibers
   //
   g_lpFiber[PRIMARY_FIBER]=ConvertThreadToFiber(&fs[PRIMARY_FIBER]);

   if (g_lpFiber[PRIMARY_FIBER] == NULL)
   {
      printf("ConvertThreadToFiber error (%d)\n", GetLastError());
      return RTN_ERROR;
   }

   //
   // Initialize the primary fiber data structure.  We don't use
   // the primary fiber data structure for anything in this sample.
   //
   fs[PRIMARY_FIBER].dwParameter = 0;
   fs[PRIMARY_FIBER].dwFiberResultCode = 0;
   fs[PRIMARY_FIBER].hFile = INVALID_HANDLE_VALUE;

   //
   // Create the Read fiber
   //
   g_lpFiber[READ_FIBER]=CreateFiber(0,ReadFiberFunc,&fs[READ_FIBER]);

   if (g_lpFiber[READ_FIBER] == NULL)
   {
      printf("CreateFiber error (%d)\n", GetLastError());
      return RTN_ERROR;
   }

   fs[READ_FIBER].dwParameter = 0x12345678;

   //
   // Create the Write fiber
   //
   g_lpFiber[WRITE_FIBER]=CreateFiber(0,WriteFiberFunc,&fs[WRITE_FIBER]);

   if (g_lpFiber[WRITE_FIBER] == NULL)
   {
      printf("CreateFiber error (%d)\n", GetLastError());
      return RTN_ERROR;
   }

   fs[WRITE_FIBER].dwParameter = 0x54545454;

   //
   // Switch to the read fiber
   //
   SwitchToFiber(g_lpFiber[READ_FIBER]);

   //
   // We have been scheduled again. Display results from the 
   // read/write fibers
   //
   printf("ReadFiber: result code is %lu, %lu bytes processed\n",
   fs[READ_FIBER].dwFiberResultCode, fs[READ_FIBER].dwBytesProcessed);

   printf("WriteFiber: result code is %lu, %lu bytes processed\n",
   fs[WRITE_FIBER].dwFiberResultCode, fs[WRITE_FIBER].dwBytesProcessed);

   //
   // Delete the fibers
   //
   DeleteFiber(g_lpFiber[READ_FIBER]);
   DeleteFiber(g_lpFiber[WRITE_FIBER]);

   //
   // Close handles
   //
   CloseHandle(fs[READ_FIBER].hFile);
   CloseHandle(fs[WRITE_FIBER].hFile);

   //
   // Free allocated memory
   //
   HeapFree(GetProcessHeap(), 0, g_lpBuffer);
   HeapFree(GetProcessHeap(), 0, fs);

   return RTN_OK;
}

VOID
__stdcall
ReadFiberFunc(
             LPVOID lpParameter
             )
{
   LPFIBERDATASTRUCT fds = (LPFIBERDATASTRUCT)lpParameter;

   //
   // If this fiber was passed NULL for fiber data, just return,
   // causing the current thread to exit
   //
   if (fds == NULL)
   {
      printf("Passed NULL fiber data; exiting current thread.\n");
      return;
   }

   //
   // Display some information pertaining to the current fiber
   //
   DisplayFiberInfo();

   fds->dwBytesProcessed = 0;

   while (1)
   {
      //
      // Read data from file specified in the READ_FIBER structure
      //
      if (!ReadFile(fds->hFile, g_lpBuffer, BUFFER_SIZE, 
         &g_dwBytesRead, NULL))
      {
         break;
      }

      //
      // if we reached EOF, break
      //
      if (g_dwBytesRead == 0) break;

      //
      // Update number of bytes processed in the fiber data structure
      //
      fds->dwBytesProcessed += g_dwBytesRead;

      //
      // Switch to the write fiber
      //
      SwitchToFiber(g_lpFiber[WRITE_FIBER]);
   } // while

   //
   // Update the fiber result code
   //
   fds->dwFiberResultCode = GetLastError();

   //
   // Switch back to the primary fiber
   //
   SwitchToFiber(g_lpFiber[PRIMARY_FIBER]);
}

VOID
__stdcall
WriteFiberFunc(
              LPVOID lpParameter
              )
{
   LPFIBERDATASTRUCT fds = (LPFIBERDATASTRUCT)lpParameter;
   DWORD dwBytesWritten;

   //
   // If this fiber was passed NULL for fiber data, just return,
   // causing the current thread to exit
   //
   if (fds == NULL)
   {
      printf("Passed NULL fiber data; exiting current thread.\n");
      return;
   }

   //
   // Display some information pertaining to the current fiber
   //
   DisplayFiberInfo();

   //
   // Assume all writes succeeded.  If a write fails, the fiber
   // result code will be updated to reflect the reason for failure
   //
   fds->dwBytesProcessed = 0;
   fds->dwFiberResultCode = ERROR_SUCCESS;

   while (1)
   {
      //
      // Write data to the file specified in the WRITE_FIBER structure
      //
      if (!WriteFile(fds->hFile, g_lpBuffer, g_dwBytesRead, 
         &dwBytesWritten, NULL))
      {
         //
         // If an error occurred writing, break
         //
         break;
      }

      //
      // Update number of bytes processed in the fiber data structure
      //
      fds->dwBytesProcessed += dwBytesWritten;

      //
      // Switch back to the read fiber
      //
      SwitchToFiber(g_lpFiber[READ_FIBER]);
   }  // while

   //
   // If an error occurred, update the fiber result code...
   //
   fds->dwFiberResultCode = GetLastError();

   //
   // ...and switch to the primary fiber
   //
   SwitchToFiber(g_lpFiber[PRIMARY_FIBER]);
}

void
DisplayFiberInfo(
                void
                )
{
   LPFIBERDATASTRUCT fds = (LPFIBERDATASTRUCT)GetFiberData();
   LPVOID lpCurrentFiber = GetCurrentFiber();

   //
   // Determine which fiber is executing, based on the fiber address
   //
   if (lpCurrentFiber == g_lpFiber[READ_FIBER])
      printf("Read fiber entered");
   else
   {
      if (lpCurrentFiber == g_lpFiber[WRITE_FIBER])
         printf("Write fiber entered");
      else
      {
         if (lpCurrentFiber == g_lpFiber[PRIMARY_FIBER])
            printf("Primary fiber entered");
         else
            printf("Unknown fiber entered");
      }
   }

   //
   // Display dwParameter from the current fiber data structure
   //
   printf(" (dwParameter is 0x%lx)\n", fds->dwParameter);
}
dev65
  • 1,440
  • 10
  • 25
  • The OP can use C++14. That implies that they are using Visual Studio 2015 or later. C++20 coroutines were introduced into Visual Studio with Visual Studio 2015. Coroutine support can be enabled independently of any particular language standard using the [/await](https://learn.microsoft.com/en-us/cpp/build/reference/await-enable-coroutine-support) compiler switch. – IInspectable Apr 13 '20 at 14:05
  • But I remember the had some bugs in these old versions – dev65 Apr 13 '20 at 14:07
  • Can you find me a C++ compiler that *doesn't* have bugs? – IInspectable Apr 13 '20 at 14:09
  • No, it wasn't stable enough to use and had many bugs . Also I didn't ban anyone from using coroutines , in fact I prefer them to fibers so I suggested them at the first line of the answer – dev65 Apr 13 '20 at 14:11
  • Got any reference for that? – IInspectable Apr 13 '20 at 14:13
0

Given that you are using winapi and UI so you already have message processing I would suggest that you break up the problematic operation into more steps and use custom messages. Have each step in the problematic operation post the message that triggers the next step. Since this is something windows already handles (dealing with messages) it should fit much more neatly into what you already have than trying to use coroutines or windows fibers.

This will slow down overall processing of the problematic operation somewhat but will keep the UI responsive.

However I would also seriously consider abandoning the single-threaded approach. If your problematic operation simply takes input and produces an output shoving that operation onto a separate thread and dealing with the result when it comes (again via a posted message) is often a very reasonable solution.

SoronelHaetir
  • 14,104
  • 1
  • 12
  • 23