2. Hello World

Now that the formalities are over with, lets jump right in. The pthread_create function creates a new thread. It takes four arguments, a thread variable or holder for the thread, a thread attribute, the function for the thread to call when it starts execution, and an argument to the function. For example:

  pthread_t         a_thread;
  pthread_attr_t    a_thread_attribute;
  void              thread_function(void *argument);
  char              *some_argument;
  
  pthread_create( &a_thread, a_thread_attribute, (void *)&thread_function, 
               (void *) &some_argument);
A thread attribute currently only specifies the minimum stack size to be used. In the future thread attributes may be more interesting, but for now, most applications can get by simply using the default by passing pthread_attr_default in the thread attribute parameter position. Unlike processes created by the UNIX fork function that begin execution at the same point as their parents, threads begin their execution at the function specified in pthread_create. The reason for this is clear; if threads did not start execution elsewhere we would have multiple threads executing the same instructions with the same resources. Recall that processes each have their own resource bundle and threads share theirs.

Now that we know how to create threads we are ready for our first application. Lets design a multi-threaded application that prints the beloved "Hello World" message on stdout. First we need two thread variables and we need a function for the new threads to call when they start execution. We also need some way to specify that each thread should print a different message. One approach is to partition the words into separate character strings and to give each thread a different string as its "startup" parameter. Take a look at the following code:

  void print_message_function( void *ptr ); 
  
  main()
  {
     pthread_t thread1, thread2;
     char *message1 = "Hello";
     char *message2 = "World";
     
     pthread_create( &thread1, pthread_attr_default,
                    (void*)&print_message_function, (void*) message1);
     pthread_create(&thread2, pthread_attr_default, 
                    (void*)&print_message_function, (void*) message2);
  
     exit(0);
  }
  
  void print_message_function( void *ptr )
  {
     char *message;
     message = (char *) ptr;
     printf("%s ", message);
  }
Note the function prototype for print_message_function and the casts preceding the message arguments in the pthread_create call. The program creates the first thread by calling pthread_create and passing "Hello" as its startup argument; the second thread is created with "World" as its argument. When the first thread begins execution it starts at the print_message_function with its "Hello" argument. It prints "Hello" and comes to the end of the function. A thread terminates when it leaves its initial function therefore the first thread terminates after printing "Hello." When the second thread executes it prints "World" and likewise terminates. While this program appears reasonable, there are two major flaws.

First and foremost, threads execute concurrently. Thus there is no guarantee that the first thread reaches the printf function prior to the second thread. Therefore we may see "World Hello" rather than "Hello World." There is a more subtle point. Note the call to exit made by the parent thread in the main block. If the parent thread executes the exit call prior to either of the child threads executing printf, no output will be generated at all. This is because the exit function exits the process (releases the task) thus terminating all threads. Any thread, parent or child, who calls exit can terminate all the other threads along with the process. Threads wishing to terminate explicitly must use the pthread_exit function.

Thus our little hello world program has two race conditions. The race for the exit call and the race to see which child reaches the printf call first. Lets fix the race conditions with a little crazy glue and duct tape. Since we want each child thread to finish before the parent thread, lets insert a delay in the parent that will give the children time to reach printf. To ensure that the first child thread reaches printf before the second, lets insert a delay prior to the pthread_create call that creates the second thread. The resulting code is:

  void print_message_function( void *ptr ); 
  
  main()
  {
     pthread_t thread1, thread2;
     char *message1 = "Hello";
     char *message2 = "World";
     
     pthread_create( &thread1, pthread_attr_default,
                    (void *) &print_message_function, (void *) message1);
     sleep(10);
     pthread_create(&thread2, pthread_attr_default, 
                    (void *) &print_message_function, (void *) message2);
  
     sleep(10);
     exit(0);
  }
  
  void print_message_function( void *ptr )
  {
     char *message;
     message = (char *) ptr;
     printf("%s", message);
     pthread_exit(0);
  }
Does this code meet our objective? Not really. It is never safe to rely on timing delays to perform synchronization. Because threads are so tightly coupled its tempting to approach them with a less rigorous attitude concerning synchronization, but that temptation must be avoided. The race condition here is exactly the same situation we have with a distributed application and a shared resource. The resource is the standard output and the distributed computing elements are the three threads. Thread one must use printf/stdout prior to thread two and both must do their business before the parent thread calls exit.

Beyond our attempt to synchronize using delays, we have made yet another blunder. The sleep function, like the exit function relates to processes. When a thread calls sleep the entire process sleeps, i.e., all threads sleep while the process sleeps. Thus we have exactly the same situation as we had without the calls to sleep but the program takes twenty seconds longer to run. The proper function to use when delaying a thread is pthread_delay_np (np stands for not portable). For example, to delay a thread for two seconds:

     struct timespec delay;
     delay.tv_sec = 2;
     delay.tv_nsec = 0;
     pthread_delay_np( &delay );
Functions covered in this section:
	pthread_create(), pthread_exit(), and pthread_delay_np().


Move to: Contents or Previous Section or Next Section