The following discussion of threading and the PI-SDK applies mainly to programs written in C/C++ where it is possible to create multiple threads of execution. Typical Visual Basic programs will create PI-SDK objects in a single apartment and a single thread will service the PI-SDK calls. Calls to the objects are synchronized (sequential). This eliminates problems associated with cross-thread calls. C/C++ programmers that require multi-threaded access to the PI-SDK in their programs should consider this section. With the advent of VB.Net it is also now relatively easy to build multi-threaded applications using Visual Basic.
The PI-SDK library is an in-process server (DLL) that gets loaded on first access into a program's process space. Calls to the PI-SDK in a program are generally all made within the same process (assuming you have not configured your machine to access the PI-SDK through DCOM), but may be made across threads. Most PI-SDK objects in this release are built using the Apartment threading model and as such, when created, are instantiated in Single Threaded Apartments (STA). Given the object hierarchy used in the PI-SDK model, almost all PI-SDK child objects will end up being instantiated in a single apartment, the one where the main PISDK object is first created. "Creatable" objects, such as NamedValues and PITime, may be created in other apartments dictated by their threading model and the type of apartment where the creation call is made. COM dictates that an object in an STA can only execute on one thread (STAs have thread affinity). Calls from other threads must be marshaled to the object's STA, which results in these other threads receiving a proxy to the object rather than an actual interface pointer.
Any C++ thread calling COM objects must first call CoInitialize, CoInitializeEx, or OLEInitialize. This call determines the apartment type for the thread. CoInitialize and OLEInitialize always uses an STA. CoInitializeEx allows the caller to specify the apartment type. Subsequent calls to create objects in the thread, will create objects in that apartment. Access to objects created in other apartments (by other threads) must be handled through proxies.
An interesting question is how a thread initially obtains a proxy to an object in another apartment. It is a violation of COM rules to pass raw object/interface pointers from one STA thread to another. Doing so can create conditions where multiple threads are simultaneously executing code in the same object. Some objects may be thread-safe and be able to handle this type of access, but as a rule, apartment threaded objects are not required to be thread-safe because the COM rules and conventions ensure calls to the object are serialized. To safely pass pointers between apartments they need to be "marshaled." A thread (or process) receiving a marshaled interface actually receives a proxy to the object. Discussion of marshalling techniques is beyond the scope of this document. Refer to Microsoft documentation for details.
The PISDK object is designed as a per-thread single point of access to an object hierarchy. The object is built as a "per thread singleton," meaning only one instance of the object is created in any thread. Multiple requests to create the object within a thread return references to the original object. The first code to request the object, (typically a call to CreateInstance, using "new" in VB.Net, or a smart pointer constructor), results in the object being created in that thread's apartment. Subsequent calls to create the PISDK object in other threads will return a PISDK object for that particular thread. The hierarchies obtained from these separate PISDK objects are separate, though they communicate with the same PI servers. This behavior is useful for writing worker threads that operate independently of each other. Threads written this way avoid the performance cost of inter-thread marshaling.
Note however that it is still perfectly acceptable to marshal a reference to a PISDK object (or its children) across threads using standard COM calls. This allows designs where multiple threads share the same COM objects. In this configuration, however, cross thread calls will be marshaled with the non-creating thread receiving a proxy to the object's interface. For example, thread 1 creates a PISDK object and retrieves a Server object from its Servers collection. It marshals this Server object to thread 2. Thread 2 then uses the marshaled server pointer to obtain a PIPoints collection. The PIPoints interface pointer in thread 2 in this case is a proxy. Subsequent calls on the proxy that return interfaces to other objects (walking the hierarchy) will also return proxies. COM automatically generates these proxies, returned through calls on another proxy.
There are some important implications of this behavior in the PI-SDK . Accessing an object through a proxy as discussed above involves cross-thread marshalling, which can have performance impacts. When calling methods that require significant processing (for example cross platform calls to a PI Server) the contribution of the proxy may be insignificant. For quick local access calls (for example retrieving cached properties) the proxy call and marshalling may have a larger contribution to total processing time.
When a thread terminates, its STA is removed, and all objects created in that STA are deleted. If some other thread is holding a proxy to one of those objects, that proxy is not deleted, but will return an RPC disconnection error on any attempt to use it. This can be a problem when the thread creating the initial PISDK object terminates. All other threads trying to use the SDK will begin to report disconnection errors. To prevent this problem, the thread that creates the initial PISDK object should not terminate until all worker threads have already terminated. You can wait for a thread to be terminated by calling WaitForSingleObject or one of its derivatives passing the thread handle, or you can use other Win32 synchronization primitives.
In addition, the creating thread should keep the PISDK object alive by holding a reference-counted pointer to that object, which is not released until just before the thread terminates. If you do not do this, the PISDK object will delete itself when there are no outstanding references to it. This is not a problem in itself, but if a worker thread subsequently tries to get a reference to the PISDK object, a new instance of it will be created in that worker thread's STA. But the worker thread, unlike the main thread, will not be waiting for all other threads to terminate, resulting in the problem described above.
One final consideration when programming multi-threaded COM applications is that STAs (such as those created by calling CoCreateInstance) need to provide a message loop to handle COM calls from other threads. COM handles the serialization in a single-threaded apartment by posting Windows messages to the apartment's thread. If the thread does not have an active message loop to process the message, the call will block, freezing the application. If an object is to be marshaled, the initial thread creating the object requires a message loop to handle requests for the proxy generated by subsequent threads expecting to marshal the object. If you can be sure that none of the objects in a particular apartment will be called from another thread, you need not have a message loop in that apartment's thread. Introducing an active message loop into a thread requires breaking up the work to be done by the thread into reasonable execution chunks so that messages continue to be serviced in a reasonable time frame.
With version 1.3.1 of the PI-SDK a Windows message pump was added to the synchronization routines managing concurrent thread access to shared resources. There are implications of this change:
If while processing a windows message in your application, you call a PI-SDK method, it is possible for other windows messages to be received by your application before your processing of the original method is complete. This is due to a message pump in the PI-SDK that is run to marshal PI-SDK calls on proxies from other threads onto the main PI-SDK apartment thread (STA) where they execute. In a sense entering a PI-SDK call while processing a windows message makes your message handler reentrant.