简单的多线程帮助? C++、WaitForSingleObject 和同步
Posted
技术标签:
【中文标题】简单的多线程帮助? C++、WaitForSingleObject 和同步【英文标题】:Simple Multi-threading help? C++, WaitForSingleObject and Synchronization 【发布时间】:2012-04-15 17:42:23 【问题描述】:因此,我正在尝试制作一个简单的多线程程序,该程序可以验证 Collatz 猜想的大量数字并返回已验证数字的总数。每个线程(总共 4 个)执行一个数字间隔,并在数字达到 1 时更新“已验证”变量。我也在计时整个过程(与单线程计算进行比较)
我遇到的问题是,当我在程序结束时打印出“已验证”的 int 时,从来没有任何一致性,所以我猜测要么是线程相互覆盖,要么是主线程在其他人之前完成,因此打印一个不完整的数字。我还假设如果主线程在其他线程之前完成,clock() 计算也将关闭。那么,如何停止主线程继续运行,直到其他线程完成(从而使其等待更新的验证计数并完成准确的时间测量)?这就是我认为 WaitForSingleObject 所做的,但我猜它只是停止主线程退出,仍然允许它计算其他函数。
这是我第一次接触多线程,我认为我不太了解同步和 WaitForSingleObject 命令的工作原理。到目前为止,我的主要功能如下:
编辑:这是我更新的 Main 函数和 Collatz 函数。我对其进行了修改,以便每个线程都访问一个单独的变量以避免同步问题,但问题仍然存在。当我打印出“已验证”时,没有一致的值 *
再次编辑:好的,所以我删除了每个 Mladen Janković 的“线程”int,并仅使用一个简单的计数器将不同的间隔分配给创建的线程。 “已验证”现在有一个一致、正确的值。但是,当有 1,000,000 个数字时,我仍然无法让程序真正完成。测试它 100,000 甚至 10,000 可以完美地工作,但是当我将它增加到 1,000,000 个数字时,程序会无限期地运行(小时)而没有实际返回值。我猜它会卡在一个特定的值上(例如 Martin James 指出的 750831)。我尝试用 int 代替 long int,但似乎仍然存在溢出问题。有什么建议么?感谢您的大力帮助。
最后编辑:好的,所以我只是使用 long long 而不是 int,现在程序可以完美运行。感谢大家的帮助!
void main()
clock_t start;
clock_t finish;
unsigned int thread = 0;
start = clock();
HANDLE h1 = (HANDLE)_beginthreadex(NULL, 0, collatz_thread, NULL, 0, NULL);
HANDLE h2 = (HANDLE)_beginthreadex(NULL, 0, collatz_thread, NULL, 0, NULL);
HANDLE h3 = (HANDLE)_beginthreadex(NULL, 0, collatz_thread, NULL, 0, NULL);
for (int i = 750001 ; i <= 1000000 ; i++) collatz(i, 4);
WaitForSingleObject( h1, INFINITE );
WaitForSingleObject( h2, INFINITE );
WaitForSingleObject( h3, INFINITE );
finish = clock() - start;
double time = finish / (double) CLOCKS_PER_SEC;
validated = v1 + v2 + v3 + v4;
cout << validated << " numbers validated." << endl;
cout << endl << time << " seconds." << endl;
unsigned _stdcall collatz_thread (void* n)
selection++; // selects a different interval each time collatz_thread is called
switch (selection)
case 1:
for (int i = 1 ; i <= 250000; i++) collatz(i, 1);
break;
case 2:
for (int i = 250001 ; i <= 500000; i++) collatz(i, 2);
break;
case 3:
for (int i = 500001 ; i <= 750000; i++) collatz(i, 3);
break;
return 0;
int collatz (int n, int thread)
int original = n;
while (n != 1)
if (n%2 == 0)
n = (n/2);
else
n = (3*n + 1);
if (n == 1)
switch (thread)
case 1:
v1++;
break;
case 2:
v2++;
break;
case 3:
v3++;
break;
case 4:
v4++;
break;
return n;
【问题讨论】:
查看我的更新答案,并提供 collatz_thread 函数的代码。 好的,给你。我还更新了帖子,提供了有关我当前问题的更多信息。 【参考方案1】:如果是共享变量,您需要同步对validated
的访问。最简单的方法是在你想增加它时使用InterlockedIncrement
函数而不是标准的++
运算符。另一种方法是在访问共享变量时使用某种同步对象,如自旋锁或互斥锁,但如果您只需要同步增量操作,那就大材小用了。
如需了解更多详情,请提供collatz
函数的代码。
正如 'usr' 所建议的,为了获得更好的性能,您可以在每个线程中使用单独的变量,然后在主线程中将它们相加。在这种情况下,您应该以这样的方式填充这些变量,这样它们就不会共享相同的缓存行以避免false sharing。
您没有提供collatz_thread
函数,这可能是导致结果不一致的另一个原因。原因是您将 pointer 传递给变量 (&thread
),该变量存储线程 # 在创建新线程的调用之间发生变化,因此根据操作系统调度程序的状态,新线程可能无法获得有机会在 thread
变量已更改为具有另一个值时开始,因此您将有多个线程执行同一组数据,而其他组可能会被跳过。由于行为取决于线程调度程序的当前状态,因此几乎无法预测。
解决方案是将thread
变量转换为void*
,而不是传递其地址,然后在collatz_thread
函数中将其转换回int
:
HANDLE h1 = (HANDLE)_beginthreadex(NULL, 0, collatz_thread, (void*)thread, 0, NULL);
正如 Martin 所建议的,您可能有整数溢出,但它不应该导致不一致的结果,只是错误的结果,但仍然是一致的。
【讨论】:
是的。如果您使用 InterlockedIncrement 虽然您不会获得高吞吐量。每个线程最好使用一个变量。 @usr: 对于这个简单的案例来说,这是更好的解决方案。 好的,谢谢你的建议。我是否也应该单独为每个线程计时(使用 clock() 函数),还是我有足够的时间来获得整个过程的准确时间? 是我还是那个 collatz 算法不稳定?如果我用 750831 调用它,那么它会遇到整数溢出。 我在我的 64 位机器上使用 Visual C++ 进行了尝试。我得到了 7993360 的一致验证。有人试过这个或知道预期值吗?我的 i7 上有 4 个线程,我得到 345 毫秒。使用 8,我得到 333 毫秒,这很奇怪,因为池中只有 4 个“x 到 x+250000”任务。一个线程需要 1014ms :(【参考方案2】:试着看看这个:
Semaphore and threads explenation from MSDN
这可能是您在网上找到的最好的文档。
现在,关于您的代码,我认为它运行得不是很好,这就是原因: WaitForSingleObject - 基本上意味着您尝试在 h1 信号量(或 h2 或 h3)上执行 -1,如果您无法做到 -1(即信号量为 0),则等待无限时间。 WaitForSimgleObject 实际上应该在您的线程例程中调用,而不是在您的主程序中。
另外,在你的线程对象中,一旦你完成了对共享变量的处理,你需要释放信号量,以便其他线程可以获得对该特定信号量的锁定。
试着看看我给你的链接上的例子,我相信你会让它工作得很快。
【讨论】:
【参考方案3】:我尝试了一下,得到了一些很好的结果,太好了 :(( 某处出了点问题,但我没有看到它。我没有得到几个小时的运行时间,即使是 n从 1 到 10,000,000,(千万):
8 tests,
8 tasks,
counting to 1000000,
using 14 threads:
Validated: 1000000 in 670ms
Validated: 1000000 in 671ms
Validated: 1000000 in 624ms
Validated: 1000000 in 656ms
Validated: 1000000 in 655ms
Validated: 1000000 in 655ms
Validated: 1000000 in 640ms
Validated: 1000000 in 686ms
Average time: 657ms
Total validated: 8000000
8 tests,
8 tasks,
counting to 10000000,
using 14 threads:
Validated: 10000000 in 8081ms
Validated: 10000000 in 7441ms
Validated: 10000000 in 7784ms
Validated: 10000000 in 7598ms
Validated: 10000000 in 7956ms
Validated: 10000000 in 7534ms
Validated: 10000000 in 7816ms
Validated: 10000000 in 7769ms
Average time: 7747ms
Total validated: 80000000
请注意,虽然它说的是 14 个线程,但这是针对整个池的。一个线程总是在等待其他任务完成时用完,因此实际上只有 13 个线程可用于运行验证。出于某种原因,这是最佳选择。
好的,我的 i7 在所有 4/8 内核上都完全正常,但我看不到运行时间缩短到几秒钟,因为我有更多内核并将工作分摊给所有内核 :(
这是我用的。这与您的操作方式有些不同,因为我有大部分工具/代码。首先是 Visual C++。有两个班。每次运行都由一个“PoolTest”类管理,该类将几个“TestTask”实例提交给一个线程池,一个用于要验证的完整整数范围的每一段。您会注意到您的代码已复制/粘贴到 TestTask 类中。我强调了 TestTask 在 PoolTest 代码中的组装位置。 'PoolTest' 类然后等待所有 'TestTask' 实例完成的事件 - 它可以这样做,因为 TestTask 在完成时回调 PoolTest 'taskComplete' 方法。此方法访问一个线程安全计数器,该计数器在提交 TestTask 时进行计数,并由“taskComplete”方法进行计数。
我重复使用的这段代码有点复杂,因为它可以多次重复运行以获得平均时间,因此可以多次发出完整的 TestTasks 集。
当最后一个 TestTask 倒计时到零时,它会发出 Event 信号,然后 PoolTest 将再次运行,向 GUI 发出整个测试运行完成的信号(不必费心列出 GUI 的东西因为不相关)。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace WindowsFormsApplication1
public class TestTask: Task
public int validated;
public int fromVal, toVal;
public int ticks;
long collatz(long n)
while (n != 1)
if (n % 2 == 0)
n = (n / 2);
else
n = (3 * n + 1);
return (n);
public override void run()
int index;
int localTo = toVal;
int localFrom = fromVal;
int localVal = 0;
for (index = localFrom; index < localTo; index++)
// if not disproved, inc the stack 'validated'
if (1 == collatz(index + 1)) localVal++;
validated = localVal; // put stack result into instance field,
public TestTask(int paramx, EventHandler OnDone)
: base(paramx, OnDone)
;
/* a task that submits testTask instances.
*/
public class PoolTest:Task
int FnumTasks;
int FnumTests;
int Fcount;
int FtestCount;
int taskCount;
int startTicks;
int endTicks;
int totalTicks;
EventHandler FonTaskComplete;
AutoResetEvent testCompleteEvent;
public int average;
public int testTicks;
public int Vone;
public int Vtot;
public TestTask thisTestTask;
public PoolTest(int testsNum, int tasks, int count, EventHandler taskDone,
EventHandler testDone)
: base(0, testDone)
FnumTests=testsNum;
FtestCount=testsNum;
FnumTasks=tasks;
Fcount=count;
Vtot = 0;
Vone = 0;
totalTicks = 0;
FonTaskComplete=taskDone; // call after each test to report ticks
testCompleteEvent= new AutoResetEvent(false);
void submitAtest() // queue up numTasks testTask instances
taskCount=FnumTasks;
startTicks = System.Environment.TickCount;
//*********************THIS IS WHERE THE RANGES AND TASKS ARE ASSEMBLED
int startNum = 0; // here, start at 0 and build up the ranges
int countIncrement=Fcount/FnumTasks; // calc. range size
int endNum=startNum+countIncrement; // and so init. start/end
TestTask newTask;
for (int i = 1; i < FnumTasks; i++) // one less than requested
newTask=new TestTask(0, taskComplete);
newTask.fromVal=startNum; // load in the start/end for the loop
newTask.toVal = endNum;
myPool.submit(newTask); // off it goes, see you later
startNum = endNum; // now move range up for
endNum += countIncrement; // next TestTask
// treat last range separately to cover div rounding when
// calculating 'countIncrement'
newTask = new TestTask(0, taskComplete); // do last one
newTask.fromVal = startNum;
newTask.toVal = Fcount;
myPool.submit(newTask); // send off the last one
//*****************************************************************
public override void run()
submitAtest(); //start off the first run of testTasks
testCompleteEvent.WaitOne();
void taskComplete(object sender, EventArgs e) // called when a testTask
bool finishedTasks; // instance is complete
lock (this)
thisTestTask = (TestTask)sender;
taskCount--; // another one down
Vone += thisTestTask.validated; // Vone - total for one run
Vtot += thisTestTask.validated; // total for all runs
finishedTasks = (taskCount == 0); // this run all done yet?
if (finishedTasks)
endTicks = System.Environment.TickCount; // yes, so calc. elapsed time
testTicks=endTicks-startTicks;
thisTestTask.ticks = testTicks;
totalTicks=totalTicks+testTicks;
if (0!=--FtestCount) // done all the test runs?
thisTestTask.validated = Vone; // use this field to return run total
FonTaskComplete(thisTestTask, null); // and signal result of test
Vone = 0;
submitAtest(); // no, so send off another load
else
average=totalTicks/FnumTests; // done all test runs!
testCompleteEvent.Set(); // signal all runs completed
;
;
【讨论】:
我认为我的问题确实是您之前提到的整数溢出。我隔离了 750831 的情况并单独对其进行了测试,结果表明它确实遭受了整数溢出的影响,使得循环运行的时间(小时)比它应该运行的时间长得多(但不知何故,它仍然以 1 的方式运行?)。在我将 int n 设置为 long long 之后,程序在我的 Core i5 上使用 4 个线程在 1314 毫秒内运行并验证了所有 1,000,000 个数字。对于单线程,程序耗时 3359ms。所以,你的结果可能是正确的,而我的结果是不正常的。以上是关于简单的多线程帮助? C++、WaitForSingleObject 和同步的主要内容,如果未能解决你的问题,请参考以下文章