Freigeben über


Adopting STM.NET? The Runtime Checker is Your Friend!

(by Lingli Zhang)

In the previous blog post, Sukhdeep explained the STM.NET contract system, and how TxCop, the static contract checker, helps you identify potential contract violations in your program at compile time. Catching errors at compile time is definitely better than later. However, as Sukhdeep pointed out in his post, the static checker is not able to catch all errors precisely since .NET languages are inherently dynamic. For example, statically, we cannot figure out the exact instance of a method that is called at a virtual method call site. TxCop performs the checking using the base class’s contract at the call site, and requires all subclasses have compatible contracts. But what if the subclass comes from a third party library that is loaded dynamically, to which we don’t have access at static checking time? Delegates and reflection introduce problems similar to third party virtual methods.

In order to address these dynamic scenarios, a runtime checker was developed as part of the STM.NET execution engine to enforce the STM programming model at runtime. In today’s blog, I will explain how the STM runtime checker works, how it can protect you from doing something wrong with STM.NET, and help you gradually adopt STM.NET in your programs.

Before diving into technical details, let’s imagine we have a curious programmer named Bob who knows little about STM, but is very excited about STM.NET. So he installed STM.NET and started to play with it. Of course, he was not patient enough to go through all samples and the programming guide. Once he figured out how to specify an atomic block, he started to try out the classic HelloWorld example with STM.NET using the STM template provided:

image

In addition, Bob did not bother to enable TxCop (described in section 6.6.2.2 of the programming guide), otherwise TxCop would have warned him that Console.WriteLine cannot be used inside an atomic block. So Bob’s HelloWorld passed compilation without errors, and Bob started to execute the program. What did Bob get? Bang! The program exits immediately with an exception:

Unhandled Exception: System.TransactionalMemory.AtomicContractViolationException: 'System.IO.TextWriter+SyncTextWriter.WriteLine(System.String)' is accessed by 'Program.Main(System.String[])' inside a transaction, but it has [AtomicNotSupported] contract.

at System.IO.TextWriter.SyncTextWriter.WriteLine(String value)
at Program.Main(String[] args) in C:\Temp\HelloWorld\HelloWorld\Program.cs:line 12
at Program.Main(String[] args) in C:\Temp\HelloWorld\HelloWorld\Program.cs:line 9

This is the STM runtime checker in action! AtomicContractViolationException is the exception issued by the STM runtime checker to indicate that some violation to the STM programming model has been detected at runtime. The message of this exception explains the nature of this violation, and provides users more information concerning the violating code to help users identify the problem and fix it. Now Bob put his head down and studied the programming guide. Finally, he found the solution to make his HelloWorld program work in STM.NET in section 10.1 of the guide. What a joyful journey of learning STM.NET!

OK, are you ready to get to know the runtime checker a little bit more? The STM runtime checker consists of two components: one is a part of the STM JIT (Just-In-Time compiler) and does the checking and instrumentation, and the other sits in the STM runtime and provides the error reporting facility. Note that the JIT has direct or indirect visibility of all code that is executed as part of a .NET program: for managed code, the JIT is responsible to translate the byte-code to native code at runtime when the method is invoked for the first time; for external unmanaged methods (such as pinvoke methods), the JIT has visibility to their call sites in the managed code, which are the switching points between the managed world and the unmanaged world. This ability makes the JIT the perfect place to conduct STM contract checking at runtime. However, the JIT is not necessarily the best place to report the violations. Due to dynamic control flow, it is possible that some problematic code is compiled, but is never executed. To truly reflect the runtime behavior, we want to only report the errors when the violating code is actually about to be executed. Therefore, we let the JIT instrument some error reporting code right before the problematic code, and the error reporting code will be triggered only if the original problematic code is about to execute.

Now let’s go over the violations that the runtime checker might report. Based on the severity of violations, we divide all violations caught by the runtime checker into three categories:

  • Critical violations: The thread is in a transaction and
    • Some language features that inherently or currently cannot be transacted are encountered, e.g., certain forms of unsafe code, 1-based arrays, etc.
    • A native method without the AtomicRequired or AtomicSupported contract is invoked[1].
  • Contract violations:
    • The thread is in a transaction and a field or a method with an AtomicNotSupported contract is accessed.
    • The thread is outside of any transaction and an AtomicRequired method or field is accessed.
  • Harmless violations: Some harmless misuses of the STM contract system, e.g., placing an AtomicMarshalReadonly attribute on a non-reference type parameter (see section 10.1 of the STM Programming Guide for a discussion of this attribute).

As you can tell, critical violations are those circumstances that the runtime is pretty sure that the program is about to behave incorrectly, so it’s better to throw an exception at that instant instead of letting the program continue and behave randomly or crash. Contract violations, on the other hand, if ignored, do not necessarily result in a system crash (if it does, it will eventually lead to a critical violation before crashing), but are good to be caught to help programmers verify their STM contract assumptions. Finally, those harmless violations do not harm program execution, but it would be nice to report them in order to alert programmers of possible bugs in their code.

One question might immediately come to your mind: can I configure the runtime checker to work on different severity of violations? Ah, absolutely! That’s what the configuration file variable STMRuntimeCheckLevel is for: minimal for covering only critical violations, highest to report all violations including harmless ones, and two levels in between (relaxed and strict) to handle contract violations. The difference between relaxed and strict is how methods without any STM contract (no assembly contract either) are handled: at the relaxed level, contract checking is ignored for these methods by the runtime checker; while at the strict level, such methods are treated as AtomicNotSupported. Note that TxCop always works at the strict level in this sense.

The motivation of having four strictness levels in STM runtime checker is to ease the adoption process of STM.NET. In an ideal world, we’d hope all programs using the STM system are annotated with appropriate STM contracts, including third party libraries. But in reality, it will be a long time before we ever get to this dream land. So during the early adoption stages of the STM system, it’s very likely that a programmer would like to experimentally run the program inside transactions before adding any STM contracts to see if it’s possible to transition to transaction-safe code. Minimalis great for this scenario. Or she may have transitioned all of her code to the transactional version, and marked them with appropriate contracts. But she also needs to use a third party library, which does not have any STM contracts. In this case, the relaxed level is useful since it allows strict checking of assemblies that are “STM aware” and at the same time more relaxed checking on legacy assemblies.

Besides the strictness level, you may also configure how the runtime checker reports the violations: throwing an exception, logging the violation (to standard error or a specified file), or both. The following table summarizes all variables that you could use to configure the behavior of the STM runtime checker in app.config:

Configuration variable

Purpose

Allowed values

STMRuntimeCheckLevel

Controls the strictness level of the runtime checker

minimal, relaxed, strict, highest

STMExceptionOnViolation

Throw exception if a violation is detected. This variable does not affect critical violations, which always trigger exceptions.

0 (disable), 1 (enable)

STMLogOnViolation

Log violations to a log file if the log is specified, otherwise dump to the standard error output.

0 (disable), 1 (enable)

STMViolationLogFile

Specify the file to log violations to

File name

 

One typical app.config looks like this (most of our samples use this configuration):

image

You can tweak your App.config to fit your particular needs.

Well, I hope this post helped you understand how the STM runtime checker works, and why it’s your good friend and guardian when you are adopting STM.NET in your programs. Check out chapter 6 and in particular, section 6.7, of the STM programming Guide for more information about the STM contract system, TxCop, and the runtime checker. If you have more questions about these topics, or any question about STM.NET in general, feel free to drop a comment here or the STM Team online forum.


[1] Note that user’s code is not allowed to add AtomicRequired or AtomicSupported to a native method, unless AtomicSuppress is added too. Only native methods that are implemented by the CLR can have an AtomicSupported or AtomicRequired annotation without an AtomicSuppress. Thus, any p/invoke in your code MUST have an effective atomic compatibility value of AtomicNotSupported OR have an AtomicSuppress attribute.