Chapter 4: Developing Phase: Process and Thread Management

This chapter discusses the differences between the UNIX and the Microsoft® .NET environments in the context of process and thread management programming. In addition, this chapter outlines the various options available for converting the code from the UNIX to the Windows® environment using .NET and illustrates these options with appropriate source code examples. You can use the information provided in this chapter to assist you in selecting the appropriate approach for migrating an existing application. You can also use the examples provided in this chapter as a basis for constructing your .NET application.

On This Page

Process Management Process Management
Thread Management Thread Management

Process Management

The UNIX and Microsoft Windows operating systems provide process and thread management. Each process may have its own code, data, system resources, and state. Threads are part of processes and each process may have one or more threads running in them. Like processes, threads also have resource and state. When you know how UNIX and .NET differ in their management of processes, you can easily replace UNIX process routines with the corresponding .NET-compatible routines.

The UNIX and Windows process management models are very different from each other. The major difference lies in the creation of processes. When converting UNIX code to make it run on the .NET platform, you need to consider the following areas:

  • Creating a process.

  • UNIX processes versus .NET application domains.

  • Processes versus threads.

  • Managing process resources limits.

The following subsections describe these topics in detail.

Creating a Process

UNIX uses fork to create a new copy of a running process and exec to replace the executable file of a process with a new one.

.NET provides a Process class in the System.Diagnostics namespace to perform process-related operations. Using this component, you can create a process, obtain a list of processes running in the system, and access the processes running in the system (including the system processes). It is also useful for starting, stopping, controlling, and monitoring applications. A process’ handle identifies it. This process handle is private to an application and cannot be shared. A process also has a process ID, which is unique and valid throughout the system. You can access the handle through the Handle property of the Process class, even after the process has exited. This enables you to access the administrative information about the process, such as the ExitCode (usually either zero for success or a nonzero error code) and the ExitTime.

.NET also provides application domains, which map to the processes in UNIX. In UNIX, an application runs within a process; whereas in .NET, an application runs within an application domain and there may be several such application domains running within the same process. Application domains are discussed in the “UNIX Processes vs. .NET Application Domains” section later in this chapter.

UNIX example: Creating a process using fork and exec

The following example code is a UNIX application that forks to create a child process and then runs the UNIX ps command by using execlp.

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main()
{
pid_t pid;
printf("Running ps with fork and execlp\n");
pid = fork();
switch(pid)
{
case -1:
perror("fork failed");
exit(1);
case 0:
if (execlp("ps", NULL) < 0) {
perror("execlp failed");
exit(1);
}
break;
default:
break;
}
printf("Done.\n");
exit(0);
}

(Source File: U_CreatingProcess-UAMV4C4.01.c)

.NET example: Creating a process using System.Diagnostics namespace

The following Managed C++ example creates a Notepad process on Windows using the System::Diagnostics::Process class.

#using <mscorlib.dll>
#using <System.dll>
using namespace System;
int main()
{
try    
{
//Creates an object of Process class
System::Diagnostics::Process *objPid = new System::
Diagnostics::Process();
//Starts a new notepad process
    objPid->Start(S"notepad.exe");
    Console::WriteLine(S"Done");
    } catch (Exception *e) {
        Console::WriteLine(S"Creating Process failed");
        Console::WriteLine(e->Message);
   }
}

(Source File: N_CreateProcess-UAMV4C4.01.cpp)

After creating and initializing an object of Process class, you can use it to obtain information about the running process. Such information includes the set of threads, the loaded modules (.dll and .exe files), and the performance information, such as the amount of memory the process is using.

The Microsoft Win32® API provides a CreateProcess function to create a process and then execute it. In theory, a P/Invoke call to the Win32 API CreateProcess can also be used to create a process in .NET. This is an unmanaged call, however, and CreateProcess can only be used to create unmanaged processes. The common language runtime (CLR) will not be capable of exercising any control over these processes.

UNIX Processes vs. .NET Application Domains

This section elaborates the differences between the UNIX processes and .NET application domains. It also discusses the advantages of using .NET application domains and provides instructions to create .NET application domains.

Operating systems and run-time environments generally provide some form of isolation between applications to ensure that the code running in one application does not affect other, unrelated applications. In UNIX, process boundaries are used to isolate applications running on the same computer. Each application is loaded into a separate process, which isolates the application from other applications running on the same computer. The applications are isolated because memory addresses are process-relative.

The .NET Framework further subdivides an operating system process into lightweight managed subprocesses, called application domains. These application domains provide isolation between applications. Application domains, which are typically created by run-time hosts, provide a more secure and versatile unit of processing. In .NET, the managed code must pass through a verification process before it can be run (unless the administrator has granted permission to skip the verification process).

The verification process determines that the code does not access invalid memory addresses or perform some other action that could cause the process in which it is running to fail. Code that passes this verification test is said to be type-safe. This capability to verify whether a piece of code is type-safe enables the CLR to provide a level of isolation that is equivalent to that of a process boundary, however, at a much lower performance cost.

Several application domains can run in a single process with the same level of isolation that exists in separate processes, but without the additional overhead of making cross-process calls or switching between processes. For example, a single browser process can run controls from several Web applications. However, these controls cannot access each other’s data and resources.

The following are the benefits offered by application domains:

  • Process isolation. Faults in one application cannot affect other applications. The type-safe code cannot cause memory faults. Application domains ensure that code running in one domain cannot affect other applications in the process.

  • Operational flexibility. You can terminate individual applications without terminating the entire process. Code running in a single application can be unloaded using application domains.

  • Restricted access to resources. Code running in one application cannot directly access code or resources from another application. The CLR enforces this isolation by preventing direct calls between objects in different application domains.

In an application domain, you need to load assemblies before running the application. Running a typical application causes several assemblies to be loaded into its application domain. The code segment and data segment of the assembly are isolated from the application that uses it.

Application Domains and Threads

Application domains (AppDomains) provide an execution boundary for the managed code. Application domains play the role of an operating system process within the CLR, but with less overhead than operating system processes. Each AppDomain is hosted in a process, with more than one AppDomain allowed per process. Application domains are switched much faster than processes. A thread is an independent path of execution and is used by the CLR to execute code. During runtime, the run-time host loads the managed code into an application domain and a particular operating system thread runs that managed code. The operating system threads can switch application domains much faster than they switch processes.

One or more managed threads can run in one or any number of application domains within the same managed process. Although each application domain is started with a single thread, code in that application domain can create additional application domains and additional threads. Therefore, a managed thread can move freely between application domains within the same managed process. No one-to-one correlation exists between application domains and threads. Several threads can be executing in a single application domain at any given time, and a particular thread is not confined to a single application domain. That is, threads are free to cross application domain boundaries and a new thread is not created for each application domain.

At any given time, every thread is executing in one application domain. The runtime keeps track of different threads that are running in different application domains. You can locate the domain in which a thread is executing at any time by calling the Thread.GetDomain method.

Programming with Application Domains

Application domains are usually created and manipulated programmatically by run-time hosts. Hence, for most applications, you do not need to create your own application domain. However, you can create and configure application domains if you create your own run-time host application or if your application needs to create or work with additional application domains that are not automatically generated by the runtime. The AppDomain class is the programmatic interface to application domains. This class includes methods to create domains, to create instances of types in domains, and to unload domains. The following C# example code shows how to create an application domain.

.NET example: Creating an application domain

The following example creates an application domain using the CreateDomain method in the System.AppDomain class and executes the assembly at the specified location in that particular application domain.

using System;
using System.Reflection;
class AppDomain1
{
public static void Main()
{
 Console.WriteLine("Creating new AppDomain.");
 //Creating a new application domain named MyDomain
 AppDomain domain = AppDomain.CreateDomain("MyDomain");
 //Executing an assembly named MyProgram.exe in the
 application domain
 domain.ExecuteAssembly("c:\\MyAssemblies\\MyProgram.exe");
 }
}

(Source File: N_ProcessVsAppDomain-UAMV4C4.01.cs)

Note More information and examples on application domains, such as configuring application domains, retrieving setup information from application domains, loading assemblies into application domains, obtaining information from assembly, and unloading application domains is available at https://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemappdomainclasstopic.asp.

Processes vs. Threads

This section will help you understand the programming differences between UNIX processes and .NET threads. In the following example, the UNIX code is forking a process, but not executing a separate run-time image. This creates a separate execution path within the application. In Windows, this is achieved by using threads instead of processes.

UNIX example: Code for forking executable

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main()
{
pid_t pid;
int n;
printf("fork program started\n");
pid = fork();
switch(pid)
{
case -1:
perror("fork failed");
exit(1);
case 0:
puts("I'm the child");
break;
default:
puts("I'm the parent");
break;
}
exit(0);
}

(Source File: U_Fork-UAMV4C4.01.c))

In .NET, the CLR uses application domains instead of processes. By using the appropriate methods in the .NET Framework class library, you can instantiate a new application domain and call a managed program. However, this is not a very efficient technique for performing multiple operations. Instead, consider using threads.

You can use the System.Threading namespace of .NET to create and manage threads, which operate within the bounds of the CLR. Threading in .NET is discussed in detail in the “Thread Management” section later in this chapter. The Win32 API provides a CreateThread method to achieve a similar functionality. A call to the Win32 API CreateThread is an unmanaged call, however, and the threads created by this method will operate outside the bounds of the CLR.

Managing Process Resource Limits

Developers often create processes that run with a specific set of resource restrictions. In some cases, they may impose the restrictions for stress testing or forced failure condition testing. In other cases, however, the limitations may be imposed to restrict runaway processes from using up all the available memory, CPU cycles, or disk space. In UNIX, the getrlimit function retrieves resource limits for a process, the getrusage function retrieves current usage, and the setrlimit function sets new limits. Table 4.1 lists the common limit names and their descriptions.

Table 4.1. Common Limit Names and Their Descriptions

Limit

Description

RLIMIT_CORE

The maximum size of a core file (in bytes) created by a process. If the core file is larger than RLIMIT_CORE, the write is terminated at this value. If the limit is set to 0, then no core files are created.

RLIMIT_CPU

The maximum CPU time (in seconds) that a process can use. If the process exceeds this time, the system generates SIGXCPU for the process.

RLIMIT_DATA

The maximum data segment size (in bytes) of a process. If the data segment exceeds this value, the functions brk, malloc, and sbrk will fail.

RLIMIT_FSIZE

The maximum size of a file (in bytes) created by a process. If the limit is 0, the process cannot create a file. If a write or truncation call exceeds the limit, further attempts will fail.

RLIMIT_NOFILE

The highest possible value for a file descriptor, plus one. This limits the number of file descriptors a process may allocate. If more files are allocated, functions allocating new file descriptors may fail with the error, EMFILE.

RLIMIT_STACK

The maximum stack size (in bytes) of a process. The stack will not automatically exceed this limit; if a process tries to exceed the limit, the system generates SIGSEGV for the process.

RLIMIT_AS

The maximum total available memory (in bytes) for a process. If this limit is exceeded, the memory functions brk, malloc, mmap, and sbrk will fail with errno set to ENOMEM, and automatic stack growth will fail as described for RLIMIT_STACK.

In .NET, you can impose resource restrictions on a process by using the various properties and members of the Process class. Table 4.2 lists the properties and methods that you can use to enforce the restrictions.

Table 4.2. Process Class Properties and Their Description

Property

Description

MaxWorkingSet

Gets or sets the maximum allowable working set size for the associated process.

MinWorkingSet

Gets or sets the minimum allowable working set size for the associated process.

PriorityClass

Gets or sets the overall priority category for the associated process.

ProcessorAffinity

Gets or sets the processors on which the threads in this process can be scheduled to run.

Threads

Gets the set of threads that are running in the associated process.

Thread Management

This section describes the functionality of threads and their usage in the UNIX and .NET environments. In thread management, you need to consider the following core areas when migrating UNIX applications to the .NET environment:

  • Creating a thread.

  • Terminating a thread.

  • Thread synchronization.

  • Thread scheduling and priorities.

The following sections describe these areas in detail.

A thread is an independent path of execution in a process that shares the address space, code, and global data of the process. Time slices are allocated to each thread based on priority and consist of an independent set of registers, stack, input/output (I/O) handles, and message queue. Threads can usually run on separate processors on a multiprocessor computer. Win32 enables you to assign threads to a specific processor on a multiprocessor hardware platform.

An application using multiple processes usually must implement some form of interprocess communication (IPC). This can result in significant overhead and possibly a communication bottleneck. In contrast, threads share the process data among them and the interthread communication can be much faster. However, threads sharing data can lead to data access conflicts between multiple threads. You can address these conflicts using synchronization techniques, such as semaphores and mutexes.

In UNIX, developers implement threads by using the POSIX pthread functions. In .NET, developers implement threading by using the System.Threading namespace. The following section describes how you should go about converting UNIX threaded applications into .NET threaded applications. As discussed in the section on processes, you may also decide to convert some of the UNIX processes of an application into threads for better performance.

Note More information on thread management functions in the Threading namespace, System.Threading, is available at https://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemthreading.asp.

Creating a Thread

This section compares the UNIX and .NET functionalities for creating threads and provides examples. When creating a thread in UNIX, use the pthread_create function. This function has three arguments: a pointer to a data structure that describes the thread, an argument specifying the attributes (usually set to NULL indicating default settings) of the thread, and the function that the thread will run. The thread finishes execution with a pthread_exit, where (in this case) it returns a string. The process can wait for the thread to complete using the function pthread_join.

UNIX example: Creating a thread

The following code example shows how to create a thread in UNIX and waits for it to finish.

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
char message[] = "Hello World";
void *thread_function(void *arg) {
    printf("thread_function started. Arg was %s\n",
(char *)arg);
    sleep(3);
    strcpy(message, "Bye!");
    pthread_exit("See Ya");
}
int main() {
    int res;
    pthread_t a_thread;
    void *thread_result;
    res = pthread_create(&a_thread, NULL, 
thread_function, (void *)message);
    if (res != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }
    printf("Waiting for thread to finish...\n");
    res = pthread_join(a_thread, &thread_result);
    if (res != 0) {
        perror("Thread join failed");
        exit(EXIT_FAILURE);
    }
    printf("Thread joined, it returned %s\n", 
(char *)thread_result);
    printf("Message is now %s\n", message);
    exit(EXIT_SUCCESS);
}

(Source File: U_CreatingThread-UAMV4C4.01.c)

In .NET, the Thread class and its associates in the System::Threading namespace are used to create a thread.

To create and run a thread in .NET

  1. Create a ThreadStart delegate that refers to the method that the thread would execute.

  2. Create a Thread object using the ThreadStart delegate.

  3. Call the start method of the Thread object.

Unlike UNIX, you must always start .NET threads explicitly after they are created. The ThreadStart delegate must refer to a void method that takes no parameters. This implies that you cannot start a thread using a method that takes parameters or obtain a return value from the method. To pass data to a thread, create an object to hold the data and the thread method, as illustrated by the code example that follows. To retrieve the results of a thread method, use a callback method.

.NET example: Creating a thread

The following Managed C++ example shows how to create a thread in the .NET Framework.

#using <mscorlib.dll>
 using namespace System;
 using namespace System::Threading;
 //The managed class ThreadExample
 public __gc class ThreadExample 
 {
 private:
 String* tMessage;
 public:
 //Constructor of ThreadExample, which is used to pass arguments
 ThreadExample(String* argMessage)
 {
     tMessage = argMessage;
 }
 //The method that is executed as a seperate thread
 void thread_function()
 {
         Console::WriteLine(S"thread_function started");
      Console::WriteLine(S"Message is {0}",tMessage);
         Thread::Sleep(3000);
         tMessage = S"Bye!";
      Console::WriteLine(S"Message is {0}",tMessage);
 }
 };
 int main() 
 {
       String *message = S"Hello World";
       ThreadExample *obTex = new ThreadExample(message);
       /* Instantiates the Thread class to execute
thread_function as a seperate  thread 
       */
Thread *oThread = new Thread(new ThreadStart(obTex,
&ThreadExample::thread_function));
        //Starts the thread
       oThread->Start();
       Console::WriteLine(S"Waiting for thread to finish...");
       oThread->Join();
       Console::WriteLine(S"Thread Joined");
       return 0;
 }

(Source File: N_CreatingThread-UAMV4C4.01.cpp)

Terminating a Thread

This section compares the UNIX and .NET functionalities for terminating threads and provides examples. UNIX uses the POSIX pthread_cancel function to terminate a thread. UNIX also offers facilities that allow a thread to specify if it is to be terminated immediately or deferred until it reaches a safe recovery point. Moreover, UNIX provides a facility known as cancellation cleanup handlers, which a thread can push and pop from a stack that is invoked in a last-in-first-out order when the thread is terminated. These cleanup handlers are coded to clean up and restore the resources before the thread is actually terminated.

UNIX example: Terminating a thread

The following code sample shows how to terminate a thread in UNIX.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_function(void *arg) {
    int i, res;
    res = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    if (res != 0) {
        perror("Thread pthread_setcancelstate failed");
        exit(EXIT_FAILURE);
    }
    res = pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
    if (res != 0) {
        perror("Thread pthread_setcanceltype failed");
        exit(EXIT_FAILURE);
    }
    printf("thread_function is running\n");
    for(i = 0; i < 10; i++) {
        printf("Thread is running (%d)...\n", i);
        sleep(1);
    }
    pthread_exit(0);
}
int main() {
    int res;
    pthread_t a_thread;
    void *thread_result;
    res = pthread_create(&a_thread, NULL, thread_function, NULL);
    if (res != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }
    sleep(3);
    printf("Cancelling thread...\n");
    res = pthread_cancel(a_thread);
    if (res != 0) {
        perror("Thread cancellation failed");
        exit(EXIT_FAILURE);
    }
    printf("Waiting for thread to finish...\n");
    res = pthread_join(a_thread, &thread_result);
    if (res != 0) {
        perror("Thread join failed");
        exit(EXIT_FAILURE);
    }
    exit(EXIT_SUCCESS);
}

(Source File: U_TerminatingThread-UAMV4C4.01.c)

In .NET, the Thread.Abort method is used to stop a thread permanently. When Abort is called, the CLR throws a ThreadAbortException in the thread on which Abort is invoked to begin the process of terminating the thread. Calling this method usually terminates the thread. The cleanup operations are handled by the CLR.

Thread.Abort does not abort the thread immediately. To ensure that the thread is stopped, you must call Thread.Join to wait on the thread. Join is a blocking call that does not return until the thread has actually stopped executing. After a thread is aborted, it cannot be restarted.

You can also call Thread.Join and pass a time-out period. If the thread dies before the timeout has elapsed, the call returns true. If the time expires before the thread dies, the call returns false. Other threads that call Thread.Interrupt can interrupt the threads that are waiting on a call to Thread.Join.

.NET example: Terminating a thread

The following sample code shows how to terminate a thread in .NET using the Thread.Abort method with managed C++.

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#using <mscorlib.dll>
using namespace System;
using namespace System::Threading;
 //The managed class ThreadExample
 public __gc class ThreadExample 
 {
 private:
 String* tMessage;
 public:
 //Constructor of ThreadExample, which is used to pass
 arguments
 ThreadExample(String* argMessage)
 {
     tMessage = argMessage;
 }
 //The method that is executed as a separate thread
 void thread_function()
 {
 try{
        Console::WriteLine(S"thread_function is running");
        Console::WriteLine(S"Message is {0}",tMessage);
       for(int i = 0; i < 10; i++) 
        {
        Console::WriteLine(S"Thread is running ({0})...",
i.ToString());
        Thread::Sleep(1000);
        }
          } catch(ThreadAbortException *e) {
          Console::WriteLine(e->Message);
            }                        
 }
 };
 int main() 
 {
 String *message = S"Hello World";
/* 
Instantiates the Thread class to execute 
thread_function as a seperate  thread 
       */
    ThreadExample *obTex = new ThreadExample(message);
Thread *oThread = new Thread(new  ThreadStart(obTex,
&ThreadExample::thread_function));
oThread->Start();
//Makes the main thread to sleep for 3 seconds
    Thread::Sleep(3000);
     Console::WriteLine(S"Cancelling Thread...");
    //Terminates the thread
    oThread->Abort();
    Console::WriteLine(S"Waiting for thread to finish...");
oThread->Join();
    return 0;
 }

(Source File: N_TerminatingThread-UAMV4C4.01.cpp)

Suspend, Resume, and Sleep Methods

In .NET, the Thread class also provides a Suspend method, which can be used to temporarily halt the execution of a thread. The execution of the halted thread can be resumed by calling the Resume method. Use the Sleep method to stop execution of a thread for a short period and restart the thread when this period is over.

Thread Synchronization

This section describes usage of the thread synchronization mechanism and various thread synchronization techniques available in the UNIX and .NET environments. When more than one thread is executing simultaneously, you have to take the initiative to protect shared resources. For example, if the thread increments a variable, you cannot predict the result because the variable may have been modified by another thread before or after the increment. You cannot predict the result because the order in which threads access a shared resource is indeterminate. UNIX and Windows provide mechanisms, called synchronization techniques, for controlling resource access. These techniques are discussed in the following sections. The next section explains the indeterminate behavior of the threads when no synchronization techniques are used. The subsequent sections elaborate on how you can handle this scenario using the various synchronizing techniques.

Multiple Nonsynchronized Threads

The following example illustrates code that is, in principle, indeterminate. The parent represents the main thread in the example. It generates a “P” and the child or the secondary thread outputs a “T”.

Note   This is a very simple example and, on most computers, the result would always be the same, but the important point to note is that this result is not guaranteed.

UNIX example: Threads with no synchronization

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_function(void *arg) {
    int count2;
    printf("thread_function is running. Argument was:
%s\n", (char *)arg);
    for (count2 = 0; count2 < 10; count2++) {
        sleep(1);
        printf("T");
    }
    sleep(3);
}
char message[] = "Hello I'm a Thread";
int main() {
    int count1, res;
    pthread_t a_thread;
    void *thread_result;
    res = pthread_create(&a_thread, NULL, thread_function
, (void *)message);
    if (res != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }
    printf("entering loop\n");
    for (count1 = 0; count1 < 10; count1++) {
        sleep(1);
        printf("P");
    }
    printf("\nWaiting for thread to finish...\n");
    res = pthread_join(a_thread, &thread_result);
    if (res != 0) {
        perror("Thread join failed");
        exit(EXIT_FAILURE);
    }
    printf("\nThread joined\n");
    exit(EXIT_SUCCESS);
}

(Source File: U_SyncTechnique-UAMV4C4.01.c)

.NET example: Threads with no synchronization

#using <mscorlib.dll>
 using namespace System;
 using namespace System::Threading;
 public __gc class ThreadExample 
 {
 public:
     static void ThreadProc()
     {
         for (int i = 0; i < 10; i++) 
         {
             Thread::Sleep(1000);
          Console::Write(S"T");
         }
      Thread::Sleep(3000);
     }
 };
 int main() 
 {
     Thread *oThread = new Thread(new ThreadStart(0, 
&ThreadExample::ThreadProc));
     oThread->Start();
     Console::WriteLine(S"entering loop");
     for (int i = 0; i < 10; i++) 
     {
       Thread::Sleep(1000);
    Console::Write(S"P");
     }
     Console::WriteLine(S"Waiting for thread to finish");
     oThread->Join();
     Console::WriteLine(S"Thread Joined");
     return 0;
 }

(Source File: N_SyncTechnique-UAMV4C4.01.cpp)

In the UNIX example, access to thread_function has not been synchronized; and in the .NET example, the access to ThreadProc has not been synchronized. It is not possible to predict the output of these code examples. In most applications, unpredictable results are an undesirable feature. Therefore, it is important to control access to shared resources in threaded code. UNIX and .NET provide mechanisms for controlling resource access.

Synchronization Techniques

Synchronization techniques are used for the following reasons:

  • To explicitly control the order in which code runs whenever tasks must be performed in a specific sequence.

  • To prevent the problems that can occur when two threads share the same resource at the same time.

There are two approaches to synchronization: polling and using synchronization objects.

Note   More information on polling is available at
https://msdn.microsoft.com/library/en-us/vbcn7/html/vaconthreadsynchronization.asp.

The next sections discuss the synchronization objects in detail.

Advanced Synchronization Techniques in .NET

The .NET Framework provides a number of objects that help in creating and managing multithreaded applications. WaitHandle objects help you respond to actions taken by other threads, especially when interoperating with unmanaged code. The ThreadPool provides the best basic thread creation and management mechanism for most tasks. Monitor, Mutex, Interlocked, and ReaderWriterLock objects provide mechanisms for synchronizing execution at a low level. Timer is a flexible way to raise activities at certain intervals and I/O asynchronous completion uses the thread pool to notify when the I/O work is completed, freeing you to do other things in the meantime.

Synchronization Using Interlocked Compare Exchange

The Interlocked methods CompareExchange, Decrement, Exchange, and Increment provide a simple mechanism for synchronizing access to a variable that is shared by multiple threads. The threads of different processes can use this mechanism if the variable is in shared memory.

The Increment and Decrement functions combine the operations of incrementing or decrementing the variable and checking the resulting value. This atomic operation is useful in a multitasking operating system in which the system can interrupt execution of one thread to grant a slice of processor time to another thread. Without such synchronization, one thread could increment a variable but be interrupted by the system before it could check the resulting value of the variable. A second thread could then increment the same variable. When the first thread receives its next time slice, it will check the value of the variable, which has now been incremented not once but twice. The Interlocked variable access functions prevent this kind of error.

The Exchange function atomically exchanges the values of the specified variables. The CompareExchange function combines two operations: comparing two values and storing a third value in one of the variables based on the outcome of the comparison. You can use CompareExchange to protect computations that are more complicated than simple increment and decrement. The following example demonstrates a thread-safe method that adds to a running total.

The following example code uses the CompareExchange method of the Interlocked class to synchronize the threads.

.NET example: Thread synchronization using Interlocked class

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#using <mscorlib.dll>
 using namespace System;
 using namespace System::Threading;
 public __gc class ThreadExample 
 {
 public:
     int run_now;
 public:
     ThreadExample()
     {
     }
     ThreadExample(int iRun)
     {
         run_now = iRun;
     }
 public:
    void ThreadProc()
     {
          Console::WriteLine(S"thread_function is running.");
    for (int i = 0; i < 10; i++) 
       {
        int iLockCompare = Interlocked::CompareExchange
(&run_now,1,2);
        if(iLockCompare ==2)
        {
          Console::Write(S"T-2");
        }
        else
        {
          Thread::Sleep(1000);
        }
    }
       Console::WriteLine(S"Child Thread Terminating");
    Thread::Sleep(3000);
     }
     void ParentThread()
     {
     Console::WriteLine(S"Main thread: Start a second thread.");
 Thread *oThread = new Thread(new ThreadStart(this,
 &ThreadExample::ThreadProc));
    oThread->Start();
    Console::WriteLine(S"entering loop");
    for (int i = 0; i < 10; i++) 
    {
     int iLock = Interlocked::CompareExchange(&run_now,2,1);
     if(iLock==1)
     {
       Console::Write(S"P-1");
     }
     else
      Thread::Sleep(1000);
    }
    Console::WriteLine(S"Waiting for thread to finish");
    oThread->Join();
    Console::WriteLine(S"Thread joined");
     }
 };
 int main()
 {
     ThreadExample *objThreadEx = new ThreadExample(1);
     objThreadEx->ParentThread();
     return 0;
 }

(Source File: N_SyncTechnique-UAMV4C4.02.cpp)

Synchronization Using Semaphores

This section describes the usage of semaphore in UNIX applications and the implementation of the similar functionality in the .NET environment. In the following UNIX example, two threads are created that use a shared memory buffer. Access to the shared memory is synchronized using a semaphore. The primary thread (main) creates a semaphore object and uses this object to handshake with the secondary thread (thread_function). The primary thread instantiates the semaphore in a state that prevents the secondary thread from acquiring the semaphore while it is initiated.

After the user types in some text at the console and presses ENTER, the primary thread relinquishes the semaphore. The secondary thread then acquires the semaphore and processes the shared memory area. At this point, the main thread is blocked and is waiting for the semaphore; it will not resume until the secondary thread has given up control by calling ReleaseSemaphore. In UNIX, the semaphore object functions of sem_post and sem_wait are all that are required to perform the handshake.

UNIX example: Synchronizing threads using semaphores

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define SHARED_SIZE 1024
char shared_area[SHARED_SIZE];
sem_t bin_sem;
void *thread_function(void *arg) {
    sem_wait(&bin_sem);
    while(strncmp("done", shared_area, 4) != 0) {
        printf("You input %d characters\n", strlen
(shared_area) -1);
        sem_wait(&bin_sem);
    }
    pthread_exit(NULL);
}
int main() {
    int res;
    pthread_t a_thread;
    void *thread_result;
    res = sem_init(&bin_sem, 0, 0);
    if (res != 0) {
        perror("Semaphore initialization failed");
        exit(EXIT_FAILURE);
    }
    res = pthread_create(&a_thread, NULL, 
thread_function, NULL);
    if (res != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }
    printf("Input some text. Enter 'done' to finish\n");
    while(strncmp("done", shared_area, 4) != 0) {
        fgets(shared_area, SHARED_SIZE, stdin);
        sem_post(&bin_sem);
    }
    printf("\nWaiting for thread to finish...\n");
    res = pthread_join(a_thread, &thread_result);
    if (res != 0) {
        perror("Thread join failed");
        exit(EXIT_FAILURE);
    }
    printf("\nThread joined\n");
    sem_destroy(&bin_sem);
    exit(EXIT_SUCCESS);
}

(Source File: U_SyncTechnique-UAMV4C4.03.c)

Semaphores are not readily available as part of the .NET Framework 1.1 class library that comes with Visual Studio .NET 2003. However, there are two ways to implement semaphores in .NET.

To implement semaphores with Win32 API using P/Invoke

  1. Make P/Invoke calls to the semaphore functions in Win32, which are present in the header file <semaphore.h>. The following examples indicate how the Win32 semaphore functions present in kernel32.dll can be made available to the .NET classes using the DllImport attribute.

    CreateSemaphore

    Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

    [DllImport("Kernel32.dll", SetLastError = true)]
    

static extern IntPtr CreateSemaphore( IntPtr lpSemaphoreAttributes, int lInitialCount, int lMaximumCount, string lpName );

**ReleaseSemaphore**

**Note:** Some of the lines in the following code have been displayed on multiple lines for better readability.

<pre IsFakePre="true" xmlns="https://www.w3.org/1999/xhtml">[DllImport("Kernel32.dll", SetLastError = true)]

static extern bool ReleaseSemaphore( IntPtr hSemaphore , int lReleaseCount, out IntPtr lpPreviousCount );

**WaitForSingleObject**

**Note:** Some of the lines in the following code have been displayed on multiple lines for better readability.

<pre IsFakePre="true" xmlns="https://www.w3.org/1999/xhtml">[DllImport("Kernel32.dll", SetLastError = true)]

static extern DWORD WaitForSingleObject(HANDLE hHandle ,DWORD dwMilliseconds);

With Win32, a combination of **WaitForSingleObject** and **ReleaseSemaphore** must be used in both the primary and the secondary threads to facilitate handshaking.
  1. Write a semaphore class using the various existing synchronization objects that .NET Framework provides.

The following Managed C++ example shows how you can write a semaphore class in .NET by using the Win32 semaphore functions in kernel32.dll through P/Invoke.

.NET example: Implementing semaphore with Win32/Win64 API

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#using <mscorlib.dll>
using namespace System;
using namespace System::ComponentModel;
using namespace System::Runtime::InteropServices;
using namespace System::Threading;
[DllImport("Kernel32.dll", CharSet=CharSet::Ansi)]
extern IntPtr __nogc* CreateSemaphore( IntPtr 
lpSemaphoreAttributes,
int lInitialCount, int lMaximumCount, String* lpName );
[DllImport("Kernel32.dll", CharSet = CharSet::Ansi)]
 extern bool ReleaseSemaphore( IntPtr hSemaphore, int
 lReleaseCount, IntPtr __nogc* lpPreviousCount );
public __gc __sealed class Semaphore : public WaitHandle
{
public:
    static int _thread = 0;
    Semaphore()
    {
    }
    Semaphore(int maxCount)
    {
Handle = CreateSemaphore(IntPtr::Zero, maxCount, 
maxCount, NULL);
           if(Handle == InvalidHandle)
           {
            throw new Win32Exception(Marshal::
GetLastWin32Error());
           }
    }
    Semaphore(int initialCount, int maxCount)
    {
Handle = CreateSemaphore(IntPtr::Zero, initialCount,
maxCount, NULL);
          if(Handle == InvalidHandle)
          {
            throw new Win32Exception(Marshal::
GetLastWin32Error());
           }
    }
    Semaphore(int maxCount, String* name)
    {
Handle = CreateSemaphore(IntPtr::Zero, maxCount, 
maxCount, name);
           if(Handle == InvalidHandle)
           {
            throw new Win32Exception(Marshal::
GetLastWin32Error());
           }
    }
    Semaphore(int initialCount, int maxCount, 
String* name)
    {
Handle = CreateSemaphore(IntPtr::Zero, initialCount, 
maxCount, name);
           if(Handle == InvalidHandle)
           {
            throw new Win32Exception(Marshal::
GetLastWin32Error());
           }
    }
    int ReleaseWin32Semaphore()
    {
           void *p;
           IntPtr __nogc* previousCount = __nogc 
new IntPtr(&p);
           if(!ReleaseSemaphore(Handle, 1,
previousCount))
          {
            throw new Win32Exception(Marshal::
GetLastWin32Error());
          }
          return previousCount->ToInt32();
    }
    int ReleaseWin32Semaphore(int count)
    {
           void *p;
           IntPtr __nogc* previousCount = __nogc 
new IntPtr(&p);
           if(!ReleaseSemaphore(Handle, count,
previousCount))
           {
            throw new Win32Exception(Marshal::
GetLastWin32Error());
           }
           return previousCount->ToInt32();
    }
     }
};

(Source File: N_SyncTechnique-UAMV4C4.03.cpp)

The latest release of .NET, Visual Studio .NET 2005, contains readymade classes for semaphores as part of the .NET Framework v2.0 class library. These semaphore classes allow you to use Win32 semaphores from managed code. More details on the semaphore class is available at https://msdn2.microsoft.com/en-US/library/system.threading.semaphore.aspx.

Synchronization Using Mutexes

A mutex is a kernel object that provides a thread with mutually exclusive access to a single resource. Any thread of the calling process can specify the mutex-object handle in a call to one of the wait functions. The single-object wait functions return when the state of the specified object is signaled. The state of a mutex object is signaled when no thread owns it. When the state of the mutex is signaled, one waiting thread is granted ownership. The state of the mutex changes to nonsignaled and the wait function returns. Only one thread can own a mutex at any given time. The owning thread uses the ReleaseMutex function to release its ownership.

The following example code illustrates the use of mutexes to coordinate access to a shared resource and to handshake between two threads. The logic is virtually identical to the semaphore example in the previous section. The only real difference is that this example uses a mutex instead of a semaphore.

UNIX example: Thread synchronization using mutexes

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define SHARED_SIZE 1024
char shared_area[SHARED_SIZE];
pthread_mutex_t shared_mutex; /* protects shared_area */
void *thread_function(void *arg) {
    pthread_mutex_lock(&shared_mutex);
    while(strncmp("done", shared_area, 4) != 0) {
        printf("You input %d characters\n", 
strlen(shared_area) -1);
        pthread_mutex_unlock(&shared_mutex);
        pthread_mutex_lock(&shared_mutex);
    }
    pthread_mutex_unlock(&shared_mutex);
    pthread_exit(0);
}
int main() {
    int res;
    pthread_t a_thread;
    void *thread_result;
    res = pthread_mutex_init(&shared_mutex, NULL);
    if (res != 0) {
        perror("Mutex initialization failed");
        exit(EXIT_FAILURE);
    }
    res = pthread_create(&a_thread, NULL, 
thread_function, NULL);
    if (res != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }
    pthread_mutex_lock(&shared_mutex);
    printf("Input some text. Enter 'done' to finish\n");
    while (strncmp("done", shared_area, 4) != 0) {
        fgets(shared_area, SHARED_SIZE, stdin);
        pthread_mutex_unlock(&shared_mutex);
        pthread_mutex_lock(&shared_mutex);
    }
    pthread_mutex_unlock(&shared_mutex);
    printf("\nWaiting for thread to finish...\n");
    res = pthread_join(a_thread, &thread_result);
    if (res != 0) {
        perror("Thread join failed");
        exit(EXIT_FAILURE);
    }
    printf("\nThread joined\n");
    pthread_mutex_destroy(&shared_mutex);
    exit(EXIT_SUCCESS);
}

(Source File: U_SyncTechnique-UAMV4C4.04.c)

In .NET, use WaitHandle.WaitOne to request ownership of a mutex. The thread that owns a mutex can request the same mutex in repeated calls to Wait without blocking its execution. However, the thread must call the ReleaseMutex method the same number of times to release ownership of the mutex. If a thread terminates normally while owning a mutex, the state of the mutex is set to signaled and the next waiting thread gets the ownership. If no one owns the mutex, the state of the mutex is signaled.

.NET example: Thread synchronization using mutexes

#using <mscorlib.dll>
using namespace System;
using namespace System::Threading;
__gc class Test
{
public:
    String* gStr;
public:
    Test()
    {
        gStr = S"";
    }
public:
    static Mutex* mut = new Mutex();
public:
    void UseResource() 
    {
      mut->WaitOne();
      while(!gStr->Equals(S"done"))
      {
Console::WriteLine(S"The length of '{0}' is {1}", gStr, __box
(gStr->Length));
    mut->ReleaseMutex();
    mut->WaitOne();
      }
      mut->ReleaseMutex();
    }
    void Parent()
    {
      mut->WaitOne();
      Thread * myThread = new Thread(new ThreadStart
(this, Test::UseResource));
      myThread->Start();
      while(!gStr->Equals(S"done"))
      {
    gStr = Console::ReadLine();
    mut->ReleaseMutex();
    mut->WaitOne();
      }
      mut->ReleaseMutex();
      Console::WriteLine(S"Waiting for thread to finish...");
      myThread->Join();
      Console::WriteLine(S"Thread joined");
    }
};
int main() 
{
    Test* objTest = new Test();
    Console::WriteLine(S"Input some text. Enter 'done' to
finish");
    objTest->Parent();
}

(Source File: N_SyncTechnique-UAMV4C4.04.cpp)

Note More information on the threading and synchronization objects in .NET is available at https://msdn.microsoft.com/library/default.asp?url=/library/en-s/cpguide/html/cpconthreadingobjectsfeatures.asp.

Thread Scheduling and Priorities

This section describes the scheduling priority of a thread in UNIX and .NET. This section helps you convert the UNIX application with different thread priorities to .NET thread priorities. A thread in .NET can be assigned any one of the following five priority values:

  • Highest

  • AboveNormal

  • Normal

  • BelowNormal

  • Lowest

The Thread::Priority property in .NET allows you to set or change the priority of the thread to any of the five priorities. The threads created within the CLR are initially assigned the priority of ThreadPriority::Normal. Threads created outside the runtime retain the priority that they had before they entered the managed environment.

The operating system uses the priority level of all the executable threads to determine which thread gets the next slice of CPU time. The scheduling algorithm used to determine the order in which the threads are executed varies with each operating system. UNIX offers both round-robin and FIFO (first-in-first-out) scheduling algorithms, whereas Windows uses only a round-robin algorithm. This does not mean that Windows is less flexible; it just means that any fine-tuning performed on thread scheduling in UNIX is implemented differently in Windows.

Threads are scheduled for execution based on their priority. Even though threads are executing within the runtime, all threads are assigned processor time slices by the operating system. As long as a thread with a higher priority is available to run, lower priority threads are not executed. When there are no more executable threads at a given priority, the scheduler moves to the next lower priority and schedules the threads at that priority for execution. If a higher priority thread becomes executable, the lower priority thread is preempted and the higher priority thread is allowed to execute once again.

Managing Thread State and Priorities in .NET

The Thread class provides a number of members for managing the thread state and priorities. Some of these members are:

  • ThreadState. Returns the current state of the thread. The initial thread state value is Unstarted. The other values are Running, WaitSleepJoin, SuspendedRequested, Suspended, and Stopped.

  • Priority. Gets or sets the priority for the specified thread.

  • Name. Gets or sets the name of the thread. If the name is not set, it returns null.

  • CurrentContext. Gets the current context in which the thread is executing.

  • IsBackground. Specifies whether the thread should execute in the background. Background threads are stopped automatically (if they are still running) when the program finishes. (Programs will wait for foreground threads to complete before terminating.)

Example of Converting UNIX Thread Scheduling into .NET

In this example, the thread priority level is set to the lowest level. For UNIX, lowering the thread priority level requires creating an attribute object before instantiating the thread, and then setting the policy of the attribute object. After this, the thread is created with the modified attribute. On successful instantiation of the thread, the priority level is adjusted to the lowest level within the designated policy and class. In UNIX, this is accomplished by a call to pthread_attr_setschedparam.

In .NET, the priority of the thread is set to lowest by setting the priority property to ThreadPriority::Lowest.

UNIX example: Thread scheduling

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
char message[] = "Hello I’m a Thread";
int thread_finished = 0;
void *thread_function(void *arg) {
    printf("thread_function running. Arg was %s\n", 
(char *)arg);
    sleep(4);
    printf("Second thread setting finished flag, and 
exiting now\n");
    thread_finished = 1;
    pthread_exit(NULL);
}
int main() {
    int count=0, res, min_priority, max_priority;
    struct sched_param scheduling_params;
    pthread_t a_thread;
    void *thread_result;
    pthread_attr_t thread_attr;
    res = pthread_attr_init(&thread_attr);
    if (res != 0) {
        perror("Attribute creation failed");
        exit(EXIT_FAILURE);
    }
    res = pthread_attr_setschedpolicy(&thread_attr,
SCHED_OTHER);
    if (res != 0) {
        perror("Setting schedpolicy failed");
        exit(EXIT_FAILURE);
    }
    res = pthread_attr_setdetachstate(&thread_attr,
PTHREAD_CREATE_DETACHED);
    if (res != 0) {
        perror("Setting detached attribute failed");
        exit(EXIT_FAILURE);
    }
    res = pthread_create(&a_thread, &thread_attr,
thread_function, (void *)message);
    if (res != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }
    max_priority = sched_get_priority_max(SCHED_OTHER);
    min_priority = sched_get_priority_min(SCHED_OTHER);
    scheduling_params.sched_priority = min_priority;
    res = pthread_attr_setschedparam(&thread_attr,
&scheduling_params);
    if (res != 0) {
        perror("Setting schedparam failed");
        exit(EXIT_FAILURE);
    }
    (void)pthread_attr_destroy(&thread_attr);
    while(!thread_finished) {
        printf("Waiting for thread to finish (%d)\n",
++count);
        sleep(1);
    }
    printf("Other thread finished, See Ya!\n");
    exit(EXIT_SUCCESS);
}

(Source File: U_ThreadSchedule-UAMV4C4.01.c)

.NET example: Thread scheduling

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#using <mscorlib.dll>
using namespace System;
using namespace System::Threading;
int thread_finished = 0;
public __gc class ThreadExample 
{
 private:
 String* tMessage;
 public:
 ThreadExample(String* argMessage)
 {
     tMessage = argMessage;
 }
 void thread_function()
 {
     Console::WriteLine(S"thread_function running.");
     Console::WriteLine(S"Message is {0}",tMessage);
     Thread::Sleep(4000);
     Console::WriteLine(S"Second thread finished, 
setting flag, and exiting now\n");
     thread_finished = 1;
 }
 };
 int main() 
 {
     int count = 0;
     String *message = S"Hello! I am Thread";
     ThreadExample *obTex = new ThreadExample(message);
 Thread *oThread = new Thread(new       ThreadStart
 (obTex,&ThreadExample::thread_function));
 oThread->Start();
     oThread->Priority = ThreadPriority::Lowest;
     while(!thread_finished)
     {
         ++count;
Console::WriteLine("Waiting for the other thread to 
finish ({0})",count.ToString());
         Thread::Sleep(1000);
     }
     Console::WriteLine(S"Other thread finished, bye!");
 return 0;
   }

(Source File: N_ThreadSchedule-UAMV4C4.01.cpp)

Managing Multiple Threads

In the following UNIX example, numerous threads are created that terminate at random times. Their termination and display messages are then caught to indicate their termination status.

UNIX example: Managing multiple threads

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREADS 5
void *thread_function(void *arg) {
    int t_number = *(int *)arg;
    int rand_delay;
    printf("thread_function running. Arg was %d\n",
t_number);
//  Seed the random-number generator with current time
//  so that the numbers will be different each time
//   function is run. 
    srand( (unsigned)time(NULL));
//  random time delay from 1 to 10
    rand_delay = 1+ 9.0*(float)rand()/(float)RAND_MAX;
    sleep(rand_delay);
    printf("See Ya from thread #%d\n", t_number);
    pthread_exit(NULL);
}
int main() {
    int res;
    pthread_t a_thread[NUM_THREADS];
    void *thread_result;
    int multiple_threads;
    for(multiple_threads = 0; multiple_threads < 
NUM_THREADS; multiple_threads++) {
        res = pthread_create(&(a_thread
[multiple_threads]), NULL, thread_function, (void *)
&multiple_threads);
        if (res != 0) {
            perror("Thread creation failed");
            exit(EXIT_FAILURE);
        }
        sleep(1);
    }
    printf("Waiting for threads to finish...\n");
    for(multiple_threads = NUM_THREADS - 1; 
multiple_threads >= 0; multiple_threads--) {
        res = pthread_join(a_thread[multiple_threads]
, &thread_result);
        if (res == 0) {
            printf("Another thread\n");
        }
        else {
            perror("pthread_join failed");
        }
    }
    printf("All done\n");
    exit(EXIT_SUCCESS);
}

(Source File: U_MultipleThreads-UAMV4C4.01.c)

.NET example: Managing multiple threads using ThreadPool class

.NET provides a ThreadPool class to manage multiple threads. When you use the ThreadPool class, the thread management is done by the infrastructure.

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#using <mscorlib.dll>
using namespace System;
using namespace System::Threading;
// TaskInfo holds state information for a task that will be
// executed by a ThreadPool thread.
public __gc class TaskInfo 
{
    // State information for the task.  
public:
    int Value;
    // Public constructor provides an easy way to supply all
    // the information needed for the task.
    TaskInfo(int number) 
    {
      �� Value = number;
    }
};
__gc class Example
{
public:
// This thread procedure performs the task.
    static void thread_function(Object* stateInfo) 
    {
    TaskInfo* ti = dynamic_cast<TaskInfo*>(stateInfo);
Console::WriteLine(S"Thread function running.The arg
was {0}",ti->Value.ToString());
    Random *objRand = new Random();
    int rand_delay = 1 + (objRand->Next() % 10);
        Thread::Sleep(rand_delay*1000);
        Console::WriteLine(S"See ya from thread
#{0}",__box(ti->Value));
    }
};
int main() 
{
    for(int i=0;i<4;i++)
    {
            TaskInfo *ti = new TaskInfo(i);
ThreadPool::QueueUserWorkItem(new WaitCallback(0, 
Example::thread_function),ti);
        Thread::Sleep(1000);
    }
Console::WriteLine(S"Waiting for threads to finish");
    Thread::Sleep(10000);
    Console::WriteLine(S"All done");
    return 0;
}

(Source File: N_MultipleThreads-UAMV4C4.01.cpp)

Download

Get the UNIX Custom Application Migration Guide

Update Notifications

Sign up to learn about updates and new releases

Feedback

Send us your comments or suggestions