对于一个程序,Boost :: mutex比没有mutex花费的时间更少

时间:2017-04-03 10:52:29

标签: c++ linux boost mutex

我已执行下面的程序,我创建了100个并发执行的线程。 请注意,这是一个示例程序。我知道下面的程序不需要多个线程,但我的目的是测试互斥锁。

class ThreadPool{

    public:
    ThreadPool(int num = 10);
    ~ThreadPool();
    void AssignPool();
    void doSometask();
    void inc();
    private:
    boost::asio::io_service ioService;
    boost::thread_group threadpool;
    boost::asio::io_service::work * work;
   volatile int p_size;
    int pool_sz;
    boost::mutex io_mutex;// with boost lock

};

void ThreadPool::AssignPool()
{
        std::cout<<std::endl<<"pool_sz="<<pool_sz<<std::endl;
        for(int i=0;i<pool_sz;i++)
        {
                ioService.post(boost::bind(&ThreadPool::doSometask, this));
        }
}

void ThreadPool::inc()
{
        p_size++;
}

void ThreadPool::doSometask()
{

//      boost::mutex::scoped_lock lock(io_mutex);
        for(int i=0;i<10000;i++){
                inc();
        }


}

ThreadPool::ThreadPool(int num):p_size(0)
{
        pool_sz = num;
        work = new  boost::asio::io_service::work(ioService);
        for(int i =0;i<num;i++)
        {
                threadpool.create_thread(boost::bind(&boost::asio::io_service::run, &ioService  ))    ;
        }

}

ThreadPool::~ThreadPool()
{
        delete work;
        ioService.stop();
        threadpool.join_all();
}

int main()
{
        ThreadPool p1(100);
        p1.AssignPool();
}

案例1: 上面的程序是通过注释&#34; boost :: mutex :: scoped_lock lock(io_mutex);&#34;来执行的。这是&#34;没有互斥的情况&#34;。 该计划所花费的时间是

real    0m1.386s
user    0m0.483s
sys 0m9.937s

案例2:使用Mutex: 但是,当我使用互斥锁运行此程序时,即&#34; boost :: mutex :: scoped_lock lock(io_mutex);&#34;线。这项计划的时间较短。

real    0m0.289s
user    0m0.067s
sys 0m0.230s

在我对互斥锁的理解中,程序应该比没有互斥锁花费更多的时间。这里出了什么问题??

3 个答案:

答案 0 :(得分:5)

在您的示例中,您将互斥锁锁定在doSometask()中,因此始终只有一个线程将运行,并且它将在屈服于另一个任务之前完成for循环。因此,程序按字面顺序运行,不会发生缓存脱粒。

如果没有锁定,所有线程将在获得处理器时间时运行,并且假设处理器数量明显低于100,则所有级别上都会进行大量缓存脱粒(如Bo Persson在评论中所写) ,这会增加运行时间。

衡量锁定对运行时间影响的更好方法是:(a)仅运行与计算机具有核心一样多的线程,以便最小化因上下文切换而导致的缓存脱粒,以及(b),将锁定放入ThreadPool::inc()方法,以便更频繁地进行同步。

作为奖励,您可以通过将p_size声明为std::atomic<int>(C ++ 11)来正确运行无锁方法,并查看基于互斥锁的同步对原子的使用的影响。

答案 1 :(得分:2)

作为评论,&#34;一次一个有序的方式&#34;比起冲进去的每个人都做得更好,但不仅如此。最有可能的主要原因是,您为每个线程在doSometask()内工作的时间对于现代CPU来说太少了。 更新您的doSometask以执行更多工作,并通过不断访问共享数据使您的线程更少依赖于相互冲突:

#include <iostream>
#include <chrono>
#include <atomic>
#include <boost/asio/io_service.hpp>
#include <boost/thread.hpp>


class ThreadPool
{
public:
    ThreadPool(int num = 10, int cycles = 10000);
    ~ThreadPool();

    void inc(volatile int* x);

    void AssignPool();
    void doSometask(volatile int* x);
    void AssignPoolSync();
    void doSometaskSync(volatile int* x);

private:
    boost::asio::io_service ioService;
    boost::thread_group threadpool;
    boost::asio::io_service::work * work;
    std::atomic<int> p_size;

    int *xsize;
    int pool_sz, cycles;
    boost::mutex io_mutex; // with boost lock
};

void ThreadPool::AssignPool()
{
    for (int i = 0; i<pool_sz; ++i)
        ioService.post(boost::bind(&ThreadPool::doSometask, this, &xsize[i]));
}

void ThreadPool::AssignPoolSync()
{
    for (int i=0; i<pool_sz; ++i)
        ioService.post(boost::bind(&ThreadPool::doSometaskSync, this, &xsize[i]));
}

void ThreadPool::inc(volatile int* x)
{
    *x = *x + 1;
}

void ThreadPool::doSometask(volatile int* x)
{
    for (int i=0; i<cycles; ++i)
    {
        inc(x);
        if (i & 255 == 0)
            p_size++; // access shared data evert 256 cycles
    }
}

void ThreadPool::doSometaskSync(volatile int* x)
{
    boost::mutex::scoped_lock lock(io_mutex);
    doSometask(x);
}

ThreadPool::ThreadPool(int num, int cycles)
{
    pool_sz = num;
    p_size = 0;
    this->cycles = cycles;
    xsize = new int[num];
    memset(xsize, 0, num * sizeof(int));
    work = new  boost::asio::io_service::work(ioService);
    for (int i=0; i<pool_sz; ++i)
        threadpool.create_thread(boost::bind(&boost::asio::io_service::run, &ioService));
}

ThreadPool::~ThreadPool()
{
    delete work;
    ioService.stop();
    threadpool.join_all();
    delete[] xsize;
}

int main(int argc, const char** argv)
{
    const int C = argc>1 ? std::stoi(argv[1]) : 10000; // number of cycles
    const int T = argc>2 ? std::stoi(argv[2]) : 100; // number of threads
    const int N = argc>3 ? std::stoi(argv[3]) : 50; // number of times to time execution

    long long t_min[2] = {0};
    for (int i = 0; i<N*2; ++i)
    {
        auto t0 = std::chrono::high_resolution_clock::now();
        {
            Sleep(1);
            ThreadPool pool(T, C);
            if (i&1)
                pool.AssignPoolSync();
            else
                pool.AssignPool();
        }
        auto t1 = std::chrono::high_resolution_clock::now();
        t_min[i&1] = std::min(i>1 ? t_min[i&1] : (t1-t0).count(), (t1-t0).count());
    }
    printf("timeSync / time: %f\n", (t_min[1] + 0.0) / (t_min[0] + 0.0));
}

使用此测试,您可以模拟更好的实际工作:线程运行的作业大多是独立的,有时他们访问共享数据。您还可以使用不同的参数运行它,以更改每个线程运行的循环周期数和线程数。

这些是我在4核心PC上运行时得到的样本结果:

test> test.exe 10000 100
timeSync / time: 1.027782

test> test.exe 500000 100
timeSync / time: 3.531433

换句话说,当每个线程只执行10000个周期时,同步版本几乎与非同步一样快,但是我将周期数增加到500000然后同步版本慢了3.5倍

答案 2 :(得分:1)

我既不是计算机科学家,也不是OS专家。 但是每当我试图比较两个相似函数的性能时,不是比较单次执行中的时间,而是多次运行函数并比较平均值(我的这个方法我错了,它对我来说大部分时间都适用。我是对此专家的意见和评论开放。我的想法是,当我使用操作系统时,资源(主要是处理器)没有完全分配给观察下的应用程序。它们同时被许多其他进程共享。

我尝试对您的应用程序执行相同操作,并获得以下结果,以执行上述应用程序1000次。

  

nomutex:11.97用户| 5.76系统| 0:20.55过去了| 86%CPU

     

withmutex:30.78用户| 8.78系统| 0:43.67过去了| 90%CPU

现在大多数设备都有多核CPU,所以我使用下面的链接强制操作系统只使用单核。 https://unix.stackexchange.com/a/23109

希望这会对你有所帮助。