Location>code7788 >text

C++ lambda reference capture of temporary object triggers core case

Popularity:558 ℃/2024-08-31 20:15:58

Today review the first few years in the project process in the accumulation of various types of technical cases, there is a small coredump case, when several more senior colleagues in the group did not see, the back of the weekend is that I checked two or three hours to solve the problem, and then today to do a systematic summary of the case to give a reproduction of the case code, the case code is relatively simple, easy to learn and understand.

1. Introduction

Principle: Temporary objects should not be captured by lambda references because they are destructed at the end of the statement they are in, and should only be captured by value.
We can make this low-level mistake when temporary objects are more hidden. In this paper, we introduce a class of CASE: take a const reference to a base class smart pointer object as a function formal parameter and do reference capture of that parameter within the function, and then use it asynchronously across threads. When the function caller uses a derived class smart pointer as a real parameter, the derived class smart pointer object will be upconverted to a base class smart pointer object, the conversion is implicit, and the resulting object is a temporary object, which is then captured by a lambda reference, and subsequent cross-threaded use triggers a "wild reference" core.

2. Cases

The following is a simple demo code to simulate this case. The code flow involved in the case is shown in the following figure:

The base class BaseTask, the derived class DerivedTask, and the main function throws the lambda closure into the worker thread for asynchronous execution.
Detailed sample code is as follows:

/**
 * @brief Keywords.:lambda、multi-threaded、std::shared_ptr implicit upconversion
 * g++ -std=c++17 -O3 -lpthread
 */

#include <atomic>
#include <chrono>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
#include <queue>
#include <string>
#include <thread>

using namespace std::chrono_literals;

/// simple thread pool
template <typename Func>
class ThreadPool {
 public:
  ~ThreadPool() {
    stop_ = true;
    for (auto& item : workers_) {
      ();
    }
  }

  void Run() {
    static constexpr uint32_t kThreadNum = 2;

    uint32_t idx = 0;
    for (uint32_t idx = 0; idx != kThreadNum; ++idx) {
      workers_.emplace_back(std::thread([this, idx] { ThreadFunc(idx); }));
      mutexs_.emplace_back(std::make_shared<std::mutex>());
    }

    job_queues_.resize(kThreadNum);
  }

  void Stop() { stop_ = true; }

  bool Post(Func&& f) {
    if (!stop_) {
      uint32_t index = ++job_cnt_ % job_queues_.size();
      auto& queue = job_queues_[index];
      std::lock_guard<std::mutex> locker(*mutexs_[index]);
      (std::move(f));
      return true;
    }

    return false;
  }

  void ThreadFunc(uint32_t idx) {
    auto& queue = job_queues_[idx];
    auto& mutex = *mutexs_[idx];
    // Empty the task queue before exiting
    while (true) {
      if (!()) {
        std::lock_guard<std::mutex> locker(mutex);
        const auto& job_func = ();
        job_func();
        ();
      } else if (!stop_) {
        std::this_thread::sleep_for(10ms);
      } else {
        break;
      }
    }
  }

 private:
  /// worker thread pool
  std::vector<std::thread> workers_;
  /// task queue,One queue per worker thread
  std::vector<std::queue<Func>> job_queues_;
  /// task queue的读写保护锁,One lock per worker thread
  std::vector<std::shared_ptr<std::mutex>> mutexs_;
  /// Whether to stop working
  bool stop_ = false;
  /// task count,用于将任务均衡分配给multi-threaded队列
  std::atomic<uint32_t> job_cnt_ = 0;
};
using MyThreadPool = ThreadPool<std::function<void()>>;

/// base classtask
class BaseTask {
 public:
  virtual ~BaseTask() = default;
  virtual void DoSomething() = 0;
};
using BaseTaskPtr = std::shared_ptr<BaseTask>;

/// derive (from raw material)task
class DeriveTask : public BaseTask {
 public:
  void DoSomething() override {
    std::cout << "derive task do someting" << std::endl;
  }
};
using DeriveTaskPtr = std::shared_ptr<DeriveTask>;

/// sample user (computing)
class User {
 public:
  User() { thread_pool_.Run(); }
  ~User() { thread_pool_.Stop(); }

  void DoJobAsync(const BaseTaskPtr& task) {
    // task be user->DoJob Calling the resulting temporary object,The reference that captures it becomes an also pointer
    thread_pool_.Post([&task] { task->DoSomething(); });
  }

 private:
  MyThreadPool thread_pool_;
};

using UserPtr = std::shared_ptr<User>;

/// The test runs out core
int main() {
  auto user = std::make_shared<User>();
  DeriveTaskPtr derive_task1 = std::make_shared<DeriveTask>();
  // derive_task will be implicitly converted to BaseTask Smart Pointer Objects,
  // 该对象be临时对象,exist DoJob End of life cycle after execution。
  user->DoJobAsync(derive_task1);

  DeriveTaskPtr derive_task3 = std::make_shared<DeriveTask>();
  user->DoJobAsync(derive_task3);

  std::this_thread::sleep_for(std::chrono::seconds(3));
  return 0;
}

In the example code above, there is a coredump, or the DoSomething of the derived class is not executed, in short, it doesn't work as expected. The reason for this is as follows: the code posts a lambda function to a thread, and the lambda function references the captured smart pointer object, which is a temporary object that will be destructed after it leaves the domain, causing the lambda function to access a "wild reference" in an asynchronous thread, causing the asynchronous thread to execute incorrectly. The reason why the captured smart pointer is a temporary object is because of the type up conversion that occurs during the call.

The above example is still relatively easy to see the point of the problem, but when our project code level is deeper, this kind of error is very difficult to see, and therefore the senior colleagues in the previous team were unable to find out what the problem is.

There are various solutions to this type of problem:
(1) Method 1: Avoid implicit conversions and eliminate temporary objects;
(2) Method 2: function and lambda capture are modified to bare pointers, eliminating temporary objects; reference is essentially a pointer, you need to pay attention to the life cycle, since the use of the reference parameter means that the caller needs to safeguard the object's lifecycle, the reference of the smart pointer in the use of the pointer is no different, so it is not as good as using bare pointers, so the caller is more aware of their need to safeguard the object's lifecycle;
(3) Approach 3: Value capture/value passing is used for asynchronous execution without reference capture, but value capture may lead to wasted performance, specifically for the example in this paper, the performance overhead here is the construction of a smart pointer object, which is not a big performance loss and is acceptable.

3. Other

The life cycle of temporary objects can be found in this document:/w/cpp/language/reference_initialization#Lifetime_of_a_temporary