C++多线程下的内存管理

/ 0评 / 0

题目很大,话题很小。原谅我找不到一个合适又简短的topic形容这一个小实验。

说到正题,其实核心模型很简单。假设有两个线程,线程1创建一个栈变量,然后通过一些方式给到线程2。线程2对其进行若干的IO操作。

这种情况下可以模拟出一个不确定的crash。说不确定,是因为什么时候发生crash不确定:线程2在一些情况下既能访问线程1的栈对象,在另一些情况下又不能。取决于操作系统调度。

线程1:

void func1(int** p) {
    int x = 64;
    *p = &x;
    f = true;
    for(int i=0; i<5e7; i++) { }
    return;
}

线程2:

void func2(int** p) {
    while(f == false) { }
    for(int i=0; i<1e9; i++) {
        cout << **p << ' ' ;
    }
    return;
}

主函数:

int main()
{
    int* p = nullptr;
    thread a(func1, &p);
    thread b(func2, &p);
    a.join();
    b.join();
    return 0;
}

大致流程是,两个线程共享一个指针。其中线程1会为这个指针赋值;线程2访问这个指针所指的对象。使用一个锁来防止线程1还未赋值时,线程2进行操作。

在main函数中,首先创建线程1,这个线程将创建一个栈变量,并将变量地址传递给指针p。线程2同样接收指针p,并对其进行两次解引用进行访问。线程1在循环一定次数后退出,线程2在访问指针p一定次数后也会退出。

问题发生在线程1和2的运行耗时之中。线程1和线程2是同步工作的,但是二者没有任何保证先后结束的手段。即,线程1可能早于线程2结束,也可能晚于。

线程1销毁,意味着栈变量将被析构,即指针p指向的内存将被视为无效内存。此时如果线程2没有结束,则将引发段错误。通过调整两个线程for循环的次数,我们可以人为的让线程1先结束,即上述代码的参数。

执行结果如下:

64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
Process returned -1073741819 (0xC0000005)   execution time : 5.314 s

通过错误码可以得知,发生了段错误。


该实验的灵感来源于某个小项目中踩到的坑。鄙人在C++的coding中非常习惯用const T&来作为传参手段。凡是不需要改动数据的参数,都会用const T&进行语义的约束和代价的最小化。

这种传参手段在单线程环境下是安全的,因为参数来自于caller,则callee可以放心地使用这块内存。

但是多线程环境则不尽然。哪怕是caller和callee的关系,依然可能导致悬空引用。众所周知引用是指针的语法糖,因此前面实验中,指针的语义就有可能被破坏。

下面说说我遇到的情况:某个GUI程序,点击按钮会响应一个事件。为了解耦事件函数和真正的动作函数,我将资源的提取和实际的操作分开。事件函数从控件中得到了变量、传递const T&给动作函数;但问题在于,事件函数却在动作函数完成之前就返回了。这意味着,动作函数收到的变量被销毁了,引用传参变成了一个野引用。这个crash一度很难找,费了很多时间才发现const T&的语义也不总是靠谱的。

正确的做法是,要么使用堆内存、要么使用拷贝传参。使用堆内存时,为了避免内存泄露,还可以套一层智能指针。

关于智能指针,还可以提一个邪恶的用法:

void func() {
    int a = 0;
    unique_ptr<int> p(&a);
    return;
}

显然,变量a被析构了两次。

发表评论

邮箱地址不会被公开。 必填项已用*标注