简短版:
如何处理产生一组线程的非原子性,运行一些自定义(在实现时未指定)回调?下面描述了几种可能的解决方案,似乎使用线程池是唯一的好解决方案。有没有一种标准的方法来处理它?无需发布完整的C ++解决方案,伪代码或简要描述就足够了。性能是这里的一个重要方面。
尽管看起来似乎微不足道,但我相信下面的代码片段出现在许多现有的应用程序中,许多(开始,可能还有一些高级)程序员可能会编写类似的结构,甚至没有意识到危险。 pthread / C ++ 11 std::thread
/ WinAPI以及许多其他低级多线程库的问题都是相同的。因此这是一个重要的问题。
长版:
我正在设计一些多线程应用程序,我决定创建一个实用程序函数,其中生成了多个线程。这可能是一个非常常见的代码,它出现在我的许多应用程序中(除非他们使用的是OpenMP):
void ParallelCall(void (*function)(int, int), int numThreads)
{
Thread *threads = new Thread[numThreads - 1];
for(int i = 1; i < numThreads; ++ i) {
if(threads[i - 1].start(&function, i, numThreads)) // this might fail
abort(); // what now?
}
(*function)(0, numThreads);
// use the calling thread as thread 0
for(int i = 1; i < numThreads; ++ i)
threads[i - 1].join();
delete[] threads;
}
这是一个用于说明问题的伪代码。正在创建并生成一堆线程(Thread
对象包装了一个pthread线程)。然后他们做了一些事情,最后他们加入了。
现在的问题是:如果由于某种原因,某些线程无法启动(可能是资源耗尽或每用户限制)会怎么样?我知道如何发现它发生了,但我不确定如何处理它。
我想我应该等待成功启动的线程完成然后抛出异常。但是,如果function
中的代码包含某些同步(例如屏障),则很容易导致死锁,因为其余的预期线程永远不会产生。
或者,我可以立即抛出异常,忽略正在运行的线程,但随后我将分配包装器对象,导致内存泄漏(并且从不加入生成的线程)。
像杀死正在运行的线程这样的东西似乎不是一个好主意(我坦率地不太确定强行杀死多线程应用程序的线程的结果是什么 - 似乎记忆将会处于未定义状态,这种情况大多难以处理 - 如果回调function
分配内存,本身可能会导致更多内存泄漏。
在让他们进入回调function
之前插入等待所有线程的等待似乎无法忍受性能(尽管它可以轻松解决问题)。另一个选择是拥有一个带有相关FIFO的生成线程池,等待任务,但是线程数存在问题(我会生成与逻辑CPU一样多的线程,但是如果{{1} }更大?我本质上是在我的代码中重新实现OS&#39;调度程序。)
这是如何解决的?有没有更好的办法?如果没有,那么潜在的(取决于回调numThreads
中的内容)死锁是否比内存泄漏更好?
答案 0 :(得分:1)
如何解决此问题:
创建每个线程,使其在允许开始用户工作功能之前等待哨兵(你需要一个调用它的lambda) 如果任何线程无法启动,请设置一个标志以指示现有线程应立即完成而不是执行用户的功能。 在错误情况下,加入已启动的线程。然后根据需要退出错误代码或异常(异常更好)。
现在你的函数是线程安全的,不会泄漏内存。
编辑:这里有一些代码可以满足您的需求,包括测试。
如果要强制模拟线程失败,请使用定义为INTRODUCE_FAILURE
的{{1}}重新编译
1
成功输出示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <memory>
#include <atomic>
#include <system_error>
#include <condition_variable>
#define INTRODUCE_FAILURE 0
// implementation
void ParallelCall(void (*function)(int, int), int numThreads)
{
std::vector<std::thread> threads;
threads.reserve(numThreads-1);
std::atomic<bool> mustAbort ( false );
std::atomic<bool> mayRun ( false );
std::mutex conditionMutex;
std::condition_variable runCondition;
for(int i = 1; i < numThreads; ++ i) {
try {
#if INTRODUCE_FAILURE == 1
if (i == 3) {
throw std::system_error(99, std::generic_category(), "the test deliberately failed a thread");
}
#endif
threads.emplace_back( std::thread{ [i, numThreads, function
, &mustAbort
, &conditionMutex
, &runCondition
, &mayRun]()->int {
std::unique_lock<std::mutex> myLock(conditionMutex);
runCondition.wait(myLock, [&mayRun]()->bool {
return mayRun;
});
myLock.unlock();
// wait for permission
if (!mustAbort) {
function(i, numThreads);
}
return 0;
}} );
}
catch(std::exception& e) { // will be a std::system_error
mustAbort = true;
std::unique_lock<std::mutex> myLock(conditionMutex);
mayRun = true;
conditionMutex.unlock();
runCondition.notify_all();
for(auto& t : threads) {
t.join();
}
throw;
}
}
std::unique_lock<std::mutex> myLock(conditionMutex);
mayRun = true;
conditionMutex.unlock();
runCondition.notify_all();
function(0, numThreads);
// use the calling thread as thread 0
for(auto& t : threads) {
t.join();
}
}
// test
using namespace std;
void testFunc(int index, int extent) {
static std::mutex outputMutex;
unique_lock<mutex> myLock(outputMutex);
cout << "Executing " << index << " of " << extent << endl;
myLock.unlock();
this_thread::sleep_for( chrono::milliseconds(2000) );
myLock.lock();
cout << "Finishing " << index << " of " << extent << endl;
myLock.unlock();
}
int main()
{
try {
cout << "initiating parallel call" << endl;
ParallelCall(testFunc, 10);
cout << "parallel call complete" << endl;
}
catch(std::exception& e) {
cout << "Parallel call failed because: " << e.what() << endl;
}
return 0;
}
失败时的输出示例:
Compiling the source code....
$g++ -std=c++11 main.cpp -o demo -lm -pthread -lgmpxx -lgmp -lreadline 2>&1
Executing the program....
$demo
initiating parallel call
Executing 0 of 10
Executing 1 of 10
Executing 4 of 10
Executing 5 of 10
Executing 8 of 10
Executing 2 of 10
Executing 7 of 10
Executing 6 of 10
Executing 9 of 10
Executing 3 of 10
Finishing 1 of 10
Finishing 5 of 10
Finishing 2 of 10
Finishing 9 of 10
Finishing 8 of 10
Finishing 4 of 10
Finishing 3 of 10
Finishing 0 of 10
Finishing 6 of 10
Finishing 7 of 10
parallel call complete
最后请求 - 不要在世界上释放你的图书馆。 std :: thread库非常全面,如果还不够,我们有OpenMP,TBB等等。
答案 1 :(得分:1)
让它们创建的线程在退出threadproc之前完成丢失的工作会怎样?
List _StillBornWork;
void ParallelCall(void (*function)(int, int), int numThreads)
{
Thread *threads = new Thread[numThreads - 1];
for(int i = 1; i < numThreads; ++ i) {
if(threads[i - 1].start(&function, i, numThreads)) {
_StillBornWork.Push(i);
}
}
(*function)(0, numThreads);
// use the calling thread as thread 0
for(int i = 1; i < numThreads; ++ i)
threads[i - 1].join();
delete[] threads;
}
ThreadProc(int i) {
while(1) {
do work
// Here we see if there was any work that didn't get done because its thread
// was stilborn. In your case, the work is indicated by the integer i.
// If we get work, loop again, else break.
if (!_StillBornWork.Pop(&i))
break; // no more work that wasn't done.
}
}