Make my COM assembly call asynchronous

2019-04-05 09:58发布

问题:

I've just "earned" the privilege to maintain a legacy library coded in C# at my current work.

This dll:

  • Exposes methods for a big legacy system made with Uniface, that has no choice but calling COM objects.
  • Serves as a link between this legacy system, and another system's API.
  • Uses WinForm for its UI in some cases.

More visually, as I understand the components :

*[Big legacy system in Uniface]* ==[COM]==> [C# Library] ==[Managed API]==> *[Big EDM Management System]*

The question is: One of the methods in this C# Library takes too long to run and I "should" make it asynchronous!

I'm used to C#, but not to COM at all. I've already done concurrent programming, but COM seems to add a lot of complexity to it and all my trials so far end in either:

  • A crash with no error message at all
  • My Dll only partially working (displaying only part of its UI, and then closing), and still not giving me any error at all

I'm out of ideas and resources about how to handle threads within a COM dll, and I would appreciate any hint or help.

So far, the biggest part of the code I've changed to make my method asynchronous :

// my public method called by the external system
public int ComparedSearch(string application, out string errMsg) {
  errMsg = "";
  try {
    Action<string> asyncOp = AsyncComparedSearch;
    asyncOp.BeginInvoke(application, null, null);
  } catch (ex) {
    // ...
  }
  return 0;
}

private int AsyncComparedSearch(string application) {
  // my actual method doing the work, that was the called method before
}

Any hint or useful resource would be appreciated. Thank you.

UPDATE 1:

Following answers and clues below (especially about the SynchronizationContext, and with the help of this example) I was able to refactor my code and making it to work, but only when called from another Window application in C#, and not through COM. The legacy system encounters a quite obscure error when I call the function and doesn't give any details about the crash.

UPDATE 2:

Latest updates in my trials: I managed to make the multithreading work when the calls are made from a test project, and not from the Uniface system. After multiple trials, we tend to think that our legacy system doesn't support well multithreading in its current config. But that's not the point of the question any more :)

Here is a exerpt of the code that seems to work:

string application;
SynchronizationContext context;

// my public method called by the external system
public int ComparedSearch(string application, out string errMsg) {
    this.application = application;
    context = WindowsFormsSynchronizationContext.Current;
    Thread t = new Thread(new ThreadStart(AsyncComparedSearchAndShowDocs));
    t.Start();
    errMsg = "";
    return 0;
}

private void AsyncComparedSearch() {
    // ANY WORK THAT AS NOTHING TO DO WITH UI
    context.Send(new SendOrPostCallback(
        delegate(object state)
        {
            // METHODS THAT MANAGE UI SOMEHOW
        }
    ), null);
}

We are now considering other solutions than modifying this COM assembly, like encapsulating this library in a Windows Service and creating an interface between the system and the service. It should be more sustainable..

回答1:

It is hard to tell without knowing more details, but there are few issues here.

You execute the delegate on another thread via BeginInvoke but you don't wait for it. Your try\catch block won't catch anything as it has already passed while the remote call is still being executed. Instead, you should put try\catch block inside AsyncComparedSearch.

As you don't wait for the end of the execution of remote method (EndInvoke or via callback) I am not sure how do you handle the results of the COM call. I guess then that you update the GUI from within AsyncComparedSearch. If so, it is wrong, as it is running on another thread and you should never update GUI from anywhere but the GUI thread - it will most likely result with a crash or other unexpected behavior. Therefore, you need to sync the GUI update work to GUI thread. In WinForms you need to use Control.BeginInvoke (don't confuse it with Delegate.BeginInvoke) or some other way (e.g. SynchronizationContext) to sync the code to GUI thread. I use something similar to this:

private delegate void ExecuteActionHandler(Action action);

public static void ExecuteOnUiThread(this Form form, Action action)
{
  if (form.InvokeRequired) { // we are not on UI thread
    // Invoke or BeginInvoke, depending on what you need
    form.Invoke(new ExecuteActionHandler(ExecuteOnUiThread), action);
  }
  else { // we are on UI thread so just execute the action
    action();
  }
}

then I call it like this from any thread:

theForm.ExecuteOnUiThread( () => theForm.SomeMethodWhichUpdatesControls() );

Besides, read this answer for some caveats.