之前的三个代码接口使用了同一把锁,共享资源的访问是有序执行的没有问题。最近改成各个接口使用单独的锁,结果漏掉了共享资源的保护,于是出现了崩溃。最近与这个崩溃做斗争并定位找到的原因,成功复现了。这里总结下,后续涉及多线程访问的务必考虑周全。
崩溃信息描述
windbg工具分析
srv*C:\symbols*http://msdl.microsoft.com/download/symbols
!analyze -v
以下是使用windbg工具分析出来的崩溃堆栈信息:
堆栈信息表明,发生了堆损坏(heap corruption),可能和双重释放(double free)有关。
可能的原因
-
双重释放(Double Free):
- 在某些情况下,可能会对同一个内存块进行两次释放操作,这会导致堆损坏和崩溃。
-
内存越界访问:
- 在代码中,可能会对内存进行越界访问,例如写入超出分配内存范围的数据,这也会导致堆损坏。
-
线程安全问题:
- 在
setCache
方法中没有使用,这可能导致多个线程同时修改sendBuffer_
,从而引发数据竞争和堆损坏。
- 在
成员变量 std::string 非线程安全
在C++标准库中,std::string
本身并不是线程安全的。尽管 std::string
的某些操作可能是线程安全的(例如,读取操作),但对其进行写操作时,仍然需要使用互斥锁来保护,以避免数据竞争和不一致。
线程安全的操作
- 读取操作:
- 多个线程可以同时读取同一个
std::string
对象,因为读取操作不会修改对象的状态。
- 多个线程可以同时读取同一个
非线程安全的操作
- 写操作:
- 多个线程同时对同一个
std::string
对象进行写操作会导致数据竞争和不一致。
- 多个线程同时对同一个
崩溃复现过程
多线程执行以下代码,为了复现崩溃,setCache中没有加锁。
/******************************************************************************
Welcome to GDB Online.
GDB online is an online compiler and debugger tool for C, C++, Python, Java, PHP, Ruby, Perl,
C#, OCaml, VB, Swift, Pascal, Fortran, Haskell, Objective-C, Assembly, HTML, CSS, JS, SQLite, Prolog.
Code, Compile, Run and Debug online from anywhere in world.
*******************************************************************************/
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <string.h>
class ProtocolSawtooth {
public:
struct protocolSendDataType
{
uint8_t dt1;
uint8_t dt2;
uint8_t dt3;
uint8_t cmd;
uint8_t res;
uint8_t res1;
uint8_t res2;
uint8_t res3;
};
void setCache(const std::string& data) {
//std::lock_guard<std::mutex> lock(mutex_);
char send[50] = {0};
memcpy(send, &sendDataType_, sizeof(sendDataType_));
sendBuffer_ = std::string(send, sizeof(sendDataType_));
}
const std::string& getCache() const {
std::lock_guard<std::mutex> lock(mutex_);
return sendBuffer_;
}
private:
protocolSendDataType sendDataType_{};
std::string sendBuffer_; // 成员变量
mutable std::mutex mutex_; // 互斥锁
};
void threadFunction(ProtocolSawtooth& obj, const std::string& data) {
obj.setCache(data);
}
int main()
{
std::cout<<"Hello World\n";
ProtocolSawtooth obj;
// 多线程示例
const int numThreads = 1000;
std::vector<std::thread> threads;
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back(threadFunction, std::ref(obj), "Data from Thread " + std::to_string(i));
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
// 获取缓存数据
const std::string& cache = obj.getCache();
std::cout << "Cached data: " << cache << std::endl;
return 0;
}
总结
如果一个函数在其文档中没有特别注明具备线程安全性,则应该认为它不具备。许多库大量使用了内部的静态数据,除非它是为多线程应用所设计,否则要牢记其内部数据可能没有利用互斥量进行适当的保护。类似,如果类的成员函数在其文档中没有特别注明对于多线程应用是安全的话,则认为它不安全。两个线程去操作相同的对象会引起问题,这是显而易见的,然而,即使两个线程去操作不同的物体依然会引起问题。出于多种原因,许多类使用了内部静态数据或者在多个看上去明显不同的对象间共享实现细则。
一般准则
以下给出几个一般准则:
操作系统提供的API具备线程安全性
POSIX线程标准要求C标准库中的大多数函数具备线程安全性,少数例外会在C标准中注明。
对于Windows提供的C标准库,如果所使用的版本没有问题,而且进行了正确的初始化,他们都是安全的。
C++标准库的线程安全性不是很明确,它在很大程度上依赖于使用的编译器。标准模板库线程安全性的SGI准则作为实际中的标准取得很大进展,但并不是统一的标准。所以在使用时,需充分考虑多线程的并发安全。
其他资源
std string与线程安全_这才是现代C++单例模式简单又安全的实现_51CTO博客_C++ 线程安全 单例模式
https://zhuanlan.zhihu.com/p/705622208