【问题标题】:Main thread and worker thread initialization主线程和工作线程初始化
【发布时间】:2017-07-30 14:32:11
【问题描述】:

我正在用 C 创建一个多线程程序,但遇到了一些麻烦。 那里有创建线程的功能:

void        create_thread(t_game_data *game_data)
{
  size_t    i;
  t_args    *args = malloc(sizeof(t_args));

  i = 0;
  args->game = game_data;
  while (i < 10)
    {
      args->initialized = 0;
      args->id = i;
      printf("%zu CREATION\n", i);//TODO: Debug
      pthread_create(&game_data->object[i]->thread_id, NULL, &do_action, args);
      i++;
      while (args->initialized == 0)
          continue;
    }
} 

这里有我的 args 结构:

typedef struct      s_args
{
  t_game_data       *object;
  size_t            id;
  int               initialized;

}args;

最后是处理创建线程的函数

void        *do_action(void *v_args)
{
  t_args    *args;
  t_game_data   *game;
  size_t    id;
  args = v_args;
  game = args->game;
  id = args->id;
  args->initialized = 1;

[...]

  return (NULL);
}

问题是:

主线程创建新线程的速度比新线程初始化变量的速度要快:

args = v_args;
game = args->game;
id = args->id;

因此,有时,2 个不同的线程会从 args-&gt;id 获得相同的 id。 为了解决这个问题,我使用变量initialized 作为布尔值,所以在新线程初始化期间让主线程“休眠”。

但我认为这确实是有罪的。 也许有一种方法可以使用互斥锁来做到这一点?但是我听说解锁不属于他的线程的互斥锁是不“合法的”。

感谢您的回答!

【问题讨论】:

    标签: c multithreading pthreads libc


    【解决方案1】:

    解决这个问题最简单的方法是将不同的t_args 对象传递给每个新线程。为此,将分配移动到循环内,并让每个线程负责释放自己的参数结构:

    void create_thread(t_game_data *game_data) {
        for (size_t i = 0; i < 10; i++) {
            t_args *args = malloc(sizeof(t_args));
    
            if (!args) {
                /* ... handle allocation error ... */
            } else {
                args->game = game_data;
                args->id = i;
                printf("%zu CREATION\n", i);//TODO: Debug
                if (pthread_create(&game_data->object[i]->thread_id, NULL,
                        &do_action, args) != 0) {
                    // thread creation failed
                    free(args);
                    // ... 
                }
            }
        }
    } 
    
    // ...
    
    void *do_action(void *v_args) {
        t_args *args = v_args;
        t_game_data *game = args->game;
        size_t    id = args->id;
    
        free(v_args);
        args = v_args = NULL;
    
        // ...
    
        return (NULL);
    }
    

    但你也写:

    为了解决这个问题,我使用了一个初始化为布尔值的变量,所以让“睡眠” 新线程初始化期间的主线程。

    但我认为这确实是有罪的。也许有办法做到这一点 用互斥锁?但我听说解锁互斥锁是不“合法的” 不属于他的线程。

    如果您仍然希望一个线程等待另一个线程修改某些数据,就像您原来的策略所要求的那样,那么您必须使用原子数据或某种同步对象。您的代码否则包含数据竞争,因此具有未定义的行为。实际上,您不能在原始代码中假设主线程永远会看到新线程对args-&gt;initialized 的写入。 “有罪”是一种不寻常的描述方式,但如果您属于圣 C 教堂,则可能更合适。

    您可以使用互斥锁解决该问题,方法是仅使用互斥锁在循环中保护args-&gt;initialized 的测试(而不是整个循环),并使用相同的互斥锁保护线程对该对象的写入,但是这是讨厌和丑陋的。等待新线程增加信号量(不是忙等待,initialized 变量被信号量替换),或者设置并等待条件变量(同样不是忙等待)会好得多,但仍需要 initialized 变量或等效变量)。

    【讨论】:

      【解决方案2】:

      问题在于,在create_thread 中,您将相同的t_args 结构传递给每个线程。实际上,您可能希望为每个线程创建自己的 t_args 结构。

      发生的事情是您的第一个线程正在启​​动传递给它的参数。在该线程可以运行do_action 之前,循环正在修改 args 结构。由于 thread2 和 thread1 都将指向相同的 args 结构,因此当它们运行 do_action 时,它们将具有相同的 id。

      哦,别忘了不要泄露你的记忆

      【讨论】:

      • 这不是答案。问题中的代码有一个循环while (args-&gt;initialized == 0) continue;,OP 认为应该停止主线程,直到子线程设置 args->initialised。公平地说,我认为这个问题并不是特别清楚。
      • 耶。我完全错过了。没想到会在那里。
      【解决方案3】:

      除了几个主要问题之外,您的解决方案理论上应该可行。

      • 主线程将在 while 循环中旋转,该循环使用 CPU 周期检查标志(这是最不严重的问题,如果您知道它不必等待很长时间就可以了)
      • 编译器优化器可以让触发器对空循环感到满意。他们通常也没有意识到变量可能会被其他线程修改,并可能在此基础上做出错误的决定。
      • 在多核系统上,主线程可能永远不会看到对args-&gt;initiialzed 的更改,或者至少直到很久以后才会看到更改在另一个尚未刷新回主内存的内核的缓存中。

      您可以使用 John Bollinger 的解决方案,该解决方案为每个线程分配一组新的参数,这很好。唯一的缺点是每个线程创建都有一个 malloc/free 对。另一种方法是使用 Santosh 建议的“适当”同步功能。我可能会考虑这一点,除非我会使用信号量,因为它比条件变量更简单。

      信号量是一个原子计数器,有两个操作:等待和信号。如果信号量的值大于零,则等待操作递减信号量,否则将线程置于等待状态。信号操作会增加信号量,除非有线程在等待它。如果有,它会唤醒其中一个线程。

      因此解决方案是创建一个初始值为 0 的信号量,启动线程并等待该信号量。然后,线程在完成初始化后向信号量发出信号。

      #include <semaphore.h>
      
      // other stuff
      
      sem_t semaphore;
      void create_thread(t_game_data *game_data)
      {
           size_t    i;
           t_args    args;
      
           i = 0;
           if (sem_init(&semaphore, 0, 0) == -1) // third arg is initial value
           {
               // error
           }
           args.game = game_data;
           while (i < 10)
           {
               args.id = i;
               printf("%zu CREATION\n", i);//TODO: Debug
               pthread_create(&game_data->object[i]->thread_id, NULL, &do_action, args);
               sem_wait(&semaphore);
               i++;
          }
          sem_destroy(&semaphore);
      }
      
      void *do_action(void *v_args) {
          t_args *args = v_args;
          t_game_data *game = args->game;
          size_t    id = args->id;
          sem_post(&semaphore);
      
          // Rest of the thread work
      
          return NULL;
      } 
      

      由于同步,我可以安全地重用 args 结构,事实上,我什至不需要 malloc - 它很小,所以我将它声明为函数的本地。

      说了这么多,我仍然认为 John Bollinger 的解决方案更适合这个用例,但一般来说了解信号量很有用。

      【讨论】:

      • 哇,我不知道!看起来很神奇!谢谢
      • 只是一个问题,将args放入堆栈不会对线程有问题吗?
      • @LightMan 不,因为所有线程共享相同的地址空间,包括主进程堆栈使用的空间。但是,您必须确保线程在函数退出之前已经完成了结构(信号量会这样做)。
      【解决方案4】:

      您应该考虑为此使用条件变量。你可以在这里找到一个例子http://maxim.int.ru/bookshelf/PthreadsProgram/htm/r_28.html。 基本上在主线程中等待并在其他线程中发出信号。

      【讨论】:

      • 有人对您的答案投了反对票,可能是因为您没有提供太多细节。为什么要使用条件变量?
      猜你喜欢
      • 1970-01-01
      • 2012-08-14
      • 1970-01-01
      • 2021-06-17
      • 1970-01-01
      • 2016-07-01
      • 1970-01-01
      • 1970-01-01
      • 2013-11-03
      相关资源
      最近更新 更多