c++ lambda捕获this 导致多线程下类释放后还在使用的错误问题

C++11引入的Lambda表达式语法中,我们可以使用[=][&]等符号来捕获当前作用域中的变量,而同时也可以使用[this]来捕获当前对象的this指针。然而,在多线程环境下,如果Lambda表达式捕获了this指针但没有正确处理,可能会导致对象在析构后依然被Lambda表达式所引用,从而产生严重的未定义行为,比如内存泄漏、指针悬挂等。

为了避免这种问题,我们需要理解Lambda表达式捕获this的工作原理,并在代码中正确使用。

下面是一个EasyX库绘图程序的示例,展示了如何使用Lambda表达式来处理多线程绘图任务。其中startRenderTask()函数将会创建一个线程,该线程将会循环执行提交的绘图任务,直到shouldStopRenderTask()函数返回true为止。renderTask()函数将会产生一个Graphics对象,然后调用renderCallback_回调函数进行绘图操作。

class Graphics {
public:
    Graphics(int width, int height) : width_(width), height_(height) {
        // 省略其他构造函数内容
    }
    ~Graphics() {
        // 省略其他析构函数内容
    }
    void DrawLine(int x1, int y1, int x2, int y2) {
        // 省略绘图代码
    }
private:
    // 省略其他成员变量与函数
    int width_, height_;
};

class RenderTask {
public:
    RenderTask(int width, int height) {
        graphics_ = new Graphics(width, height);
    }
    ~RenderTask() {
        delete graphics_;
    }
    void run() {
        // 省略绘图代码
        graphics_->DrawLine(0, 0, 100, 100);
    }
    Graphics* getGraphics() const {
        return graphics_;
    }
private:
    Graphics* graphics_;
};

class GraphicsRenderer {
public:
    GraphicsRenderer(int width, int height) : should_stop_(false), task_(width, height) {
        thread_ = std::thread(&GraphicsRenderer::startRenderTask, this);
    }
    ~GraphicsRenderer() {
        should_stop_ = true;
        thread_.join();
    }
    void setRenderCallback(std::function<void(Graphics*)> callback) {
        renderCallback_ = callback;
    }
    void submitRenderTask(std::unique_ptr<RenderTask> task) {
        std::unique_lock<std::mutex> lock(task_mutex_);
        task_queue_.push(std::move(task));
        task_condition_.notify_one();
    }
    bool shouldStopRenderTask() const {
        return should_stop_;
    }
private:
    void startRenderTask() {
        std::unique_ptr<RenderTask> task = nullptr;
        while (!shouldStopRenderTask()) {
            {
                std::unique_lock<std::mutex> lock(task_mutex_);
                while (task_queue_.empty()) {
                    task_condition_.wait(lock);
                }
                task = std::move(task_queue_.front());
                task_queue_.pop();
            }
            if (task) {
                auto graphics = task->getGraphics();
                renderCallback_(graphics);
                delete task;
                task = nullptr;
            }
        }
    }
    std::thread thread_;
    bool should_stop_;
    std::function<void(Graphics*)> renderCallback_;
    std::queue<std::unique_ptr<RenderTask>> task_queue_;
    std::mutex task_mutex_;
    std::condition_variable task_condition_;
};

在使用Lambda表达式作为回调函数时,我们需要注意几个问题:

问题 1:如何正确捕获this指针?

在回调函数中使用Lambda表达式时,我们通常需要捕获当前类对象的this指针,从而访问类的成员变量和函数。然而,如果我们使用错误的捕获方式,会出现对象在析构后仍然被Lambda表达式所引用的问题。

例如,在GraphicsRenderer::setRenderCallback()函数中,我们可能会将Lambda表达式回调函数的捕获方式设置为[=][&],从而捕获当前作用域下的所有变量。但是,这样会导致Lambda表达式捕获当前作用域下的所有变量,从而可能包括当前对象的this指针。当GraphicsRenderer对象在析构时,由于Lambda表达式仍然引用了this指针,从而可能导致未定义行为。

为了避免该问题,我们应该使用[this]作为捕获方式,这样可以保证只捕获当前对象的this指针,从而避免对象在析构后仍被Lambda表达式所引用的问题。

例如,修改GraphicsRenderer::setRenderCallback()函数如下:

void setRenderCallback(std::function<void(Graphics*)> callback) {
    renderCallback_ = [this, callback](Graphics* graphics) {
        callback(graphics);
    };
}

注意,在Lambda表达式中,我们需要将外部传入的函数指针作为参数传递给内部的Lambda表达式,从而避免捕获错误的变量。

问题 2:如何避免对象在析构后,Lambda仍然访问对象的成员变量或函数?

当使用Lambda表达式作为回调函数时,我们需要明确回调函数的生命周期,以避免在对象销毁后,Lambda表达式仍然访问对象成员变量或函数的问题。

例如,在上面的示例中,回调函数renderCallback_可能在多个线程中被调用,如果在回调函数中使用对象的成员变量或函数,很容易出现线程竞争问题,导致未定义行为。

为了避免该问题,我们可以采用以下两种方式之一:

  • 确保所有回调函数的生命周期必须在对象的生命周期内。在析构函数中等待所有回调函数执行完毕后,再释放对象。
  • 在回调函数中不要使用对象的成员变量或函数,而是在回调函数中构造一个新的std::shared_ptr指针,以指向需要访问的成员变量或函数,并捕获该指针。这样可以确保所有操作都在回调函数内完成,避免了竞争问题。

例如,在上面的示例中,我们可以使用第二种方法,修改GraphicsRenderer的相关部分代码如下:

class GraphicsRenderer {
public:
    GraphicsRenderer(int width, int height) : should_stop_(false), task_(width, height), graphics_(nullptr) {
        thread_ = std::thread(&GraphicsRenderer::startRenderTask, this);
    }
    ~GraphicsRenderer() {
        should_stop_ = true;
        thread_.join();
        graphics_.reset();
    }
    void setRenderCallback(std::function<void(std::shared_ptr<Graphics>)> callback) {
        renderCallback_ = [this, callback](Graphics* graphics) {
            std::shared_ptr<Graphics> shared_graphics(graphics, [this](Graphics* graphics) {
                delete graphics;
            });
            callback(shared_graphics);
        };
    }
    void submitRenderTask(std::unique_ptr<RenderTask> task) {
        std::unique_lock<std::mutex> lock(task_mutex_);
        task_queue_.push(std::move(task));
        task_condition_.notify_one();
    }
    bool shouldStopRenderTask() const {
        return should_stop_;
    }
private:
    void startRenderTask() {
        std::unique_ptr<RenderTask> task = nullptr;
        while (!shouldStopRenderTask()) {
            {
                std::unique_lock<std::mutex> lock(task_mutex_);
                while (task_queue_.empty()) {
                    task_condition_.wait(lock);
                }
                task = std::move(task_queue_.front());
                task_queue_.pop();
            }
            if (task) {
                graphics_.reset(task->getGraphics());
                renderCallback_(graphics_.get());
                task.reset(nullptr);
            }
        }
    }
    std::thread thread_;
    bool should_stop_;
    std::function<void(std::shared_ptr<Graphics>)> renderCallback_;
    std::queue<std::unique_ptr<RenderTask>> task_queue_;
    std::mutex task_mutex_;
    std::condition_variable task_condition_;
    std::shared_ptr<Graphics> graphics_;
};

修改后的代码中,我们将Lambda表达式改为捕获std::shared_ptr<Graphics>指针,并在回调函数中使用该指针来访问Graphics对象的成员变量和函数。该方式可以确保所有操作都在回调函数内完成,避免了竞争问题。同时,我们在std::shared_ptr的构造函数中指定了一个lambda表达式作为deleter,用于释放Graphics对象内存。需要注意的是,在shtGraphics变量被销毁时,其内部持有的指向Graphics对象的内存也会被自动释放。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:c++ lambda捕获this 导致多线程下类释放后还在使用的错误问题 - Python技术站

(0)
上一篇 2023年5月22日
下一篇 2023年5月22日

相关文章

  • Golang Gin框架实现多种数据格式返回结果详解

    Golang Gin框架是常用的Web框架之一,它提供了丰富的API和中间件,能够快速搭建Web服务,同时还支持多种数据格式的返回结果。下面是一份实现多种数据格式返回结果的攻略,包括JSON、XML、HTML和Plain Text格式的返回结果。 简介 首先,为了使用Gin框架,需要先安装Gin模块: go get -u github.com/gin-gon…

    C 2023年5月23日
    00
  • R语言的一个加法函数使用介绍

    当使用R语言进行数据分析和可视化时,经常需要编写一些自定义函数来增强数据操作的效率和可重复性。这里我为大家介绍一个R语言的加法函数,帮助大家了解如何自定义函数并灵活运用。 函数定义 首先定义一个简单的加法函数,用于计算两个数的和。 add <- function(x, y) { return(x + y) } 这里使用了R语言的函数声明语法,将函数名设…

    C 2023年5月22日
    00
  • C语言控制进程之进程等待详解

    C语言控制进程之进程等待详解 什么是进程等待 进程等待是指程序在执行过程中,等待子进程结束并获取子进程的退出状态,以便对进程执行状态进行处理。 进程等待函数 进程等待函数是 头文件中定义的,常用的有以下两个: pid_t wait(int *status) wait()函数会等待任意一个子进程,获取子进程的退出状态并存储到status指向的整型变量中,返回结…

    C 2023年5月30日
    00
  • C语言实现扫雷游戏(可展开)

    当然可以,以下是详细讲解“C语言实现扫雷游戏(可展开)”的完整攻略: 1. 确定游戏的基本规则 在开始编写程序之前,我们需要确定游戏的基本规则,包括地图大小、雷的数量和插旗操作等。以一个简单的扫雷游戏为例,我们可以设定: 地图大小:10*10; 雷的数量:10个; 插旗操作:如果玩家认为某个位置可能有雷,可以在该位置上插上一个旗帜。 2. 编写随机生成雷的函…

    C 2023年5月23日
    00
  • C 作用域规则

    C 作用域规则详解 在 C 语言中,变量的作用域指的是变量可以被访问的范围。C 语言定义了几种作用域,其中包括块作用域、函数作用域、文件作用域和函数形参作用域等。本文将详细介绍 C 作用域规则以及示例说明。 1. 块作用域 块作用域是指只能在定义变量的块或函数内使用变量的作用域。块作用域中定义的变量通常称为局部变量。 1.1. 示例 1 #include &…

    C 2023年5月10日
    00
  • C程序 计算自然数之和

    让我为您详细讲解如何使用“C程序 计算自然数之和”。 什么是C程序 计算自然数之和 “C程序 计算自然数之和”是一段使用C语言编写的程序,它可以计算从1到N的所有自然数之和,并将结果输出到屏幕上。该程序能够帮助学习C语言的初学者熟悉基础语法和算法思想。 如何使用C程序 计算自然数之和 使用C程序 计算自然数之和非常简单,您只需要按照以下步骤进行操作即可。 1…

    C 2023年5月10日
    00
  • C指针原理教程之编译原理-小型计算器实现

    为了实现一个小型计算器,我们需要了解C语言中指针的使用原理和编译原理。以下是详细的攻略: 编译原理基础 编译器是将高级语言程序转换为机器语言程序的软件。编译过程分为四个阶段:预处理、编译、汇编和链接。具体步骤如下: 预处理:处理以#开头的预编译指令,如#include和#define。 编译:将源代码翻译成汇编语言。 汇编:将汇编语言转换成机器代码。 链接:…

    C 2023年5月23日
    00
  • PHP 实现 JSON 数据的编码和解码操作详解

    PHP 实现 JSON 数据的编码和解码操作详解 JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,通常用于前后端数据交互。PHP 提供了对 JSON 数据的编码和解码支持,使得开发者可以方便地将 PHP 数据结构转换成 JSON 数据字符串,或将 JSON 数据字符串转换成 PHP 数据结构。 JSON 编码 PH…

    C 2023年5月23日
    00
合作推广
合作推广
分享本页
返回顶部