As a professional C++ developer, utilizing timer functions efficiently can optimize application performance and responsiveness. In this comprehensive guide, we will explore the ins and outs of working with timers in C++.

An Introduction to Timer Functions

A timer function allows timing operations and triggering events after preset intervals. It is essential for measuring code execution times, implementing delays, or scheduling repetitive tasks.

The C++ standard library provides the <chrono> and <thread> headers that include useful timer functions. However, we can also use POSIX functions like sleep() for basic interval timing.

Here are some common use cases for timer functions:

  • Benchmarking code snippets to compare performance
  • Creating countdown timers and stopwatches
  • Running periodic background tasks like data syncs
  • Implementing timeouts for function calls or network requests
  • Throttling resource usage for APIs and databases
  • Debouncing user inputs for smoother UIs

Next, we will break down the timer APIs available in C++.

Chrono Library for High-Resolution Timing

The std::chrono namespace offers the most convenient and portable way to work with timers in C++. It provides duration types and clock facilities to measure time spans and absolute timestamps.

To create a timer, we need a std::chrono::steady_clock representing a monotonic timer. We can query the current point in time using now() and later check the elapsed duration.

Here is an example benchmark for a sorting algorithm using steady_clock:

std::chrono::steady_clock::time_point start = 
    std::chrono::steady_clock::now();

// Algorithm to benchmark
std::sort(data.begin(), data.end());  

std::chrono::steady_clock::time_point end = 
    std::chrono::steady_clock::now();

std::cout << "Time elapsed: " << 
    std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() <<
    " ms" << std::endl;

For interval timing, the std::this_thread::sleep_for() function suspends execution for a given duration. Let‘s build a simple countdown timer with it:

#include <iostream>
#include <chrono>
#include <thread>

int main() {

  int seconds = 10;

  std::cout << seconds << std::endl;

  while (seconds > 0) {

    std::this_thread::sleep_for(std::chrono::seconds(1));

    seconds--;

    std::cout << seconds << std::endl;

  }

  std::cout << "Done!" << std::endl;

  return 0;
}

The chrono library uses templated duration types for code clarity. We used std::chrono::seconds for the sleep interval above. Other handy duration types are milliseconds, minutes, and hours.

Timer Callbacks with std::thread

The <thread> header offers a std::thread class to launch async tasks and set up callback timers.

We can start a new thread with a function object representing the task code. And the thread keeps running independently in the background until the function returns.

Here is an example to print a message every two seconds using thread callbacks:

#include <iostream>  
#include <chrono>
#include <thread>

void printMessage() {

  while (true) {

    std::this_thread::sleep_for(std::chrono::seconds(2));  
    std::cout << "Hello World!" << std::endl;

  }

}

int main() {

  std::thread t1(printMessage);

  t1.join(); // Wait for thread to finish

  return 0;  

}

The std::thread constructor launches printMessage() asynchronously in a new thread t1. Then t1.join() blocks execution till the thread returns.

We can also detach threads to run completely in parallel using t1.detach(). The detached threads keep executing even after main() exits.

Now let‘s build a timer with detachable thread callbacks:

#include <iostream>
#include <thread>
#include <chrono>

// Timer class 
class Timer {

  public:

  Timer(int interval, std::function<void(void)> task) {

    _interval = interval;
    _task = task;

    _thread = std::thread([this]() {

      while (true) {

        std::this_thread::sleep_for(
            std::chrono::milliseconds(_interval)
        );

        _task();

      }

    });

    _thread.detach();

  }

  private:

  std::thread _thread;
  int _interval;
  std::function<void(void)> _task;

};


// Usage

void printHello() {
  std::cout << "Hello World!" << std::endl;  
}

int main() {

  Timer t(2000, printHello);  

  std::cin.get(); // Exit on user input

  return 0;
}

Our Timer class launches a detached thread that keeps calling the given callback task forever. We pass the printHello function as the callback with a 2-second interval.

This allows running any timer logic asynchronously without blocking the main thread.

Using Timeouts with Condition Variables

The std::condition_variable class allows thread coordination with wait/notify semantics. We can build timeouts for function calls using wait_for() to abandon waiting after a certain duration.

Consider a basic work queue implementation:

#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mut;
std::queue<int> q;
std::condition_variable cond_var;

void consumer() {

  int data = 0;

  while (true) {

    std::unique_lock<std::mutex> lck(mut);

    if (q.empty()) {

      // Timeout after 3 seconds  
      if(cond_var.wait_for(lck, std::chrono::seconds(3)) == 
          std::cv_status::timeout) {

        std::cout << "No items after 3s timeout" << std::endl;
        break;

      }

    } else {

      // Consume an item  
      data = q.front(); 
      q.pop();

      lck.unlock();
      std::cout << "Consumed " << data << std::endl;

    }   

  }

}

int main() {

  // Producer thread
  std::thread producer([&]() {

    for(int i = 0; i < 5; ++i) {

      std::this_thread::sleep_for(std::chrono::seconds(1)); 

      std::unique_lock<std::mutex> lck(mut);
      q.push(i);
      lck.unlock();
      cond_var.notify_one();

    }

  });

  // Consumer thread
  std::thread consumer_thread(consumer); 

  producer.join();
  consumer_thread.join();

  return 0;
}

Here, the consumer waits on the condition variable with a mutex. If the queue is empty for more than 3 seconds, we timeout and exit.

This allows implementing timeouts for inter-thread communications using conditional waiting.

Scheduling Recurring Tasks with Timers

We can utilize timer callbacks to run periodic background jobs in a program. This is useful for data syncs, updates, redraws, etc.

Here is an example to run a task every 5 minutes using steady_clock and a callback thread:

#include <iostream>
#include <chrono>
#include <thread>
#include <functional>

// Runs a recurring task
void startRepeatingTask(int interval, std::function<void(void)> task) {

  auto lastRun = std::chrono::steady_clock::now();

  std::thread([=]() {

    while (true) {

      auto now = std::chrono::steady_clock::now();

      if (std::chrono::duration_cast<std::chrono::minutes>
          (now - lastRun).count() >= interval) {

        task();

        lastRun = now;

      }

    }

  }).detach();

}


// Sample recurring task
void doWork() {
  std::cout << "Doing work" << std::endl;  
}


int main() {

  startRepeatingTask(5, doWork);

  std::cin.get(); // Wait for input to exit

  return 0;  
}

Here startRepeatingTask launches a detached thread that keeps checking if 5 minutes have elapsed since the last run. If so, it runs the given callback task and updates lastRun.

We can expand on this idea to run any periodic jobs like database backups, log rotations, etc.

Benchmarking Code with High-Resolution Timers

Measuring algorithmic performance is vital for writing optimized C++ programs. The <chrono> library provides useful APIs for microbenchmarking code.

We usually test the execution time of a code snippet under ideal conditions to quantify its throughput. High-resolution timers allow accurate measurements by reducing noise.

Here is an example benchmark for sorting algorithms with steady_clock:

#include <iostream>
#include <vector>
#include <algorithm>
#include <chrono>

std::vector<int> getData(int size) {

  std::vector<int> data(size);
  std::generate(data.begin(), data.end(), std::rand);
  return data;

}

template <typename F>
long benchmark(F sortFn, std::vector<int> data) {

  auto start = std::chrono::steady_clock::now();

  sortFn(data.begin(), data.end());

  auto end = std::chrono::steady_clock::now();

  auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

  return duration.count();

}


int main() {

  auto data = getData(10000);

  auto timeTakenByQuicksort = benchmark(std::quick_sort, data);

  auto timeTakenByHeapsort = benchmark(std::make_heap, data);

  std::cout << "Quicksort Took " << timeTakenByQuicksort << " microseconds " << std::endl;
  std::cout << "Heapsort Took " << timeTakenByHeapsort << " microseconds" << std::endl;

  return 0;
}

We take timestamps before and after sorting using steady_clock for precision. By calculating the duration, we can compare algorithms accurately up to microseconds.

Such microbenchmarks help quantify bottlenecks when used judiciously.

Implementing Timeouts for External Calls

Adding timeouts prevents blocking indefinitely on external operations like network I/O. We can leverage timer callbacks to abort on timeout and retry when required.

Here is an example of a timeout wrapper for network requests:

#include <iostream>
#include <thread>
#include <future>

std::future<std::string> getDataFromNetwork() {

  return std::async(std::launch::async, []() {

    std::this_thread::sleep_for(std::chrono::seconds(4));  
    return "Result from network";

  });

}

auto requestWithTimeout(int timeoutMs, auto requestFn) {

  std::future<decltype(requestFn().get())> result; 

  auto timer = std::async(std::launch::async, [&]() {

    std::this_thread::sleep_for(std::chrono::milliseconds(timeoutMs));
    result.wait(); 

  });

  try {

    result = requestFn();
    result.get(); // Check for exceptions

  } catch(const std::future_error& e) {

    throw std::runtime_error("Request timed out");

  }

  timer.wait();
  return result.get();

}


int main() {

  try {

    auto result = requestWithTimeout(2000, getDataFromNetwork);
    std::cout << result << std::endl;

  } catch (const std::exception& e) {

    std::cerr << "Error: " << e.what() << ‘\n‘;

  }

  return 0;
}

We pass the network request async task and a timeout to requestWithTimeout. It races the request future against a timer future. If the timer finishes first, we throw a timeout error.

Such wrappers help set timeouts when calling external services to prevent deadlocks. We can also retry in case of transient errors.

Debouncing Inputs for Smoother UIs

For interactive UIs, we often need to debounce rapid user inputs to prevent erratic updates. A common example is live search boxes to throttle text changes.

Here is a debounce wrapper using a timer callback:

#include <functional>
#include <chrono>
#include <thread>

template <typename F>
void debounce(F inputHandler, int delayMs) {

  std::mutex m;
  bool taskPending = false;

  auto runTask = [&]() {

    inputHandler();  
    taskPending = false;

  };

  auto scheduler = [&]() {

    {
      std::lock_guard<std::mutex> lock(m);
      if (!taskPending) 
        return; 
    }

    runTask();

  };

  std::thread([&]() {

    while (true) {

      {
        std::unique_lock<std::mutex> lock(m);

        taskPending = true;

      }

      std::this_thread::sleep_for(std::chrono::milliseconds(delayMs));
      scheduler();

    }

  }).detach();

}


// Usage

void searchHandler(std::string query) {

  std::cout << "Searching for " << query << std::endl;

}

int main() {

  debounce(searchHandler, 500); 

  while (true) {

    std::string query;
    std::cin >> query;

  }

  return 0;

}

Here debounce launches a thread that delays calling the task for the given interval. Further inputs restart the timer to implement throttling.

Such rate-limiting of event handlers helps in building responsive UIs. The same concept applies for other user inputs like buttons, sliders, etc.

Conclusion

In summary, the C++ standard library provides versatile APIs for efficiently working with timers. The chrono facilities allow high-precision measurements and intervals. Thread callbacks help run timed background tasks asynchronously.

Here are some key takeaways:

  • Use steady_clock for microbenchmarking code and measuring durations
  • Implement delays and polling with sleep_for()
  • Build recurrent timers with detached thread callbacks
  • Add timeouts to external calls using futures
  • Debounce UIs by rate-limiting input handlers

By mastering these timer concepts, you can write optimized event-driven applications in C++. Identify parts of your programs that need scheduled work or timeboxing. Apply the suitable timer technique early in development.

The thread and chrono libraries complement the raw speed of C++ perfectly. Use them judiciously to craft robust and responsive applications.

Similar Posts