CIS 307: Measuring Time, Pthreads

[Time], [Pthreads], [Locks for Pthreads]

Measuring Time

Measuring time on the computer is non trivial. Here are some of the things we may want to do with time. In all these cases we have a problem of Precision and of Accuracy. By Precision we mean how finely we can specify time, in seconds, milliseconds, microseconds, and even nanoseconds. By Accuracy we mean that the measure truly represents reality. For example, we can have a clock that is precise down to the nanosecond but that gives a time that off by 0.1 seconds, i.e. its accuracy is only 0.1 seconds. My wrist-watch is precise down to the second, but it is very inaccurate: it is usually about ten minutes off. Since the measurament of durations, i.e. of time intervals, is affected by intervening events (interrupts, context switches, availability of needed buffers, etc.), it is often inaccurate. We need usually to determine durations as the average of a number of repeated experiments.

Another problem is that "the current time" may mean a number of different things. It could be the standard Greenwich time. Or it could be the time in our time zone. Or it could be the time, measured in some unit (called a tick) since a standard initial time, called the epoch, usually set at 00:00:00 on 1 Jan 1970 at Greenwich.

We can use the function time to determine the time it takes to execute a command or program. But the precision of this command is low, depending on the version, tenths or hundredths of a second. We can request the system to suspend the execution of a program with the sleep command. Infortunately the interval can only be specified in seconds.

We will consider some data types and commands that we can use for dealing with time. I highly recommend that you use these tools to measure accurately and precisely the time it takes to execute various system services and some of your code.

Here are some useful functions (getclock and ctime_r) and data type (timespec) available in Unix:

   #include <sys/timers.h>
   int getclock(int clktyp, struct timespec *tp);
          clktyp: it specifies the type of clock being used.
              We use for it the value TIMEOFDAY
	  tp: A data structure where getclock will save the
	      current time.
   struct timespec {
		time_t tv_sec;   /* number of seconds */
		long   tv_nsec;} /* number of nanoseconds */
   int ctime_r(const time_t *timer, char *buffer, int len);
	  timer:  number of seconds since epoch (1 jan 1970 at 00:00:00)
	  buffer: array where ctime_r will save the representation of
	      timer as a date string
	  len: size of buffer.
          Be sure to compile programs containing ctime_r using the
          library libc_r.a, i.e. use the compilation command modifier
          -lc_r
We can use these functions to determine the time it takes to execute a Unix command. This is done in the following program. Assuming that the corresponding executable has been placed in the file timespec, one can determine the time required to execute a shell command such as who with the call
    % timespec who
You may want to compare the values you get with timespec and who. Here is the program:

   /* timespec.c -- compile with "cc timespec.c -o timespec -lc_r"*/

   #include  <sys/types.h>
   #include  <sys/timers.h>
   #include  <stdlib.h>

   #define TIMLEN 60

   void timespec2string(struct timespec *ts, char buffer[]);

   int main(char argc, char *argv[])
   {
     char buffer[TIMLEN] = "";
     struct timespec tspec1, tspec2;

     if (argc < 2) {
       printf("Write:  timespec shellcommand\n");
       exit(1);}
     getclock((timer_t)TIMEOFDAY, &tspec1;
     if (system(argv[1]) != 0) {
       perror("system");
       exit(1);}
     getclock((timer_t)TIMEOFDAY, &tspec2);
     timespec2string(&tspec1, buffer, TIMLEN);
     printf("Before: %s\n", buffer);
     timespec2string(&tspec2, buffer, TIMLEN);
     printf("After : %s\n", buffer);
   }

   void timespec2string(struct timespec *ts, char buffer[], int len)
   /* It reads the time from ts and puts it in buffer (of size len) */
   /* as a string */
   {
     ctime_r(&(ts->tv_sec), buffer, len);
     /* ctime_r terminates the time with a carriage return. We eliminate it.*/
     /* We report time in microseconds. That is the precision on our system.*/
     sprintf(&buffer[24], " and %6d microseconds", (ts->tv_nsec)/1000);
    }

Notice that we have called getclock before and after the fragment we want to time, without printing out any information in between. This is so as to get a measure that does not include extraneous printing activities.
When doing measurements it is wise to do them a number of times and compute average and standard deviation.

Threads

We will now consider the use of threads, as they are provided on OSF/1 on our alphas with the pthreads package. Note that these commands are specific to OSF/1 and this package. On my Sun workstation, Solaris uses different thread commands.
Before I forget, remember to compile programs that use threads in one of the ways shown below

cc sourcename.c -o execname -lpthreads

cc sourcename.c -o execname -threads

As you remember, threads defined within a process share the address space of that process. This is very convenient since each thread can communicate with the other threads in that process through memory. It is also very dangerous because we get into problems of mutual exclusion and synchronization.

OSF/1 has a large number of functions for managing threads, using mutexes, using condition variables, etc. Check these services with the command

man pthread

We use the functions pthread_create and pthread_delay_np in a program just to see an example of concurrency. Here are the specifications of pthread_create and pthread_delay_np.

    #include <pthread.h>
    int pthread_create(
	pthread_t *thread,	/*The thread that is created */
	pthread_attr_t attr,    /*attributes for thread, pthread_attr_default*/
	pthread_startroutine_t start_routine, /*function executed by thread*/
	pthread_addr_t arg); /*address of argument passed to startroutine*/
        /* Returns 0 iff successful */

    #include <pthread.h>
    int pthread_delay_np(
	struct timespec *interval); /* delay thread for specified time */
    where
	struct timespec {
	    time_t tv_sec;   /* seconds */
	    long   tv_nsec;} /* nanoseconds */

    #include <pthread.h>

And here is the example program. We run three concurrent threads, and this number can be easily changed. Later on in the course we will explore ways to synchronize threads. In the program we use rand_r instead of rand to generate random numbers because rand_r is re-entrant (i.e. thread-safe). Notice that each thread executes code that writes to different locations so as not to have race conditions. The exception is the variable shrd which is write-shared to demonstreate that concurrent threads actually share the same address space.

   /* threadz.c -- compile with "cc threadz.c -o threadz -threads"*/

   #include  <sys/types.h>
   #include  <sys/timers.h>
   #include  <pthread.h>
   #include  <stdlib.h>

   #define THREADSCOUNT 3
   #define TOTALRUN   16
   #define TMIN 1.0
   #define TMAX 3.0
   #define TIMLEN 60

   int shrd;
   struct state {
     pthread_t t;          /* A thread */
     int who;              /* It identifies a thread */
     int seed;             /* The seed used for random number generator*/
     int val;              /* Value returned by random number generator */
     char buffer[TIMLEN];  /* String represnting current time */
   } states[THREADSCOUNT];

   void moo(struct state *s);

   int main(void)
   {
      int    i;
      struct timespec maintime;

      /* Initialize states */
      states[0].seed = 1507; states[1].seed = 1511; states[2].seed = 1577;
      for (i=0; i<THREADSCOUNT; i++) {
        states[i].who = i;
        if (pthread_create(&(states[i].t), pthread_attr_default, 
	                   (void *)moo, &(states[i])) != 0){
	     perror("pthread_create");
	     exit(1);}}
      /* Wait a while then exit: all existing threads will die */
      maintime.tv_sec = TOTALRUN;
      maintime.tv_nsec =0;
      pthread_delay_np(&maintime);
    }

   void getTime(struct timespec *ts, char buffer[], int len)
   /* It places the current time in ts and puts in buffer (of length len) */
   /* as a string the current time as a string */
   {
     getclock((timer_t)TIMEOFDAY, ts);
     ctime_r(&(ts->tv_sec), buffer, len);
     sprintf(&buffer[24], " and %d microseconds", (ts->tv_nsec)/1000);
   }

   void moo(struct state *s) {
     /* We assume that a thread sleeps in each loop, from a minimum of */
     /* TMIN to a maximum of TMAX, at random.                          */

     struct timespec tspec;
     struct timespec interval;
     float r;          /* random number in interval 0.0 .. 1.0 */

     while (1){
         getTime(&tspec, s->buffer, TIMLEN);
         rand_r(&(s->seed), &(s->val));
         r = (s->val)/((float)RAND_MAX); 
         shrd = s->who;
         printf("Thread %d with shrd = %d sleeps %f at time %s\n", s->who,
	        shrd, TMIN+(r*(TMAX-TMIN)), s->buffer);
         /* sleep for an a time between TMIN and TMAX */
         interval.tv_sec = TMIN + (int)(r*(TMAX-TMIN));
         interval.tv_nsec = (int)(r*1000000000);
         pthread_delay_np(&interval);
         getTime(&tspec, s->buffer, TIMLEN);
         printf("Thread %d with shrd = %d after sleep at time %s\n", 
		s->who, shrd, s->buffer);}
   }

If you run this program you will notice:

You can mark for deletion and reclaim the storage associated with a thread (of course, after it has terminated executing) with the command:

   #include <pthread.h>
   int pthread_detach(pthread_t * thread);

This command will not terminate a thread that is executing, only indicating that we want to reclaim automatically its storage when it terminates execution.

Locks for Pthreads

We can use mutual exclusion semaphores, or locks, or mutexes with pthreads. These locks should be global to the threads.

    #include <pthread.h>
    int pthread_mutex_init(
	pthread_mutex_t *mutex;    /* The mutex being created */
	pthread_mutexattr_t attr); /* usually pthread_mutexattr_default */
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_unlock(pthread_mutex_t *mutex);

There are three kinds of mutexes depending on the value of the pthread_mutexattr_t attribute. We could have MUTEX_FAST_NP (the default), to be used in the standard lock..unlock protocol; MUTEX_RECURSIVE_NP: which allows one thread to do things like "lock .. lock .. unlock .. unlock"; MUTEX_NONRECURSIVE_NP is like the fast lock, but with better debugging facilities. One normally uses for the attribute the value pthread_mutexattr_default.

With pthreads are also available condition variables. They will make the creation of monitors very easy (as we will see). Of course, these monitors will operate only within a single Unix process.

Here is a program with threads that use locks to share a resource.

   #include	<sys/types.h>
   #include        <pthread.h>

   pthread_t       t1, t2;
   pthread_mutex_t mutex;

   struct { int x;
         int y;} foo;     /*This is a global data structure shared by threads*/

   void moo(int * a);

   int main(void)
   {
     int   who[2] = {1,2};

     /* Create a mutex */
     if (pthread_mutex_init(&mutex, pthread_mutexattr_default)) {
        perror("pthread_mutex_init");
        exit(1);}

     /* Create two threads */

     if (pthread_create(&t1, pthread_attr_default, (void *)moo,&who[0]) != 0) {
	    perror("pthread_create");
	    exit(1);
	  }
     if (pthread_create(&t2, pthread_attr_default, (void *)moo, &who[1]) != 0) {
	    perror("pthread_create");
	    exit(1);
	    }
     /* Wait a while then exit: threads will die */
     sleep(16);
   }

  void moo(int * a) {

    while (1){
      if (pthread_mutex_lock(&mutex)) {
	perror("pthread_mutex_lock");
	exit(1);}
      printf("I am thread %d before sleep; x=%d, y=%d\n", *a, foo.x, foo.y);
      foo.x = foo.y = *a;
      sleep(*a);
      printf("I am thread %d after sleep; x=%d, y=%d\n", *a, foo.x, foo.y);
      if (pthread_mutex_unlock(&mutex)) {
	perror("pthread_mutex_unlock");
	exit(1);}
      /* Here is a small delay to give the other thread a chance to run */
      sleep(1);
      }
  }


ingargiola.cis.temple.edu