主线程和工作线程初始化

Main thread and worker thread initialization

我正在用 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->id 获得相同的 id。 为了解决这个问题,我使用变量 initialized 作为布尔值,因此在新线程初始化期间使 "sleep" 成为主线程。

但我认为那真的很罪恶。 也许有一种方法可以用互斥锁来做到这一点?但是我听说解锁一个不属于他的线程的互斥体不是"legal"。

感谢您的回答!

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

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

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

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

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

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);
}

但你也写:

To solve that, I use an variable initialized as a bool so make "sleep" the main thread during the new thread's initialization.

But I think that is really sinful. Maybe there is a way to do that with a mutex? But I heard it wasn't "legal" to unlock a mutex which does not belong his thread.

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

你可以用互斥来解决这个问题,方法是在你的循环中只保护 args->initialized 的测试 - 而不是整个循环 - 用互斥锁保护线程对该对象的写入相同的互斥量,但那是令人讨厌和丑陋的。最好等待新线程递增信号量(不是忙等待,并且 initialized 变量被信号量替换),或者设置并等待条件变量(同样不是忙等等,但仍然需要 initialized 变量或等效变量)。

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

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

您可以使用 John Bollinger 的解决方案,为每个线程 malloc 一组新的 args,这很好。唯一的缺点是每个线程创建一个 malloc/free 对。另一种方法是像 Santosh 建议的那样使用 "proper" 同步函数。我可能会考虑这一点,除非我会使用信号量,因为它比条件变量更简单。

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

因此解决方案是创建一个初始值为 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 的解决方案更适合这个用例,但通常了解信号量很有用。