我有一个使用自定义线程池类运行的多线程应用程序。线程都执行相同的功能,具有不同的参数。
这些参数按以下方式提供给线程池类:
// jobParams is a struct of int, double, etc...
jobParams* params = new jobParams;
params.value1 = 2;
params.value2 = 3;
int jobId = 0;
threadPool.addJob(jobId, params);
一旦线程无关,它就会获得下一个参数并运行作业功能。我决定在threadpool类中删除参数:
ThreadPool::~ThreadPool() {
for (int i = 0; i < this->jobs.size(); ++i) {
delete this->jobs[i].params;
}
}
但是,这样做时,我有时会遇到堆损坏错误:
为RtlFreeHeap
指定的地址无效
奇怪的是,在一个案例中它完美地运行,但在另一个程序中它崩溃了这个错误。我尝试在其他地方删除指针:在执行作业函数后的线程中(我得到相同的堆损坏错误)或在作业函数本身结束时(在这种情况下没有错误)。
我不明白如何从不同的地方删除相同的指针(我检查过,地址是相同的)会改变任何东西。这与多线程的事实有什么关系吗?
我有一个关键部分来处理对参数的访问。我不认为问题是关于同步访问。无论如何,只有在完成所有线程后才会调用析构函数,并且我不会在其他地方删除任何指针。指针可以自动删除吗?
至于我的代码。作业列表是一个结构的队列,由作业的id(以后能够获得特定作业的输出)和参数组成。
每次完成执行上一个作业时,线程都会调用 getNextJob()
(它们有一个指向ThreadPool的指针)。
void ThreadPool::addJob(int jobId, void* params) {
jobData job; // jobData is a simple struct { int, void* }
job.ID = jobId;
job.params = params;
// insert parameters in the list
this->jobs.push(job);
}
jobData* ThreadPool::getNextJob() {
// get the data of the next job
jobData* job = NULL;
// we don't want to start a same job twice,
// so we make sure that we are only one at a time in this part
WaitForSingleObject(this->mutex, INFINITE);
if (!this->jobs.empty())
{
job = &(this->jobs.front());
this->jobs.pop();
}
// we're done with the exclusive part !
ReleaseMutex(this->mutex);
return job;
}
答案 0 :(得分:5)
让我们把它转过头来:你为什么要使用指针?
class Params
{
int value1, value2; // etc...
}
class ThreadJob
{
int jobID; // or whatever...
Params params;
}
class ThreadPool
{
std::list<ThreadJob> jobs;
void addJob(int job, const Params & p)
{
ThreadJob j(job, p);
jobs.push_back(j);
}
}
没有新的,删除或指针......显然,某些实现细节可能会被翘起,但是你可以了解整体情况。
答案 1 :(得分:4)
感谢额外的代码。现在我们可以看到一个问题 -
在getNextJob中
if (!this->jobs.empty())
{
job = &(this->jobs.front());
this->jobs.pop();
在“pop”之后,'job'指向的内存为 undefined 。不要使用引用,复制实际数据!
尝试这样的事情(它仍然是通用的,因为JobData是通用的):
jobData ThreadPool::getNextJob() // get the data of the next job
{
jobData job;
WaitForSingleObject(this->mutex, INFINITE);
if (!this->jobs.empty())
{
job = (this->jobs.front());
this->jobs.pop();
}
// we're done with the exclusive part !
ReleaseMutex(this->mutex);
return job;
}
此外,在向队列添加作业时,您还必须锁定互斥锁,以防止列表损坏。 AFAIK std :: lists本质上不是线程安全的......?
答案 2 :(得分:2)
根据规范,在指针上使用operator delete会导致未定义的行为。
C ++规范草案的第5.3.5节。第3段。
在第一个备选(删除对象)中,如果操作数的静态类型与其动态类型不同,则静态类型应为操作数的动态类型的基类,静态类型应具有虚拟析构函数或者行为未定义。在第二种方法(删除数组)中,如果要删除的对象的动态类型与其静态类型不同,则行为是未定义的.73)
相应的脚注。
这意味着无法使用void *类型的指针删除对象,因为没有类型为void的对象
答案 3 :(得分:1)
必须同步对作业队列的所有访问,即通过在访问之前锁定作业队列,一次仅从1个线程执行。您是否已经有一个关键部分或一些类似的模式来保护共享资源?同步问题通常会导致奇怪的行为和难以重现的错误。
答案 4 :(得分:1)
很难用这么多代码给出明确的答案。但一般来说,多线程编程都是关于同步对可能从多个线程访问的数据的访问。如果没有长或其他同步原语保护对线程池类本身的访问,那么你可能有多个线程同时到达你的删除循环,此时你几乎可以保证双重释放内存。 / p>
当您在作业结束时删除作业的参数时,您没有崩溃的原因可能是因为您的工作队列已经隐式地序列化了对单个作业的参数的访问。或者你可能会变得幸运。在任何一种情况下,最好将锁和同步原语视为不是保护代码的东西,而是保护数据的东西(我一直认为这个术语“关键部分”在这里有点误导,因为它往往会引导人们想到“代码行”而不是数据访问方面。在这种情况下,因为你想要访问你的工作数据多线程,您需要通过锁或其他同步原语来保护它。
答案 5 :(得分:1)
如果您尝试删除对象两次,则第二次将失败,因为堆已被释放。这是正常行为。
现在,由于您处于多线程上下文中...可能是“几乎”并行完成删除,这可能会避免第二次删除时出错,因为第一次删除尚未完成。
答案 6 :(得分:1)
使用smart pointers或其他RAII来处理您的记忆。
如果你有权访问boost或tr1 lib,你可以这样做。
class ThreadPool
{
typedef pair<int, function<void (void)> > Job;
list< Job > jobList;
HANDLE mutex;
public:
void addJob(int jobid, const function<void (void)>& job) {
jobList.push_back( make_pair(jobid, job) );
}
Job getNextJob() {
struct MutexLocker {
HANDLE& mutex;
MutexLocker(HANDLE& mutex) : mutex(mutex){
WaitForSingleObject(mutex, INFINITE);
}
~MutexLocker() {
ReleaseMutex(mutex);
}
};
Job job = make_pair(-1, function<void (void)>());
const MutexLocker locker(this->mutex);
if (!this->jobList.empty()) {
job = this->jobList.front();
this->jobList.pop();
}
return job;
}
};
void workWithDouble( double value );
void workWithInt( int value );
void workWithValues( int, double);
void test() {
ThreadPool pool;
//...
pool.addJob( 0, bind(&workWithDouble, 0.1));
pool.addJob( 1, bind(&workWithInt, 1));
pool.addJob( 2, bind(&workWithValues, 1, 0.1));
}