Thanks for Not Sharing, Or, How to Define Thread-Local Data

ntil not long ago, multithreaded apps relied on nonstandard, platform-dependent APIs. C++0x is now in the process of adding a standard threading API, including auxiliary facilities such as thread-local storage. The following sections will explain how to create thread-local data and explain the restrictions on the use of such data.


An object with static storage duration is indiscriminately shared among all the threads of a given process, but you need something quite different?you need a per-thread copy of that object. How do you go about getting it?


Use thread-local variables to declare objects that are unique to a thread.

Presenting the Problem
Suppose you’re designing server application that serves incoming requests from a client. The client must login before issuing a request from the server. However, if the client has already logged in successfully, you certainly don’t want to force it to repeat the slow login process. To keep track of the client’s status, the thread function uses a local static flag that is set to true after the first successful login of the client. If that flag is false, the thread function will call a login function in a loop up to MAX_TRY times before processing the client’s request. If the login attempts have failed, the thread function returns a negative value:

int client_req(){  static bool connected=false;  for (int n=0; n

Using local static variables inside a function is a common technique used to recall a previously stored state or session. However, there is one problem with this approach: the variable connected is shared among all threads calling client_req(). Therefore, a race-condition will occur if one thread is reading connected while another thread is modifying it. Until not long ago, the common solution to this problem was to synchronize access to the shared data, e.g., by using critical sections or mutexes. However, a closer examination of the code reveals that there's no call for synchronized access at all. Rather, what you really need is for each thread to receive its own private copy of connected. How do you do that?

Declarations
Objects with auto and register storage types are not a problem?they are thread unique anyway because each thread has its own stack. By contrast, static storage objects reside in a memory section that is accessible to all threads of the same process. Objects with static storage duration include:

  • Global objects, and more generally, objects declared in a namespace scope
  • File-scope static objects
  • Local static objects declared inside a function or a block
  • Static data members of a class

Any of these can now become thread-local. The C++0x thread_local keyword designates a thread-local object. The resolution and scope of a variable declared with the thread_local storage type are as they would be without the thread_local keyword. For example:

extern thread_local int global_i; //globalint client_req(){ thread_local static bool connected=false; //local static}thread_local static void *p=0; //file static

Using thread_local for variables that do not have static storage type is an error:

int func(thread_local int x);//error, arguments are auto

Notice that static and thread_local aren't mutually exclusive. In the declarations of p above, static indicates internal linkage (i.e., the variable isn't visible from other translation units), whereas thread_local indicates that every thread gets a private copy of p.

Addressing
Thread-local variables differ from other variables in certain crucial aspects. When applied to a thread-local object, the & (address-of) operator is evaluated at runtime and returns the address of the current thread's variable. Since operator & is evaluated at runtime, a thread-local object's address is not a constant. The address of a thread-local variable is stable for the lifetime of that thread. Such an address may be used freely during the variable's lifetime by any thread in the program. You may take the address of a thread-local variable and pass it to other threads. All addresses of thread-local variables are invalidated and may not be used once that thread has terminated.

Dynamic Initialization
Thread-local variables may be statically-initialized as would ordinary static variables. According to the latest proposal, dynamic initialization for thread-local objects and variables is also supported. In this respect, the C++0x thread-local proposal is a significant improvement over the existing, nonstandard __thread storage type that certain compilers support. Consider:

thread_local std::string s("hi");//dynamic initializationthread_local int num=4; //static initializationthread_local int num2=func(); //dynamic initializationthread_local std::string* p; //static initializationthread_local static char buf[MAX];//static initialization

Generally speaking, compilers that support thread-local storage as a nonstandard extension (see list below) will compile only the declarations that require static initialization whereas compilers supporting the C++0x thread_local keyword will compile all of the declarations above, including those requiring dynamic initialization.

Currently, thread-local storage is already available as a nonstandard extension from the following vendors:

The thread-local storage type is one of several concurrency support components that are being added to C++0x these days, including synchronization objects, threads, condition variables, atomic types, and a new memory model. The C++0x type traits library also includes a trait class for identifying objects with thread-local storage.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: