Previous | Index | Next 

[HOWTO] Avoid reentrancy problems in free-threaded .NET components

When migrating ActiveX DLL components you might need to pay attention to the differences between the threading models used by VB6 and VB.NET components. This detail becomes very important when the migrated component is going to be used by clients that run in different threads, as is the case of ASP.NET pages or COM+ server components.

VB6 components use Single-Threaded Apartment (STA) model: each thread lives in an “apartment” of its own. In plain English, this means that each thread works with a different set of global variables. For example, assume that the ActiveX DLL project contains a BAS module with the following variable declaration:

        ' ...(in the UserInfo.bas file)
        Public UserName As String 

Thanks to STA threading model, each client – running in a different thread – can write this variable and read it back, without worrying about another client (in another thread) overwriting the variable with a different value. As you see, STA makes multi-threading programming virtually as easy as single-thread (i.e. traditional) programming. Of course,when writing multi-threaded apps you have to take care of many other potential concurrency issues – for example, two threads working on the same temporary file or registry key – but at least you don’t need to worry about global variables.

By contrast, components written in VB.NET – and all .NET languages, for that matter – use free-threading, which means that all threads can access all variables at the same time. Therefore, the VB.NET component exposes the same UserName field to all threads, which means that any thread can overwrite the value stored there by another thread.

Note: before continuing, please notice that only variables declared in BAS module are subject to this problem. In fact, local variables are stored on the call stack and are never shared among threads. Also, class fields are never shared among threads, because typically each thread create a new instance of the class and then works with that instance. (We don’t consider advanced scenarios in which a thread creates an object and passes it along to another thread.)

To simplify programming in free-threaded contests, the .NET Framework supports the ThreadStatic attribute. You can apply this attribute only to static class fields – either fields defined in modules or fields defined in classes and marked with the Shared kewword:

        <ThreadStatic()> _ 
        Public UserName As String 

All fields marked with the ThreadStatic attribute are stored in the Thread Local Storage (TLS) area. The separation among threads is automatic, because each thread owns a different TLS.

In theory, when migrating a VB6 DLL meant to be used by free-threaded clients, you should scrutinize each and every variable in BAS modules and decide whether to tag them with the ThreadStatic attribute. To make this task as easy as possible, as well as to avoid any manual edit of the resulting VB.NET code, starting with version 1.10.03 VB Migration Partner supports the ThreadSafe pragma, which can be applied at the project-, file-, method- and variable-level scope. In most cases, therefore, all you need to do is using a single pragma that affects all the variables in the current project:

        '## project:ThreadSafe True
        Public UserName As String

The pragma takes a boolean argument, which defaults to True if omitted. In practice, you will specify False only to revert the effect of a pragma with a broader scope.

The ThreadSafe pragma has a limitation, though: it doesn’t work with static local variables. To see why this is important and why you need to work around this limitation, consider the following VB6 code:

        ' (inside a BAS module)

        '## ThreadSafe
        Public UserName As String 

        ' we need to avoid recursive calls to this method 
        Static Sub DoSomething(ByVal text As String) 
            Dim callInProgress As Boolean 
            ' exit if another (recursive) call is in progress 
            If callInProgress Then Exit Function 
            callInProgress = True 
            ... 
            ' re-enable calls to this method 
            callInProgress = False 
        End Sub 

This is how VB Migration Partner converts the previous code:

        <ThreadStatic()> _ 
        Private UserName As String
  
        ' we need to avoid recursive calls to this method 
        Sub DoSomething(ByVal text As String) 
        ' UPGRADE_ISSUE (#10D8): Unable to apply the ThreadStatic attribute to local static 
        ' variables. Transform the variable into a module field and re-migrate. 
        Static callInProgress As Boolean 
            ' exit if another (recursive) call is in  progress 
            If callInProgress Then Exit Function 
            callInProgress = True 
            ... 
            ' re-enable calls to this method 
            callInProgress = False 
        End Sub 

The problem with previous code is that the callInProgress variable – unlike the UserName field – is actually shared by all threads, which can lead to a number of elusive problems. (In this specific case, a call will be rejected immediately if the method is being called from another thread.) VB Migration Partner hasn’t generated the ThreadStatic attribute, because that attribute can be used only on class fields, not on local variable. To ensure that thread-safeness is preserved is therefore necessary that you edit the VB6 code and move the variable out of the method, as follows:

        '## ThreadSafe
        Private UserName As String
        Private callInProgress As Boolean
  
        ' we need to avoid recursive calls to this method 
        Static Sub DoSomething(ByVal text As String) 
            ' exit if another (recursive) call is in progress 
            If callInProgress Then Exit Function 
            callInProgress = True 
            ... 
            ' re-enable calls to this method 
            callInProgress = False 
        End Sub 

Once again, please notice that authoring a component to be used in a multi-threaded environment requires more than just applying the ThreadSafe pragma: you also have to arbitrate the usage of shared resources – e.g. files, registry, printer, I/O ports, database connections, just to name a few. Some of these issues might be void if you are converting a VB6 multi-threaded component, because you can expect that the original VB6 developers took care of concurrency on shared resources, but new issues may arise just because of the many subtle differences between the STA and the free threading models.

 

Previous | Index | Next