Concurrency Code Analysis in Visual Studio 2019
The battle against concurrency bugs poses a serious challenge to C++ developers. The problem is exacerbated by the advent of multi-core and many-core architectures. To cope with the increasing complexity of multithreaded software, it is essential to employ better tools and processes to help developers adhere to proper locking discipline. In this blog post, we’ll walk through a completely rejuvenated Concurrency Code Analysis toolset we are shipping with Visual Studio 2019 Preview 2.
A perilous landscape
The most popular concurrent programming paradigm in use today is based on threads and locks. Many developers in the industry have invested heavily in multithreaded software. Unfortunately, developing and maintaining multithreaded software is difficult due to a lack of mature multi-threaded software development kits.
Developers routinely face a dilemma in how to use synchronization. Too much may result in deadlocks and sluggish performance. Too little may lead to race conditions and data inconsistency. Worse yet, the Win32 threading model introduces additional pitfalls. Unlike in managed code, lock acquire and lock release operations are not required to be syntactically scoped in C/C++. Applications therefore are vulnerable to mismatched locking errors. Some Win32 APIs have subtle synchronization side effects. For example, the popular “SendMessage” call may induce hangs among UI threads if not used carefully. Because concurrency errors are intermittent, they are among the hardest to catch during testing. When encountered, they are difficult to reproduce and diagnose. Therefore, it is highly beneficial to apply effective multi-threading programming guidelines and tools as early as possible in the engineering process.
Importance of locking disciplines
Because one generally cannot control all corner cases induced by thread interleaving, it is essential to adhere to certain locking disciplines when writing multithreaded programs. For example, following a lock order while acquiring multiple locks or using std::lock() consistently helps to avoid deadlocks; acquiring the proper guarding lock before accessing a shared resource helps to prevent race conditions. However, these seemingly simple locking rules are surprisingly hard to follow in practice.
A fundamental limitation in today’s programming languages is that they do not directly support specifications for concurrency requirements. Programmers can only rely on informal documentation to express their intention regarding lock usage. Thus, developers have a clear need for pragmatic tools that help them confidently apply locking rules.
Concurrency Toolset
To address the deficiency of C/C++ in concurrency support, we had shipped an initial version of concurrency analyzer back in Visual Studio 2012, with promising initial results. This tool had a basic understanding of the most common Win32 locking APIs and concurrency related annotations.
In Visual Studio 2019 Preview 2, we are excited to announce a completely rejuvenated set of concurrency checks to meet the needs of modern C++ programmers. The toolset comprises a local intra-procedural lock analyzer with built-in understanding of common Win32 locking primitives and APIs, RAII locking patterns, and STL locks.
Getting started
The concurrency checks are integrated as part of the code analysis toolset in Visual Studio. The default “Microsoft Native Recommended Ruleset” for the project comprises the following rules from the concurrency analyzer. This means, whenever you run code analysis in your project, these checks are automatically executed. These checks are also automatically executed as part of the background code analysis runs for your project. For each rule, you can click on the link to learn more about the rule and its enforcement with clear examples.
- C26100: Race condition. Variable should be protected by a lock.
- C26101: Failing to use interlocked operation properly for a variable.
- C26110: Caller failing to acquire a lock before calling a function which expects the lock to be acquired prior to being called.
- C26111: Caller failing to release a lock before calling a function which expects the lock to be released prior to being called.
- C26112: Caller cannot hold any lock before calling a function which expects no locks be held prior to being called.
- C26115: Failing to release a lock in a function. This introduces an orphaned lock.
- C26116: Failing to acquire a lock in a function, which is expected to acquire the lock.
- C26117: Releasing an unheld lock in a function.
- C26140: Undefined lock kind specified on the lock.
If you want to try out the full set of rules from this checker, there’s a ruleset just for that. You must right click on Project > Properties > Code Analysis > General > Rulesets > Select “Concurrency Check Rules”.
You can learn more about each rule enforced by the checker by searching for rule numbers in the ranges C26100 – C26199 in our Code Analysis for C/C++ warning document.
Concurrency toolset in action
The initial version of concurrency toolset was capable of finding concurrency related issues like race conditions, locking side effects, and potential deadlocks in mostly C-like code.
The tool had in-built understanding of standard Win32 locking APIs. For custom functions with locking side effects, the tool understood a number of concurrency related annotations. These annotations allowed the programmer to express locking behavior. Here are some examples of concurrency related annotations.
- _Acquires_lock_(lock): Function acquires the lock object “lock”.
- _Releases_lock_(lock): Function releases the lock object “lock”.
- _Requires_lock_held_(lock): Lock object “lock” must be acquired before entering this function.
- _Guarded_by_(lock) data: “data” must always be protected by lock object “lock”.
- _Post_same_lock(lock1, lock2): “lock1” and “lock2” are aliases.
For a complete set of concurrency related annotations, please see this article Annotating Locking Behavior.
The rejuvenated version of the toolset builds on the strengths of the initial version by extending its analysis capabilities to modern C++ code. For example, it now understands STL locks and RAII patterns without having to add any annotations.
Now that we have talked about how the checker works and how you can enable them in your project, let’s look at some real-world examples.
Example 1
Can you spot an issue with this code1 ?
struct RequestProcessor { CRITICAL_SECTION cs_; std::map<int, Request*> cache_; bool HandleRequest(int Id, Request* request) { EnterCriticalSection(&cs_); if (cache_.find(Id) != cache_.end()) return false; cache_[Id] = request; LeaveCriticalSection(&cs_); } void DumpRequestStatistics() { for (auto& r : cache_) std::cout << "name: " << r.second->name << std::endl; } };
1 If you have seen this talk given by Anna Gringauze in CppCon 2018, this code may seem familiar to you.
Let’s summarize what’s going on here:
- In function
HandleRequest
, we acquire lockcs
on line 6. However, we return early from line 8 without ever releasing the lock. - In function
HandleRequest
, we see thatcache_
access must be protected by lockcs
. However, in a different function,DumpStatistics
, we accesscache_
without acquiring any lock.
If you run code analysis on this example, you’ll get a warning in method HandleRequest
, where it will complain about the leaked critical section (issue #1):
Next, if you add the _Guarded_by_
annotation on the field cache_
and select ruleset “Concurrency Check Rules”, you’ll get an additional warning in method DumpRequestStatistics
for the possible race condition (issue #2):
Example 2
Let’s look at a more modern example. Can you spot an issue with this code1 ?
struct RequestProcessor2 { std::mutex m_; std::map<int, Request*> cache_; void HandleRequest(int Id, Request* request) { std::lock_guard grab(m_); if (cache_.find(Id) != cache_.end()) return; cache_[Id] = request; } void DumpRequestStatistics() { for (auto& r : cache_) std::cout << "name: " << r.second->name << std::endl; } };
As expected, we don’t get any warning in HandleRequest
in the above implementation using std::lock_guard
. However, we still get a warning in DumpRequestStatistics
function:
There are a couple of interesting things going on behind the scenes here. First, the checker understands the locking nature of std::mutex
. Second, it understands that std::lock_guard
holds the mutex and releases it during destruction when its scope ends.
This example demonstrates some of the capabilities of the rejuvenated concurrency checker and its understanding of STL locks and RAII patterns.
Give us feedback
We’d love to hear from you about your experience of using the new concurrency checks. Remember to switch to “Concurrency Check Rules” for your project to explore the full capabilities of the toolset. If there are specific concurrency patterns you’d like us to detect at compile time, please let us know.
If you have suggestions or problems with this check — or any Visual Studio feature — either Report a Problem or post on Developer Community. We’re also on Twitter at @VisualC.
0 comments