简单的多线程帮助? C++、WaitForSingleObject 和同步

Posted

技术标签:

【中文标题】简单的多线程帮助? C++、WaitForSingleObject 和同步【英文标题】:Simple Multi-threading help? C++, WaitForSingleObject and Synchronization 【发布时间】:2012-04-15 17:42:23 【问题描述】:

因此,我正在尝试制作一个简单的多线程程序,该程序可以验证 Collat​​z 猜想的大量数字并返回已验证数字的总数。每个线程(总共 4 个)执行一个数字间隔,并在数字达到 1 时更新“已验证”变量。我也在计时整个过程(与单线程计算进行比较)

我遇到的问题是,当我在程序结束时打印出“已验证”的 int 时,从来没有任何一致性,所以我猜测要么是线程相互覆盖,要么是主线程在其他人之前完成,因此打印一个不完整的数字。我还假设如果主线程在其他线程之前完成,clock() 计算也将关闭。那么,如何停止主线程继续运行,直到其他线程完成(从而使其等待更新的验证计数并完成准确的时间测量)?这就是我认为 WaitForSingleObject 所做的,但我猜它只是停止主线程退出,仍然允许它计算其他函数。

这是我第一次接触多线程,我认为我不太了解同步和 WaitForSingleObject 命令的工作原理。到目前为止,我的主要功能如下:

编辑:这是我更新的 Main 函数和 Collat​​z 函数。我对其进行了修改,以便每个线程都访问一个单独的变量以避免同步问题,但问题仍然存在。当我打印出“已验证”时,没有一致的值 *

再次编辑:好的,所以我删除了每个 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;

【问题讨论】:

查看我的更新答案,并提供 collat​​z_thread 函数的代码。 好的,给你。我还更新了帖子,提供了有关我当前问题的更多信息。 【参考方案1】:

如果是共享变量,您需要同步对validated 的访问。最简单的方法是在你想增加它时使用InterlockedIncrement 函数而不是标准的++ 运算符。另一种方法是在访问共享变量时使用某种同步对象,如自旋锁或互斥锁,但如果您只需要同步增量操作,那就大材小用了。

如需了解更多详情,请提供collatz函数的代码。

正如 'usr' 所建议的,为了获得更好的性能,您可以在每个线程中使用单独的变量,然后在主线程中将它们相加。在这种情况下,您应该以这样的方式填充这些变量,这样它们就不会共享相同的缓存行以避免false sharing。

您没有提供collatz_thread 函数,这可能是导致结果不一致的另一个原因。原因是您将 pointer 传递给变量 (&amp;thread),该变量存储线程 # 在创建新线程的调用之间发生变化,因此根据操作系统调度程序的状态,新线程可能无法获得有机会在 thread 变量已更改为具有另一个值时开始,因此您将有多个线程执行同一组数据,而其他组可能会被跳过。由于行为取决于线程调度程序的当前状态,因此几乎无法预测。

解决方案是将thread 变量转换为void*,而不是传递其地址,然后在collatz_thread 函数中将其转换回int

HANDLE h1 = (HANDLE)_beginthreadex(NULL, 0, collatz_thread, (void*)thread, 0, NULL);

正如 Martin 所建议的,您可能有整数溢出,但它不应该导致不一致的结果,只是错误的结果,但仍然是一致的。

【讨论】:

是的。如果您使用 InterlockedIncrement 虽然您不会获得高吞吐量。每个线程最好使用一个变量。 @usr: 对于这个简单的案例来说,这是更好的解决方案。 好的,谢谢你的建议。我是否也应该单独为每个线程计时(使用 clock() 函数),还是我有足够的时间来获得整个过程的准确时间? 是我还是那个 collat​​z 算法不稳定?如果我用 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 和同步的主要内容,如果未能解决你的问题,请参考以下文章

c++ windows中客户端服务器编程中的多线程

C++多线程

纯 C++ 中的多线程?

为啥 C++ 中的多线程会降低性能

c++ 中的多线程线程安全动画建议

Linux C++的多线程编程