You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WinAPI多线程计算π异常问题排查及正确实现咨询

问题描述

需要用WinAPI实现多线程计算π,采用数值积分公式,i的取值范围为0到N=10^8。要求每个线程每次仅处理固定40项计算,通过任务池模式为完成任务的线程分配新任务,线程在获取新任务前处于等待状态。

自行编写了ThreadManager类实现逻辑:为每个线程分配自动重置事件(auto-reset event),线程完成计算后触发事件;主线程等待事件信号,更新共享ThreadStruct的任务参数后再次触发事件,让线程执行新任务。但存在以下问题:

  • 单线程运行时,结果与π相差极大(如0.0001776328265)
  • 将N缩小到1000时,计算次数远超预期的25次
  • 添加mutex同步后,计算速度变慢且结果仍随机错误(如16线程时得到50.26492171)

咨询:问题根源是什么?若实现思路完全错误,该如何用WinAPI组织多线程计算?使用类实现是否不合适?

以下是完整实现代码与头文件:

实现代码

#include "ThreadManager.h"
#include <string>

HANDLE ThreadManager::mutex = (CreateMutexA(nullptr, true, "m"));

ThreadManager::ThreadManager(size_t threadCount)
{
    threads.reserve(threadCount);
    for (int i = 0; i < threadCount; i++)
    {
        threadInfo.push_back(new ThreadStruct(i * OP_COUNT));
        HANDLE event = CreateEventA(nullptr, false, true, std::to_string(i).c_str());

        if (event)
        {
            threadEvents.push_back(event);
            DuplicateHandle(GetCurrentProcess(), event, GetCurrentProcess(),
                            &(threadInfo[i]->threadEvent), 0, false, DUPLICATE_SAME_ACCESS);
        }
        else std::cout << "Unknown error: " << GetLastError() << std::endl;
        HANDLE thread = CreateThread(nullptr, 0,
                                     reinterpret_cast<LPTHREAD_START_ROUTINE>(&ThreadManager::threadFunc),
                                     threadInfo[i],
                                     CREATE_SUSPENDED, nullptr);
        if (thread) threads.push_back(thread);
        else std::cout << "Unknown error: " << GetLastError() << std::endl;
    }
}

double ThreadManager::run()
{
    size_t operations_done = threads.size() * OP_COUNT;
    for (HANDLE t : threads) ResumeThread(t);
    DWORD index;
    Sleep(10);
    while (operations_done < ThreadManager::N)
    {
        ReleaseMutex(ThreadManager::mutex);
        index = WaitForMultipleObjects(this->threadEvents.size(), this->threadEvents.data(), false, 10000);
        WaitForSingleObject(ThreadManager::mutex, 1000);
        threadInfo[index] -> operationIndex = operations_done + OP_COUNT;
        SetEvent(threadEvents[index]);
        //std::cout << "Operations completed: " << operations_done << "/1000" << std::endl;
        operations_done += OP_COUNT;
    }
    long double res_pi = 0;
    for (auto&& ts: this->threadInfo)
    {
        res_pi += ts->pi;
        ts->operationIndex = N;
    }
    res_pi /= N;
    WaitForMultipleObjects(this->threads.size(), this->threads.data(), true, 10000);
    std::cout.precision(10);
    std::cout << "Pi value for " << threads.size() << " threads: " << res_pi;
    threads.clear();
    return 0;
}

ThreadManager::~ThreadManager()
{
    if (!threads.empty())
        for (HANDLE t: threads)
        {
            TerminateThread(t, -1);
            CloseHandle(t);
        }
    std::destroy(threadInfo.begin(), threadInfo.end());
}

long double ThreadManager::calc(size_t startIndex)
{
    long double xi = 0;
    long double pi = 0;
    for (size_t i = startIndex; i < startIndex + OP_COUNT; i++)
    {
        const long double ld_i = i;
        const long double half = 0.5f;
        xi = (ld_i + half) * (1.0 / N);
        pi += ((4.0 / (1.0 + xi * xi)));
    }
    return pi;
}

DWORD WINAPI ThreadManager::threadFunc(ThreadStruct *ts)
{
    while (ts->operationIndex < N)
    {
        WaitForSingleObject(ts->threadEvent, 1000);
        ts->pi += calc(ts->operationIndex);
        WaitForSingleObject(ThreadManager::mutex, 1000);
        SetEvent(ts->threadEvent);
        ReleaseMutex(ThreadManager::mutex);
    }
    return 0;
}

ThreadStruct::ThreadStruct(size_t opIndex)
{
    this -> pi = 0;
    this -> operationIndex = opIndex;
}

头文件代码

#ifndef MULTITHREADPI_THREADMANAGER_H
#define MULTITHREADPI_THREADMANAGER_H
#include <iostream>
#include <vector>
#include <list>
#include <windows.h>
#include <memory>

struct ThreadStruct
{
    size_t operationIndex;
    long double pi;
    HANDLE threadEvent = nullptr;
    explicit ThreadStruct(size_t opIndex);
};


class ThreadManager
{
public:
    explicit ThreadManager(size_t threadCount);
    double run();
    ~ThreadManager();

private:
    std::vector<ThreadStruct*> threadInfo;
    std::vector<HANDLE> threads;
    std::vector<HANDLE> threadEvents;
    static HANDLE mutex;
    static long double calc(size_t startIndex);
    static const int OP_COUNT = 40;
    static const int N = 100000000;
    static DWORD WINAPI threadFunc(ThreadStruct* ts);
};


#endif //MULTITHREADPI_THREADMANAGER_H

问题根源分析

1. 数值计算逻辑错误

积分公式计算π的正确形式是:$\pi = \int_0^1 \frac{4}{1+x^2} dx$,用矩形法近似时,每个区间的面积是$\frac{1}{N} \times \frac{4}{1+x_i^2}$,其中$x_i = (i + 0.5)/N$。你最终将累加结果除以N,属于重复缩放——calc函数里的每个项已经是$\frac{4}{1+x_i^2}$,正确做法是将累加结果乘以$\frac{1}{N}$,而非除以N。这是单线程结果严重偏离π的核心原因。

2. 任务分配与同步逻辑混乱

  • 初始任务分配错误:构造函数中给线程分配的初始任务是i * OP_COUNT,当线程数×OP_COUNT超过N时会直接越界,导致无效计算。
  • 事件同步逻辑错误:自动重置事件在WaitForSingleObject后会自动复位,但主线程和工作线程的触发/等待逻辑完全混乱:
    • 工作线程完成计算后调用SetEvent,主线程等待事件后又立刻SetEvent,导致工作线程可能重复执行同一个任务,这就是N=1000时计算次数远超25次的原因。
    • 主线程在循环中先ReleaseMutex再等待事件,导致多个线程同时修改共享数据,引发竞态条件,结果随机错误。
  • Mutex初始化错误:CreateMutexA的第二个参数设为true,表示主线程一开始就占有互斥体,但构造函数完成后未释放,导致工作线程一开始就被阻塞,直到主线程在run函数里第一次调用ReleaseMutex才得以继续,严重影响并发效率。

3. 线程终止逻辑错误

析构函数中使用TerminateThread强制终止线程,会导致线程资源泄漏,且可能丢失未完成的计算结果。正确做法是让线程自然退出,而非强制终止。


修正方案

1. 修复数值计算逻辑

将run函数中的res_pi /= N改为res_pi *= (1.0L / N),确保积分缩放正确。

2. 重构任务池与同步逻辑

放弃每个线程一个事件的复杂模式,改用全局原子任务计数器+互斥体的简单方案,让线程主动获取任务:

  • 用原子计数器记录当前待处理的任务起始索引,每个线程获取一段任务(40项)后更新计数器,直到计数器超过N。
  • 用互斥体保护计数器的读写,避免竞态条件。
  • 工作线程逻辑简化为:循环获取任务段→计算→累加结果→直到任务耗尽。

3. 修正同步与线程管理

  • Mutex初始化时第二个参数设为false,让互斥体处于未占有状态。
  • 移除不必要的DuplicateHandle操作,直接将事件句柄传给线程结构体即可。
  • 析构函数中等待所有线程自然退出后再关闭句柄,禁止使用TerminateThread。

修正后的核心代码示例

// ThreadManager类的静态成员,替代全局变量
volatile LONG ThreadManager::g_nextTaskIndex = 0;
HANDLE ThreadManager::g_taskMutex = CreateMutexA(nullptr, false, nullptr);
long double ThreadManager::g_totalPi = 0;
HANDLE ThreadManager::g_totalMutex = CreateMutexA(nullptr, false, nullptr);

DWORD WINAPI ThreadManager::threadFunc(LPVOID param) {
    long double localPi = 0;
    while (true) {
        // 获取任务段
        WaitForSingleObject(g_taskMutex, INFINITE);
        size_t startIndex = static_cast<size_t>(InterlockedAdd(&g_nextTaskIndex, OP_COUNT));
        ReleaseMutex(g_taskMutex);
        
        if (startIndex >= N) break;
        
        // 计算当前任务段(避免越界)
        size_t endIndex = min(startIndex + OP_COUNT, N);
        long double xi = 0;
        for (size_t i = startIndex; i < endIndex; ++i) {
            xi = (i + 0.5L) * (1.0L / N);
            localPi += 4.0L / (1.0L + xi * xi);
        }
    }
    // 将局部结果累加至全局
    WaitForSingleObject(g_totalMutex, INFINITE);
    g_totalPi += localPi;
    ReleaseMutex(g_totalMutex);
    return 0;
}

// ThreadManager的run函数简化为:
double ThreadManager::run() {
    g_nextTaskIndex = 0;
    g_totalPi = 0;
    for (HANDLE t : threads) ResumeThread(t);
    WaitForMultipleObjects(threads.size(), threads.data(), TRUE, INFINITE);
    long double resPi = g_totalPi * (1.0L / N);
    std::cout.precision(10);
    std::cout << "Pi value for " << threads.size() << " threads: " << resPi << std::endl;
    return resPi;
}

关于类实现的适用性

用类实现完全合适,只需将原全局变量改为类的静态成员,封装线程管理、同步对象和计算逻辑,提高代码的可维护性和封装性。


总结

核心问题是数值计算的缩放错误和同步逻辑的混乱,修正后即可得到正确结果。任务池模式的正确实现应该是让线程主动获取任务,而非主线程逐个分配,这样更高效也更易维护。

内容的提问来源于stack exchange,提问作者GaussGun

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 15:30:59