Threading in C#

Joseph Albahari

 

Getting Started

Basic Synchronization

Using Threads

Advanced Topics

Overview and Concepts

Creating and Starting Threads

Synchronization Essentials

Locking and Thread Safety

Interrupt and Abort

Thread State

Wait Handles

Synchronization Contexts

Apartments and Windows Forms

BackgroundWorker

ReaderWriterLock

Thread Pooling

Asynchronous Delegates

Timers

Local Storage

Non-Blocking Synchronization

Wait and Pulse

Suspend and Resume

Aborting Threads

Download e-book as PDF

 

Post Comments

Part 3
Using Threads

Apartments and Windows Forms

Apartment threading is an automatic thread-safety regime, closely allied to COM – Microsoft's legacy Component Object Model. While .NET largely breaks free of legacy threading models, there are times when it still crops up because of the need to interoperate with older APIs. Apartment threading is most relevant to Windows Forms, because much of Windows Forms uses or wraps the long-standing Win32 API – complete with its apartment heritage.

An apartment is a logical "container" for threads. Apartments come in two sizes – "single" and "multi". A single-threaded apartment contains just one thread; multi-threaded apartments can contain any number of threads. The single-threaded model is the more common and interoperable of the two.

As well as containing threads, apartments contain objects. When an object is created within an apartment, it stays there all its life, forever house-bound along with the resident thread(s). This is similar to an object being contained within a .NET synchronization context, except that a synchronization context does not own or contain threads. Any thread can call upon an object in any synchronization context – subject to waiting for the exclusive lock. But objects contained within an apartment can only be called upon by a thread within the apartment.

Imagine a library, where each book represents an object. Borrowing is not permitted – books created in the library stay there for life. Furthermore, let's use a person to represent a thread.

A synchronization context library allows any person to enter, as long as only one person enters at a time. Any more, and a queue forms outside the library.

An apartment library has resident staff – a single librarian for a single-threaded library, and whole team for a multi-threaded library. No-one is allowed in other than members of staff – a patron wanting to perform research must signal a librarian, then ask the librarian to do the job! Signaling the librarian is called marshalling – the patron marshals the method call over to a member of staff (or, the member of staff!) Marshalling is automatic, and is implemented at the librarian-end via a message pump – in Windows Forms, this is the mechanism that constantly checks for keyboard and mouse events from the operating system. If messages arrive too quickly to be processed, they enter a message queue, so they can be processed in the order they arrive.

Specifying an Apartment Model

A .NET thread is automatically assigned an apartment upon entering apartment-savvy Win32 or legacy COM code. By default, it will be allocated a multi-threaded apartment, unless one requests a single-threaded apartment as follows:

Thread t = new Thread (...);
t.SetApartmentState (ApartmentState.STA);

One can also request that the main thread join a single-threaded apartment using the STAThread attribute on the main method:

class Program {
  [STAThread]
  static void Main() {
  ...

Apartments have no effect while executing pure .NET code. In other words, two threads with an apartment state of STA can simultaneously call the same method on the same object, and no automatic marshalling or locking will take place. Only when execution hits unmanaged code can they kick in.

The types in the System.Windows.Forms namespace extensively call Win32 code designed to work in a single-threaded apartment. For this reason, a Windows Forms program should have have the [STAThread] attribute on its main method, otherwise one of two things will occur upon reaching Win32 UI code:

Control.Invoke

In a multi-threaded Windows Forms application, it's illegal to call a method or property on a control from any thread other than the one that created it. All cross-thread calls must be explicitly marshalled to the thread that created the control (usually the main thread), using the Control.Invoke or Control.BeginInvoke method. One cannot rely on automatic marshalling because it takes place too late – only when execution gets well into unmanaged code, by which time plenty of internal .NET code may already have run on the "wrong" thread – code which is not thread-safe.

An excellent solution to managing worker threads in Windows Forms applications is to use BackgroundWorker. This class wraps worker threads that need to report progress and completion, and automatically calls Control.Invoke as required.

BackgroundWorker

BackgroundWorker is a helper class in the System.ComponentModel namespace for managing a worker thread. It provides the following features:

The last two features are particularly useful – it means you don't have to include a try/catch block in your worker method, and can update Windows Forms controls without needing to call Control.Invoke.

BackgroundWorker uses the thread-pool, which recycles threads to avoid recreating them for each new task. This means one should never call Abort on a BackgroundWorker thread.

Here are the minimum steps in using BackgroundWorker:

This then sets it in motion. Any argument passed to RunWorkerAsync will be forwarded to DoWork's event handler, via the event argument's Argument property. Here's an example:

class Program {
  static BackgroundWorker bw = new BackgroundWorker();
  static void Main() {
    bw.DoWork += bw_DoWork;
    bw.RunWorkerAsync ("Message to worker");     
    Console.ReadLine();
  }
 
  static void bw_DoWork (object sender, DoWorkEventArgs e) {
    // This is called on the worker thread
    Console.WriteLine (e.Argument);        // writes "Message to worker"
    // Perform time-consuming task...
  }

BackgroundWorker also provides a RunWorkerCompleted event which fires after the DoWork event handler has done its job. Handling RunWorkerCompleted  is not mandatory, but one usually does so in order to query any exception that was thrown in DoWork. Furthermore, code within a RunWorkerCompleted event handler is able to update Windows Forms controls without explicit marshalling; code within the DoWork event handler cannot.

To add support for progress reporting:

Code in the ProgressChanged event handler is free to interact with UI controls just as with RunWorkerCompleted. This is typically where you will update a progress bar.

To add support for cancellation:

Here's an example that implements all the above features:

using System;
using System.Threading;
using System.ComponentModel;
 
class Program {
  static BackgroundWorker bw;
  static void Main() {
    bw = new BackgroundWorker();
    bw.WorkerReportsProgress = true;
    bw.WorkerSupportsCancellation = true;
    bw.DoWork += bw_DoWork;
    bw.ProgressChanged += bw_ProgressChanged;
    bw.RunWorkerCompleted += bw_RunWorkerCompleted;
 
    bw.RunWorkerAsync ("Hello to worker");
    
    Console.WriteLine ("Press Enter in the next 5 seconds to cancel");
    Console.ReadLine();
    if (bw.IsBusy) bw.CancelAsync();
    Console.ReadLine();
  }
 
  static void bw_DoWork (object sender, DoWorkEventArgs e) {
    for (int i = 0; i <= 100; i += 20) {
      if (bw.CancellationPending) {
        e.Cancel = true;
        return;
      }
      bw.ReportProgress (i);
      Thread.Sleep (1000);
    }
    e.Result = 123;    // This gets passed to RunWorkerCopmleted
  }
 
  static void bw_RunWorkerCompleted (object sender,
  RunWorkerCompletedEventArgs e) {
    if (e.Cancelled)
      Console.WriteLine ("You cancelled!");
    else if (e.Error != null)
      Console.WriteLine ("Worker exception: " + e.Error.ToString());
    else
      Console.WriteLine ("Complete - " + e.Result);      // from DoWork
  }
 
  static void bw_ProgressChanged (object sender,
  ProgressChangedEventArgs e) {
    Console.WriteLine ("Reached " + e.ProgressPercentage + "%");
  }
}

Press Enter in the next 5 seconds to cancel
Reached 0%
Reached 20%
Reached 40%
Reached 60%
Reached 80%
Reached 100%
Complete – 123

Press Enter in the next 5 seconds to cancel
Reached 0%
Reached 20%
Reached 40%

You cancelled!

Subclassing BackgroundWorker

BackgroundWorker is not sealed and provides a virtual OnDoWork method, suggesting another pattern for its use. When writing a potentially long-running method, one could instead – or as well – write a version returning a subclassed BackgroundWorker, pre-configured to perform the job asynchronously. The consumer then only need handle the  RunWorkerCompleted and ProgressChanged events. For instance, suppose we wrote a time-consuming method called GetFinancialTotals:

public class Client {
  Dictionary <string,int> GetFinancialTotals (int foo, int bar) { ... }
  ...
}

We could refactor it as follows:

public class Client {
  public FinancialWorker GetFinancialTotalsBackground (int foo, int bar) {
    return new FinancialWorker (foo, bar);
  }
}
 
public class FinancialWorker : BackgroundWorker {
  public Dictionary <string,int> Result;   // We can add typed fields.
  public volatile int Foo, Bar;            // We could even expose them
                                           // via properties with locks!
  public FinancialWorker() {
    WorkerReportsProgress = true;
    WorkerSupportsCancellation = true;
  }
 
  public FinancialWorker (int foo, int bar) : this() {
    this.Foo = foo; this.Bar = bar;
  }
 
  protected override void OnDoWork (DoWorkEventArgs e) {
    ReportProgress (0, "Working hard on this report...");
    Initialize financial report data
 
    while (!finished report ) {
      if (CancellationPending) {
        e.Cancel = true;
        return;
      }
      Perform another calculation step
      ReportProgress (percentCompleteCalc, "Getting there...");
    }      
    ReportProgress (100, "Done!");
    e.Result = Result = completed report data;
  }
}

Whoever calls GetFinancialTotalsBackground then gets a FinancialWorker – a wrapper to manage the background operation with real-world usability. It can report progress, be cancelled, and is compatible with Windows Forms without Control.Invoke. It's also exception-handled, and uses a standard protocol (in common with that of anyone else using BackgroundWorker!)

This usage of BackgroundWorker effectively deprecates the old "event-based asynchronous pattern".

ReaderWriterLock

Commonly, instances of a type are thread-safe for concurrent read operations, but not for concurrent updates (nor a concurrent read and update). This can also be true with resources such as a file. While protecting instances of such types with a simple exclusive lock for all modes of access usually does the trick, it can unreasonably restrict concurrency if there are many readers and just occasional updates. An example of where this this could occur is in a business application server, where commonly used data is cached for fast retrieval in static fields. The ReaderWriterLock class is designed to provide maximum-availibility locking in just this scenario.

ReaderWriterLock provides separate methods for acquiring read and write locks – AcquireReaderLock and AcquireWriterLock. Both methods require a timeout argument, and will throw an ApplicationException if a timeout occurs (rather than returning false, like most equivalent methods in threading classes). Timeouts can occur quite easily if the resource is heavily contended.

A lock is released by calling ReleaseReaderLock or ReleaseWriterLock. These methods honor nested locking; a ReleaseLock method is also provided that clear all lock nesting levels in one hit. (One can subsequently call RestoreLock to re-lock to the same level it was prior to calling ReleaseLock – so as to mimic Monitor.Wait's lock toggling behavior).

One can start out with a read-lock by calling AcquireReaderLock then upgrade to a write-lock by calling UpgradeToWriterLock. The method returns a cookie that can then be used to then call DowngradeFromWriterLock. This system allows a reader to temporarily request write-access whilst not having to re-queue after demotion.

In the following example, four threads are started – one that continually adds items to a list; another that continually removes items from the list, and two that continually report the number of items in the list. The former two obtain write-locks; the latter two obtain read-locks. A timeout of 10 seconds is used for each lock (exception handlers are generally included to catch the resultant ApplicationException; in this example they are omitted for brevity).

class Program {
  static ReaderWriterLock rw = new ReaderWriterLock ();
  static List <int> items = new List <int> ();
  static Random rand = new Random ();
 
  static void Main (string[] args) {
    new Thread (delegate() { while (true) AppendItem(); } ).Start();
    new Thread (delegate() { while (true) RemoveItem(); } ).Start();
    new Thread (delegate() { while (true) WriteTotal(); } ).Start();
    new Thread (delegate() { while (true) WriteTotal(); } ).Start();
  }
 
  static int GetRandNum (int max) { lock (rand) return rand.Next (max); }
 
  static void WriteTotal() {
    rw.AcquireReaderLock (10000);
    int tot = 0; foreach (int i in items) tot += i;
    Console.WriteLine (tot);
    rw.ReleaseReaderLock();
  }
 
 static void AppendItem () {
    rw.AcquireWriterLock (10000);
    items.Add (GetRandNum (1000));
    Thread.SpinWait (400);
    rw.ReleaseWriterLock();
  }
 
  static void RemoveItem () {
    rw.AcquireWriterLock (10000);
    if (items.Count > 0)
      items.RemoveAt (GetRandNum (items.Count));
    rw.ReleaseWriterLock();
  }
}

Because adding an item to a List is quicker than removing it, this example includes a SpinWait in the AppendItem method to help even things out – keeping the total item count from running away.

Thread Pooling

If your application has lots of threads that spend most of their time blocked on a Wait Handle, you can reduce the resource burden via thread pooling. A thread pool economizes by coalescing many Wait Handles onto a few threads.

To use the thread pool, you register a Wait Handle along with a delegate to be executed when the Wait Handle is signaled. This is done by calling ThreadPool.RegisterWaitForSingleObject, such in this example:

class Test {
  static ManualResetEvent starter = new ManualResetEvent (false);
 
  public static void Main() {
    ThreadPool.RegisterWaitForSingleObject (starter, Go, "hello", -1, true);
    Thread.Sleep (5000);
    Console.WriteLine ("Signaling worker...");
    starter.Set();
    Console.ReadLine();
  }
 
  public static void Go (object data, bool timedOut) {
    Console.WriteLine ("Started " + data);
    // Perform task...
  }
}

(5 second delay)
Signaling worker...
Started hello

In addition to the Wait Handle and delegate, RegisterWaitForSingleObject accepts a "black box" object which it passes to your delegate method (rather like with a ParameterizedThreadStart), as well as a timeout in milliseconds (-1 meaning no timeout) and a boolean flag indicating if the request is one-off rather than recurring.

All pooled threads are background threads, meaning they terminate automatically when the application's foreground thread(s) end. However if one wanted to wait until any important jobs running on pooled threads completed before exiting an application, calling Join on the threads would not be an option, since pooled threads never finish! The idea is that they are instead recycled, and end only when the parent process terminates. So in order to know when a job running on a pooled thread has finished, one must signal – for instance, with another Wait Handle.

Calling Abort on a pooled thread is Bad Idea. The threads need to be recycled for the life of the application domain.

You can also use the thread pool without a Wait Handle by calling the QueueUserWorkItem  method – specifying a delegate for immediate execution. You don't then get the saving of sharing threads amongst multiple jobs, but do get another benefit: the thread pool keeps a lid on the total number of threads (25, by default), automatically enqueuing tasks when the job count goes above this. It's rather like an application-wide producer-consumer queue with 25 consumers! In the following example, 100 jobs are enqueued to the thread pool, of which 25 execute at a time. The main thread then waits until they're all complete using Wait and Pulse:

class Test {
  static object workerLocker = new object ();
  static int runningWorkers = 100;
 
  public static void Main() {
    for (int i = 0; i < runningWorkers; i++) {
      ThreadPool.QueueUserWorkItem (Go, i);
    }
    Console.WriteLine ("Waiting for threads to complete...");
    lock (workerLocker) {
      while (runningWorkers > 0) Monitor.Wait (workerLocker);
    }
    Console.WriteLine ("Complete!");
    Console.ReadLine();
  }
 
  public static void Go (object instance) {
    Console.WriteLine ("Started: " + instance);
    Thread.Sleep (1000);
    Console.WriteLine ("Ended: " + instance);
    lock (workerLocker) {
      runningWorkers--; Monitor.Pulse (workerLocker);
    }
  }
}

In order to pass more than a single object to the target method, one can either define a custom object with all the required properties, or call via an anonmymous method. For instance, if the Go method accepted two integer parameters, it could be started as follows:

ThreadPool.QueueUserWorkItem (delegate (object notUsed) { Go (23,34); });

Another way into the thread pool is via asynchronous delegates.

Asynchronous Delegates

In Part 1 we described how to pass data to a thread, using ParameterizedThreadStart. Sometimes you need to go the other way, and get return values back from a thread when it finishes executing. Asynchronous delegates offer a convenient mechanism for this, allowing any number of typed arguments to be passed in both directions. Furthermore, unhandled exceptions on asynchronous delegates are conveniently re-thrown on the original thread, and so don't need explicit handling. Asynchronous delegates also provide another way into the thread pool.

The price you must pay for all this is in following its asynchronous model. To see what this means, we'll first discuss the more usual, synchronous, model of programming. Let's say we want to compare two web pages. We could achieve this by downloading each page in sequence, then comparing their output as follows:

static void ComparePages() {
  WebClient wc = new WebClient ();
  string s1 = wc.DownloadString ("http://www.oreilly.com");
  string s2 = wc.DownloadString ("http://oreilly.com");
  Console.WriteLine (s1 == s2 ? "Same" : "Different");
}

Of course it would be faster if both pages downloaded at once. One way to view the problem is to blame DownloadString for blocking the calling method while the page is downloading. It would be nice if we could call DownloadString in a non-blocking asynchronous fashion, in other words:

  1. We tell DownloadString to start executing.
  2. We perform other tasks while it's working, such as downloading another page.
  3. We ask DownloadString for its results.

The WebClient class actually offers a built-in method called DownloadStringAsync which provides asynchronous-like functionality. For now, we'll ignore this and focus on the mechanism by which any method can be called asynchronously.

The third step is what makes asynchronous delegates useful. The caller rendezvous with the worker to get results and to allow any exception to be re-thrown. Without this step, we have normal multithreading. While it's possible to use asynchronous delegates without the rendezvous, you gain little over calling ThreadPool.QueueWorkerItem or using BackgroundWorker.

Here's how we can use asynchronous delegates to download two web pages, while simultaneously performing a calculation:

delegate string DownloadString (string uri);
 
static void ComparePages() {
 
  // Instantiate delegates with DownloadString's signature:
  DownloadString download1 = new WebClient().DownloadString;
  DownloadString download2 = new WebClient().DownloadString;
  
  // Start the downloads:
  IAsyncResult cookie1 = download1.BeginInvoke (uri1, null, null);
  IAsyncResult cookie2 = download2.BeginInvoke (uri2, null, null);
  
  // Perform some random calculation:
  double seed = 1.23;
  for (int i = 0; i < 1000000; i++) seed = Math.Sqrt (seed + 1000);
  
  // Get the results of the downloads, waiting for completion if necessary.
  // Here's where any exceptions will be thrown:
  string s1 = download1.EndInvoke (cookie1);
  string s2 = download2.EndInvoke (cookie2);
  
  Console.WriteLine (s1 == s2 ? "Same" : "Different");
}

We start by declaring and instantiating delegates for methods we want to run asynchronously. In this example, we need two delegates so that each can reference a separate WebClient object (WebClient does not permit concurrent access—if it did, we could use a single delegate throughout).

We then call BeginInvoke. This begins execution while immediately returning control to the caller. In accordance with our delegate, we must pass a string to BeginInvoke (the compiler enforces this, by manufacturing typed BeginInvoke and EndInvoke methods on the delegate type).

BeginInvoke requires two further arguments—an optional callback and data object; these can be left null as they're usually not required. BeginInvoke returns an IASynchResult object which acts as a cookie for calling EndInvoke. The IASynchResult object also has the property IsCompleted which can be used to check on progress.

We then call EndInvoke on the delegates, as their results are needed. EndInvoke waits, if necessary, until its method finishes, then returns the method's return value as specified in the delegate (string, in this case). A nice feature of EndInvoke is that if the DownloadString method had any ref or out parameters, these would be added into EndInvoke's signature, allowing multiple values to be sent back by to the caller.

If at any point during an asynchronous method's execution an unhandled exception is encountered, it's re-thrown on the caller's thread upon calling EndInvoke. This provides a tidy mechanism for marshaling exceptions back to the caller.

If the method you're calling asynchronously has no return value, you are still (technically) obliged to call EndInvoke. In a practical sense this is open to interpretation; the MSDN is contradictory on this issue. If you choose not to call EndInvoke, however, you'll need to consider exception handling on the worker method.

Asynchronous Methods

Some types in the .NET Framework offer asynchronous versions of their methods, with names starting with "Begin" and "End". These are called asynchronous methods and have signatures similar to those of asynchronous delegates, but exist to solve a much harder problem: to allow more concurrent activities than you have threads. A web or TCP sockets server, for instance, can process several hundred concurrent requests on just a handful of pooled threads if written using NetworkStream.BeginRead and NetworkStream.BeginWrite.

Unless you're writing a specialized high concurrency application, however, you should avoid asynchronous methods for a number of reasons:

If you're simply after parallel execution, you're better off calling the synchronous version of the method (e.g. NetworkStream.Read) via an asynchronous delegate. Another option is to use ThreadPool.QueueUserWorkItem or BackgroundWorker—or simply create a new thread.

Asynchronous Events

Another pattern exists whereby types can provide asynchronous versions of their methods. This is called the "event-based asynchronous pattern" and is distinguished by a method whose name ends with "Async", and a corresponding event whose name ends in "Completed". The WebClient class employs this pattern in its DownloadStringAsync method. To use it, you first handle the "Completed" event (e.g. DownloadStringCompleted) and then call the "Async" method (e.g. DownloadStringAsync). When the method finishes, it calls your event handler. Unfortunately, WebClient's implementation is flawed: methods such as DownloadStringAsync block the caller for a portion of the download time.

The event-based pattern also offers events for progress reporting and cancellation, designed to be friendly with Windows applications that update forms and controls. If you need these features in a type that doesn't support the event-based asynchronous model (or doesn't support it correctly!) you don't have to take on the burden of implementing the pattern yourself, however (and you wouldn't want to!) All of this can be achieved more simply with the BackgroundWorker helper class.

Timers

The easiest way to execute a method periodically is using a timer – such as the Timer class provided in the System.Threading namespace. The threading timer takes advantage of the thread pool, allowing many timers to be created without the overhead of many threads. Timer is a fairly simple class, with a constructor and just two methods (a delight for minimalists, as well as book authors!)

public sealed class Timer : MarshalByRefObject, IDisposable
{
  public Timer (TimerCallback tick, object state, 1st, subsequent);
  public bool Change (1st, subsequent);   // To change the interval
  public void Dispose();                // To kill the timer
}
1st = time to the first tick in milliseconds or a TimeSpan
subsequent = subsequent intervals in milliseconds or a TimeSpan
 (use Timeout.Infinite for a one-off callback)

In the following example, a timer calls the Tick method which writes "tick..." after 5 seconds have elapsed, then every second after that – until the user presses Enter:

using System;
using System.Threading;
 
class Program {
  static void Main() {
    Timer tmr = new Timer (Tick, "tick...", 5000, 1000);
    Console.ReadLine();
    tmr.Dispose();         // End the timer
  }
 
  static void Tick (object data) {
    // This runs on a pooled thread
    Console.WriteLine (data);          // Writes "tick..."
  }
}

The .NET framework provides another timer class of the same name in the System.Timers namespace. This simply wraps System.Threading.Timer, providing additional convenience while using the same thread pool – and the identical underlying engine. Here's a summary of its added features:

Here's an example:

using System;
using System.Timers;   // Timers namespace rather than Threading
 
class SystemTimer {
  static void Main() {
    Timer tmr = new Timer();       // Doesn't require any args
    tmr.Interval = 500;
    tmr.Elapsed += tmr_Elapsed;    // Uses an event instead of a delegate
    tmr.Start();                   // Start the timer
    Console.ReadLine();
    tmr.Stop();                    // Pause the timer
    Console.ReadLine();
    tmr.Start();                   // Resume the timer
    Console.ReadLine();
    tmr.Dispose();                 // Permanently stop the timer
  }
 
  static void tmr_Elapsed (object sender, EventArgs e) {
    Console.WriteLine ("Tick");
  }
}

The .NET framework provides yet a third timer – in the System.Windows.Forms namespace. While similar to System.Timers.Timer in its interface, it's radically different in the functional sense. A Windows Forms timer does not use the thread pool, instead firing its "Tick" event always on the same thread that originally created the timer. Assuming this is the main thread – also responsible for instantiating all the forms and controls in the Windows Forms application – the timer's event handler is then able to interact with the forms and controls without violating thread-safety – or the impositions of apartment-threading. Control.Invoke is not required.

A Windows Forms timer is intended for jobs that may involve updating the user interface and which execute quickly. Quick execution is important because the Tick event is called on the main thread – which if tied up, will make the user interface unresponsive.

Local Storage

Each thread gets a data store isolated from all other threads. This is useful for storing "out-of-band" data – that which supports the execution path's infrastructure, such as messaging, transaction or security tokens. Passing such data around via method parameters would be extremely clumsy and would alienate all but your own methods; storing such information in static fields would mean sharing it between all threads.

Thread.GetData reads from a thread's isolated data store; Thread.SetData writes to it. Both methods require a LocalDataStoreSlot object to identify the slot – this is just a wrapper for a string that names the slot – the same one can be used across all threads and they'll still get separate values. Here's an example:

class ... {
  // The same LocalDataStoreSlot object can be used across all threads.
  LocalDataStoreSlot secSlot = Thread.GetNamedDataSlot ("securityLevel");
 
  // This property has a separate value on each thread.
  int SecurityLevel {
    get {
      object data = Thread.GetData (secSlot);
      return data == null ? 0 : (int) data;    // null == uninitialized
    }
    set {
      Thread.SetData (secSlot, value);
    }
  }
  ...

Thread.FreeNamedDataSlot will release a given data slot across all threads – but only once all LocalDataStoreSlot objects of the same name have dropped out of scope and been garbage collected. This ensures threads don't get data slots pulled out from under their feet – as long as they keep a reference to the appropriate LocalDataStoreSlot object for as long as it's in use.

 

 

Post Comments

© 2006, O'Reilly Media, Inc. All rights reserved

Threading in C#

Getting Started

Overview and Concepts

Creating and Starting Threads

Basic Synchronization

Synchronization Essentials

Locking and Thread Safety

Interrupt and Abort

Thread State

Wait Handles

Synchronization Contexts

Using Threads

Apartments and Windows Forms

BackgroundWorker

ReaderWriterLock

Thread Pooling

Asynchronous Delegates

Timers

Local Storage

Advanced Topics

Non-Blocking Synchronization

Wait and Pulse

Suspend and Resume

Aborting Threads


Download PDF

Post Comments

Contact Author


Last updated: 2006-10-16