Multithreaded COM server problem...

J

John Lull

I'm writing a multithreaded COM server to manage a pool of hardware resources.
All objects are designed to be thread-safe, and I've set sys.coinit_flags to
COINIT_MULTITHREADED before importing pythoncom.

The problem is that all requests to the server seem to be serialized by COM. To
demonstrate the problem, I'm including a simple test server that exposes one
interface object, PyComThreads.Application, with a single method sleep(delay).
I'm also including 2 test programs -- test20.py uses the server to delay 20
seconds, and test1.py delays only one second. The server prints status messages
to the trace collector debugging tool when creating the Application object, and
at the beginning and end of the specified delay.


When I run the 20-second test program, then a couple seconds later run the
1-second test program, I had expected to see something like this:

Object 8087872 in thread 8160416 created
Object 8087872 in thread 8160416 delays 20 seconds
Object 8086008 in thread 8156272 created
Object 8086008 in thread 8156272 delays 1 seconds
Object 8086008 delay ends
Object 8087872 delay ends


Instead, I see:

Object 8087872 in thread 8160416 created
Object 8087872 in thread 8160416 delays 20 seconds
Object 8087872 delay ends
Object 8086008 in thread 8160416 created
Object 8086008 in thread 8160416 delays 1 seconds
Object 8086008 delay ends


Apparently the requests from both client applications are being serialized by
the COM interface.

I need each request (or at least each interface object) to run in its own
thread, or in a pool of threads, and haven't been able to figure out how to
accomplish this. Any suggestions would be appreciated.


Regards,
John

----------
PyComThreads.py:

import sys
import threading
from time import sleep
import win32traceutil

sys.coinit_flags = 0 # 0 == pythoncom.COINIT_MULTITHREADED # !!!!!
import pythoncom

class Application:
""" Test version of a Multi-threaded local server """

_reg_progid_ = 'PyComThreads.Application'
_reg_verprogid_ = 'PyComThreads.Application.100'
_reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER
_reg_clsid_ = '{56BEC27D-EDC4-43A0-AEB7-77E4A1381C0F}'

_public_methods_ = ['sleep']
_public_attrs_ = []
_readonly_attrs_ = []

def __init__(self):
print 'Object %s in thread %s created' % \
(id(self), id(threading.currentThread()))

def sleep(self, delay):
print 'Object %s in thread %s delays %s seconds' % \
(id(self), id(threading.currentThread()), delay)
sleep(delay)
print 'Object %s delay ends' % id(self)

# COM server registration, etc.
if __name__ == '__main__':
if hasattr(sys, 'argv'):
# If *no* command-line arguments, we were not invoked as a server.
# Assume the user wants us to self-register.
if len(sys.argv) == 1:
sys.argv.append('--register')

import win32com.server.register
win32com.server.register.UseCommandLine(Application, debug=0)

----------
test20.py:

from win32com.client import Dispatch
app=Dispatch('PyComThreads.Application')
app.sleep(20)

----------
test1.py:

from win32com.client import Dispatch
app=Dispatch('PyComThreads.Application')
app.sleep(1)
 
M

Michel Claveau/Hamster

Hi !


* Sorry for my bad english : french is very more easy, and nice ;-) *

I have try another way. I use a procedure for manage thread :

GlobalTacheID=[None]*16

def tache(ordre, numtache=1, nomtache=None, fonction=None):
global GlobalTacheID
import threading
if ordre=="Lance":
GlobalTacheID[numtache] =
threading.Thread(target=fonction,name=nomtache)
GlobalTacheID[numtache].start()
print(nomtache+' num.'+str(numtache))
elif ordre=="Liste":
return(string.join(threading.enumerate(),'\n'))
elif ordre=="Etat":
return(str(GlobalTacheID[numtache].isAlive()))

And, in my public_method, i call this procedure, with the parameters
"ordre=Lance" for run a thread, "ordre=Etat" for see his state (fonction=
the name of the method 'threaded').

And it's OK. I can run a thread from an Excel-COM-client. I can also run an
thread on a method caontained in a string gived by Excel.

Python is very smart-dynamic-agile language.

Bonne soirée
 
M

Mark Hammond

John said:
I'm writing a multithreaded COM server to manage a pool of hardware resources.
All objects are designed to be thread-safe, and I've set sys.coinit_flags to
COINIT_MULTITHREADED before importing pythoncom.

Note that this flag is effeectively ignored for the server. Objects are
created in the same "apartment" as their creator. This, the code
implementing your object need not specify this, but the code *creating*
the object must. This is what determines the appartment.

COM threading rules are complex, but "Python Programming on Win32" (1/2
by me :) covers these rules in words I would have trouble finding again :)

Mark.
 
J

John Lull

Mark Hammond said:
Note that this flag is effeectively ignored for the server. Objects are
created in the same "apartment" as their creator. This, the code
implementing your object need not specify this, but the code *creating*
the object must. This is what determines the appartment.

COM threading rules are complex, but "Python Programming on Win32" (1/2
by me :) covers these rules in words I would have trouble finding again :)

It covers threading rules for in-process servers pretty thoroughly.

Unfortunately, this one has to be a local server since it's providing shared
access to a pool of hardware devices from multiple distributed clients. I've
carefully reviewed chapters 5 & 12, and appendix D, and wasn't able to find
anything addressing threading models in the local server in detail. If I've
missed something, I'd be grateful for any additional hints.

Thanks.


Regards,
John
 
M

Mark Hammond

John said:
It covers threading rules for in-process servers pretty thoroughly.

Unfortunately, this one has to be a local server since it's providing shared
access to a pool of hardware devices from multiple distributed clients. I've
carefully reviewed chapters 5 & 12, and appendix D, and wasn't able to find
anything addressing threading models in the local server in detail. If I've
missed something, I'd be grateful for any additional hints.

The problem is that your client code is not running a message loop. If
you change the loop of your client test code to something like:

for i in range(delay)*10:
time.sleep(0.1)
pythoncom.PumpWaitingMessages()

It works as you expect. A better choice would probably be
win32event.MsgWaitForMultipleObjects, but that depends on what your app
really does.

Mark.
 
J

John Lull

John Lull wrote: ....

The problem is that your client code is not running a message loop. If
you change the loop of your client test code to something like:

for i in range(delay)*10:
time.sleep(0.1)
pythoncom.PumpWaitingMessages()

It works as you expect. A better choice would probably be
win32event.MsgWaitForMultipleObjects, but that depends on what your app
really does.

Mark.

I presume you meant my server code. This still leaves all calls to the
server running in a single thread, however. If I insert a call to
PumpWaitingMessages() in a short operation, and it happens to start a
long operation, the result of that short operation will be delayed
until the long operation completes. This will make my server unusable.

I think I really need a server with at least requests from different
clients running in separate threads.

I tried changing win32com.server.localserver to do:
sys.coinit_flags = 0
instead of:
sys.coinit_flags = 2

At first glance this seems to do what I want -- requests to the server
seem to run from a thread pool. However, I also get intermittent (but
frequest) startup errors, with a Microsoft Visual C++ Runtime Library
error dialog (during pythoncom.PumpMessages but before my server
module gets imported) reporting:
Runtime Error!
Program: c:\Apps\Python2.2\pythonw.exe
abnormal program termination

When I click OK, my client application reports (reformatted for
posting):
Traceback (most recent call last):
File "D:\Cust\Kinetics\comThreads\test20.py", line 7, in ?
app=Dispatch('PyComThreads.Application')
File "C:\Apps\Python2.2\lib\site-packages\win32com
\client\__init__.py", line 95, in Dispatch
dispatch, userName = dynamic._GetGoodDispatchAndUserName(
dispatch,userName,clsctx)
File "C:\Apps\Python2.2\lib\site-packages\win32com
\client\dynamic.py", line 84, in _GetGoodDispatchAndUserName
return (_GetGoodDispatch(IDispatch, clsctx), userName)
File "C:\Apps\Python2.2\lib\site-packages\win32com
\client\dynamic.py", line 72, in _GetGoodDispatch
IDispatch = pythoncom.CoCreateInstance(IDispatch, None, clsctx,
pythoncom.IID_IDispatch)
pywintypes.com_error: (-2146959355, 'Server execution failed',
None, None)

I do *not* seem to get this error if the server is already running
because of a request from another process.

If I'm not able to track down the cause of this error, is there some
way I can (during object creation) create a new thread to handle all
requests on the new object? I know how to create the thread, what I
don't know is how I would tell the COM mechanism to route all requests
on the new object to the message pump in the new thread.

Thanks for your assistance.

Regards,
John
 
M

Mark Hammond

John said:
deletions):




I presume you meant my server code.

Nope - I meant the client. The server is already running such a loop
thank to localserver.py, which is hosting the object.

The client code's main (and only) thread is blocked in a system call,
but it appears COM wants it to pump messages so the marshalling magic
happens. I can only speculate why COM needs this to happen in this
trivial case, but COM's rules do state this requirement.
This still leaves all calls to the
server running in a single thread, however. If I insert a call to
PumpWaitingMessages() in a short operation, and it happens to start a
long operation, the result of that short operation will be delayed
until the long operation completes. This will make my server unusable.

Make the change I suggested, and it works as you hope.
At first glance this seems to do what I want -- requests to the server
seem to run from a thread pool. However, I also get intermittent (but
frequest) startup errors, with a Microsoft Visual C++ Runtime Library
error dialog (during pythoncom.PumpMessages but before my server
module gets imported) reporting:
Runtime Error!
Program: c:\Apps\Python2.2\pythonw.exe
abnormal program termination

That sucks, and is almost certainly a thread-state error. If you have a
debug build of Python, it will offer to break into the debugger at this
point.

Mark.
 
J

John Lull

Nope - I meant the client. The server is already running such a loop
thank to localserver.py, which is hosting the object.

The client code's main (and only) thread is blocked in a system call,
but it appears COM wants it to pump messages so the marshalling magic
happens. I can only speculate why COM needs this to happen in this
trivial case, but COM's rules do state this requirement.

Now I'm *really* confused.

Perhaps I need to clarify a bit. The sleep() method in my sample
server is a perhaps-too-simplified substitute for what the real server
is doing. It provides a variety of high-level operations on a piece of
hardware. Some of the operations take as long as several dozen
seconds, others take a millisecond or so. I need the client to block
waiting for completion of each operation, regardless of how long the
operation takes. I cannot break one of the long operations up into a
series of calls from the client -- it must be implemented as a single
call. My example would, perhaps, have been clearer if I'd named the
method someLongRunningMethod() instead of sleep().

I've tried doing roughly what you suggested inside my test client,
calling PumpWaitingMessages() both before and after each COM
operation. That still leaves me with the same basic problem -- inside
the server, all of my COM objects are created on the server's main
thread, instead of on a separate thread for each client. That leaves
all COM operations serialized through that single thread. My test
client at the moment looks like this:

import sys
from pythoncom import PumpWaitingMessages
from win32com.client import Dispatch

PumpWaitingMessages()
app=Dispatch('PyComThreads.Application')
PumpWaitingMessages()
app.someLongRunningMethod(20)
PumpWaitingMessages()

If this is not essentially what you meant, could you please let me
know? The server is exactly like I had posted originally, except for
renaming its sleep() method to someLongRunningMethod().

That sucks, and is almost certainly a thread-state error.

That was my thought.


Thanks.

Regards,
John
 
M

Mark Hammond

John said:
Now I'm *really* confused.

Perhaps I need to clarify a bit. The sleep() method in my sample
server is a perhaps-too-simplified substitute for what the real server
is doing. It provides a variety of high-level operations on a piece of
hardware. Some of the operations take as long as several dozen
seconds, others take a millisecond or so. I need the client to block
waiting for completion of each operation, regardless of how long the
operation takes. I cannot break one of the long operations up into a
series of calls from the client -- it must be implemented as a single
call. My example would, perhaps, have been clearer if I'd named the
method someLongRunningMethod() instead of sleep().

Is there any way you can do it asynchronously? The main thread spawns a
second thread to do the work. The main thread then spins around a
MsgWaitForMultipleObjects, with an object set by the second thread. The
main thread will then be able to run a message pump, but also detect
when the second thread is done.

Apart from that, I am afraid I am out of ideas. I believe however that
you are hitting pure COM issues, and nothing related to Python. Looking
for the answer beyond the Python world may be fruitful.

Mark.
 
J

John Lull

Is there any way you can do it asynchronously? The main thread spawns a
second thread to do the work. The main thread then spins around a
MsgWaitForMultipleObjects, with an object set by the second thread. The
main thread will then be able to run a message pump, but also detect
when the second thread is done.

Yes, except there doesn't seem to be any way to return a value from
the first call until after the second call completes.
Apart from that, I am afraid I am out of ideas. I believe however that
you are hitting pure COM issues, and nothing related to Python. Looking
for the answer beyond the Python world may be fruitful.

The three reasonable possibilities I see at the moment are:

1. Dig into exactly how apartment-threaded servers are supposed to be
written in C or C++ & see how much of that is transferrable to Python.
I'm not confident this would give me a solution, though.

2. Activate the server in 2 steps -- have the main thread create (on
client request) "interlock" object that, when created, fires up a new
thread for the client who created it. The new thread then calls
factory.RegisterClassFactories() for the real worker object, then
starts a message pump. The client then requests COM to create the
worker object. The worker object creation method then calls
factory.RevokeClassFactories() to revoke that thread's registration.
The interlock and worker classes have to coordinate so that only one
worker thread is registered as class factory at any time, and only one
client is able to create the worker object at any time. The main
thread also has to register as class factory only for the interlock
object, not for the worker object. This requires changes to one py2exe
module and to all applications that use my server, and will complicate
shutdown, since I have to shut down the new thread only when all
object's it's pumping messages for are gone. There's also the
substantial possibility that the same bug I ran into when setting
sys.coinit_flags=0 in localserver.py will rear its head here.

3. Dig into why setting sys.coinit_flags=0 in localserver.py doesn't
work. This is probably the right thing to do, both because I know it
will yield a solution, and because it would solve the same issue for
anyone else with similar needs. Unfortunately it means I have to dig
into Python threading internals, pythoncom internals, and the whole
COM mechanism rather more heavily than I'd hoped to.


Regards,
John
 
M

Mark Hammond

John said:
1. Dig into exactly how apartment-threaded servers are supposed to be
written in C or C++ & see how much of that is transferrable to Python.
I'm not confident this would give me a solution, though. ....
3. Dig into why setting sys.coinit_flags=0 in localserver.py doesn't
work. This is probably the right thing to do, both because I know it
will yield a solution, and because it would solve the same issue for
anyone else with similar needs. Unfortunately it means I have to dig
into Python threading internals, pythoncom internals, and the whole
COM mechanism rather more heavily than I'd hoped to.

I think these are pretty-much the same option. The win32com extensions
make no attempt to insulate you from these threading issues - it will
blindly do what you ask. In that regard, Python already acts very much
like a C/C++ application - under the covers, we have a C++ pointer to a
COM object, and when Python code tries to make a call, from whatever
thread, we just make the call. The same thing is true in reverse - we
hand out a C++ pointer, and when an incoming call is made on that, we
blindly call Python on that thread.

The Python GIL is in the picture, but I see no evidence it has any
bearing in this case. Certainly, in the simple example we started with,
no threads were blocked waiting for this lock.

Re sys.soinit_flags - I am fairly confident that this will cause
CoInitEx to be called with the specified flag as pythoncom is imported.
However, note that any exceptions raised by this function are masked
when called implicitly during that import. Try explicitly calling
pythoncom.CoInitializeEx(0) near the top of localserver.py - any
exceptions you see would also have happened for the implicit call made
at import time.

I'd welcome any evidence that a C++ app would behave differently, and am
keen to help you resolve (or at least explain) this for everyone's
future benefit.

Mark.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,011
Latest member
AjaUqq1950

Latest Threads

Top