2

カスタム スレッド プール クラスを使用して実行するマルチスレッド アプリケーションがあります。スレッドはすべて、異なるパラメータで同じ関数を実行します。

これらのパラメーターは、次の方法でスレッドプール クラスに与えられます。

// 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() {
    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;
}
4

7 に答える 7

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);
  }
}

新規、削除、またはポインターはありません...明らかに、実装の詳細の一部がコックされている可能性がありますが、全体像を把握できます。

于 2009-08-18T12:02:17.370 に答える
4

追加のコードをありがとう。今、私たちは問題を見ることができます-

getNextJobで

if (!this->jobs.empty())
{
    job = &(this->jobs.front());
    this->jobs.pop();

「ポップ」の後、「ジョブ」が指すメモリは未定義です。参照を使用せず、実際のデータをコピーしてください。

次のようなものを試してください(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は本質的にスレッドセーフではありません...?

于 2009-08-18T12:09:32.033 に答える
2

void へのポインターで演算子 delete を使用すると、仕様に従って未定義の動作が発生します。

C++ 仕様のドラフトの第 5.3.5 章。パラグラフ3。

最初の選択肢 (オブジェクトの削除) では、オペランドの静的型がその動的型と異なる場合、静的型はオペランドの動的型の基本クラスであり、静的型は仮想デストラクタを持つか、動作が未定義です。 . 2 番目の選択肢 (配列の削除) では、削除するオブジェクトの動的な型がその静的な型と異なる場合、動作は未定義です 73)。

および対応する脚注。

これは、void* 型のオブジェクトが存在しないため、void* 型のポインターを使用してオブジェクトを削除できないことを意味します。

于 2009-11-10T15:19:00.570 に答える
1

スマートポインタまたは他のRAIIを使用してメモリを処理します。


boostまたはtr1libにアクセスできる場合は、次のようにすることができます。

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));
}
于 2009-08-18T12:27:57.260 に答える
1

オブジェクトを 2 回削除しようとすると、ヒープが既に解放されているため、2 回目は失敗します。これは正常な動作です。

さて、あなたはマルチスレッドのコンテキストにいるので...最初の削除がまだ確定されていないため、削除が「ほぼ」並行して行われる可能性があります。

于 2009-08-18T11:46:20.257 に答える
1

ジョブ キューへのすべてのアクセスは同期する必要があります。つまり、アクセス前にジョブ キューをロックすることにより、一度に 1 つのスレッドからのみ実行されます。共有リソースを保護するためのクリティカル セクションまたは同様のパターンが既にありますか? 同期の問題は、多くの場合、再現が困難な奇妙な動作やバグにつながります。

于 2009-08-18T11:44:47.980 に答える
1

この量のコードで決定的な答えを出すのは困難です。しかし、一般的に言えば、マルチスレッド プログラミングとは、複数のスレッドからアクセスされる可能性のあるデータへのアクセスを同期することです。スレッドプール クラス自体へのアクセスを保護する long またはその他の同期プリミティブがない場合、複数のスレッドが同時に削除ループに到達する可能性があり、その時点でメモリを二重に解放することがほぼ保証されます。

ジョブ関数の最後でジョブのパラメーターを削除してもクラッシュしない理由は、単一のジョブのパラメーターへのアクセスが作業キューによって既に暗黙的にシリアル化されているためである可能性があります。または、あなたはただ幸運になっているかもしれません。どちらの場合でも、ロックと同期プリミティブはコードを保護するものではなく、データを保護するものと考えるのが最善です(「クリティカル セクション」という用語は、ここでは少し誤解を招くといつも思っていました。データアクセスという観点ではなく、「コード行のセクション」と考えるように人々を導きます..この場合、複数のスレッドからジョブデータにアクセスしたいので、ロックなどで保護する必要があります.その他の同期プリミティブ。

于 2009-08-18T11:46:15.810 に答える