【问题标题】:Efficient and fast way for thread argument线程参数的高效快捷方式
【发布时间】:2011-02-17 17:54:01
【问题描述】:

用参数创建线程最有效的方法是什么?参数是一个struct,如果struct不能留在父线程栈上,有两种解决方案。

动态内存分配

struct Arg{
    int x;
    int y;
};

void* my_thread(void* v_arg){
    Arg* arg = (Arg*) v_arg;

    //... running

    delete arg;
    return NULL;
}

//Creating a thread
void a_function(){
    Arg* arg = new Arg;
    arg->x = 1; arg->y = 2;

    pthread_t t;
    pthread_create(&t, NULL, my_thread, arg);
    pthread_detach(t);
}

带信号量

struct Arg{
    sem_t sem;
    int x;
    int y;
};

void* my_thread(void* v_arg){
    Arg* arg = (Arg*) v_arg;
    int arg_x = v_arg->x;
    int arg_y = v_arg->y;
    sem_post( &(v_arg->sem) );

    //... running

    return NULL;
}

//Creating a thread
void a_function(){
    Arg arg;
    arg.x = 1; arg.y = 2;
    sem_init( &(arg.sem), 0, 0);

    pthread_t t;
    pthread_create(&t, NULL, my_thread, &arg);
    pthread_detach(t);

    sem_wait( &(arg.sem) );
    sem_destroy( &(arg.sem) );
}

我使用 Linux 和 Windows。

【问题讨论】:

  • sn-p 表示您正在使用某种 C 风格的语言,我从您在 Linux 中编程这一事实推断它是 C,但我无法确定。由于它与问题高度相关,您能否用所使用的语言标记问题,和/或提供更多细节?
  • @KeithS:这绝对看起来像 Linux 上的 C 或 C++(他使用的是 pthreads),但类似的代码可用于 Windows,据我记得他的问题在两个平台上都有效。
  • 它是 C++,但可以使用 C。 Windows 有 pthreads。我以 pthreads 为例。

标签: c++ c multithreading performance pthreads


【解决方案1】:

在您发布的代码中,最有效的实现是使用堆分配(您的第一个示例)。这样做的原因是堆分配(使用 new() 或 malloc)比上下文切换便宜得多。考虑在您的第二个示例中需要发生什么:

  1. 为 Arg 分配堆栈空间
  2. 初始化信号灯
  3. 启动线程并切换上下文
  4. 将变量复制到新堆栈中
  5. 切换上下文返回
  6. 销毁信号量
  7. 分离线程
  8. 切换上下文

或者,您的第一个示例:

  1. 为 Arg 分配堆空间
  2. 启动线程
  3. 分离线程
  4. 切换上下文

【讨论】:

  • 这个答案不太对劲。如果机器是多核/SMP,第一个示例将不涉及任何上下文切换,仅涉及一些原子操作,并且可能在“父”线程中进行一点旋转(它不应该旋转足够长的时间以实际进入睡眠状态)。另一方面,malloc 涉及获取和释放全局状态的锁。即使你有一个基于 arena/threadcache-based 的分配器,这种在一个线程中分配内存并将其“捐赠”给另一个线程的模式也会导致更糟糕的争用行为,并可能导致糟糕的内存碎片。
  • @R - 原则上,我认为你说得有道理。但是,对于发布的代码,malloc(在 glibc 实现以及其他实现中)仅涉及对 fastbins 的原子操作,因为结构小于 64 字节。没有锁定。
  • 与我下面的解决方案相比,此答案的性能较低。
【解决方案2】:

这取决于。如果您的结构不大,最好动态分配它以最大程度地减少奇怪的同步调用。否则,如果您的结构很大并且您为内存很少的系统编写代码,则最好使用信号量(甚至是 condvar)。

【讨论】:

    【解决方案3】:

    原子操作解决方案。这是为您的论点获取记忆的一种非常高速的方法。

    如果参数的大小始终相同,请预先分配一堆。将 pNext 添加到您的结构中以将它们链接在一起。创建一个 _pRecycle 全局以将所有可用的作为链接列表保存,使用 pNext 链接它们。当您需要参数时,请使用原子操作将 CAS 放在垃圾列表的开头。完成后,使用原子操作将 arg 放回垃圾列表的开头。

    CAS 指的是 __sync_bool_compare_and_swap 之类的东西,成功时返回 1。

    获取参数记忆:

    while (1)  {  // concurrency loop
      pArg = _pRecycle;  // _pRecycle is the global ptr to the head of the available arguments
      // POINT A
      if (CAS(&_pRecycle, pArg->pNext, pArg))  // change pRecycle to next item if pRecycle hasn't changed.
        break; // success
      // POINT B
    }
    // you can now use pArg to pass arguments
    

    完成后回收参数内存:

    while (1)  {  // concurrency loop
      pArg->pNext = _pRecycle;
      if (CAS(&_pRecycle, pArg, pArg->pNext))  // change _pRecycle to pArg if _pRecycle hasn't changed.
        break; // success
    }
    // you have given the mem back 
    

    如果在 A 点和 B 点之间换出另一个线程时,如果某些东西使用并回收 pArg,则会出现竞争条件。如果您的工作需要很长时间来处理,这不会是一个问题。否则,您需要对列表的头部进行版本控制...为此,您需要能够一次以原子方式更改两件事...联合与 64 位 CAS 相结合!

    typedef union _RecycleList {
      struct {
        int   iversion;
        TArg *pHead;
      }
      unsigned long n64;  // this value is iVersion and pHead at the same time!
    } TRecycleList;
    
    TRecycleList _Recycle;
    

    获取内存:

    while (1)  // concurrency loop
    {
      TRecycleList Old.n64 = _Recycle.n64;
      TRecycleList New.n64 = Old.n64;
      New.iVersion++;
      pArg = New.pHead;
      New.pHead = New.pHead->pNext;
      if (CAS(&_Recycle.n64, New.n64, Old.n64)) // if version isnt changed we get mem
        break; // success
    }
    

    把内存放回去:

    while (1)  // concurrency loop
    {
      TRecycleList Old.n64 = _Recycle.n64;
      TRecycleList New.n64 = Old.n64;
      New.iVersion++;
      pArg->pNext = New.pHead;
      New.pHead = pArg;
      if (CAS(&_Recycle.n64, New.n64, Old.n64))  // if version isnt changed we release mem
        break; // success
    }
    

    由于 99.9999999% 的时间不会有两个线程同时执行代码以抢占内存,因此您可以获得出色的性能。我们的测试表明,CAS 的速度只有设置 _pRecycle = pRecycle->pNext 的 2 倍。 64 位和 128 位 CAS 与 32 位一样快。基本上它会尖叫。每隔一段时间,当两个线程实际竞争时,并发循环将执行两次。总有人会赢,所以比赛很快就结束了。

    【讨论】:

      猜你喜欢
      • 2011-06-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-02-24
      • 2014-11-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多