【问题标题】:Pthreads- Mutex Locks but variables don't changePthreads-互斥锁但变量不会改变
【发布时间】:2020-09-28 20:17:43
【问题描述】:

我正在编写一个非常简单的程序来演示我从 C++ 移植回 C 的 Pthreads 实现。

我创建了两个锁步线程并给他们两个工作

每一步增加一次 a1

一个递减a2每一步

在同步阶段(当互斥锁为 t1 和 t2 都锁定时)我比较 a1 和 a2 以查看我们是否应该停止步进。

我想知道我是不是发疯了,因为不仅变量在步进和锁定后并不总是变化,而且它们有时以不同的速率变化即使在锁定之后线程仍在运行。

编辑:是的,我确实对此进行了研究。是的,C++ 实现有效。是的,C++ 实现与这个几乎相同,但我必须在 c 中转换 PTHREAD_MUTEX_INITIALIZER 和 PTHREAD_COND_INITIALIZER 并将其作为第一个参数传递给每个函数。我花了一段时间试图调试这个(没有淘汰 gdb)但无济于事。

#ifndef LOCKSTEPTHREAD_H
#define LOCKSTEPTHREAD_H
#include <pthread.h>
#include <stdio.h>
typedef struct {
    pthread_mutex_t myMutex;
    pthread_cond_t myCond;
    pthread_t myThread;
    int isThreadLive;
    int shouldKillThread;
    void (*execute)();
} lsthread;
void init_lsthread(lsthread* t);
void start_lsthread(lsthread* t);
void kill_lsthread(lsthread* t);
void kill_lsthread_islocked(lsthread* t);
void lock(lsthread* t);
void step(lsthread* t);
void* lsthread_func(void* me_void);
#ifdef LOCKSTEPTHREAD_IMPL
//function declarations

void init_lsthread(lsthread* t){
    //pthread_mutex_init(&(t->myMutex), NULL);
    //pthread_cond_init(&(t->myCond), NULL);
    t->myMutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
    t->myCond = (pthread_cond_t)PTHREAD_COND_INITIALIZER;
    t->isThreadLive = 0;
    t->shouldKillThread = 0;
    t->execute = NULL;
}
void destroy_lsthread(lsthread* t){
    pthread_mutex_destroy(&t->myMutex);
    pthread_cond_destroy(&t->myCond);
}
void kill_lsthread_islocked(lsthread* t){
    if(!t->isThreadLive)return;
    //lock(t);
    t->shouldKillThread = 1;
    step(t);
    pthread_join(t->myThread,NULL);
    t->isThreadLive = 0;
    t->shouldKillThread = 0;
}

void kill_lsthread(lsthread* t){
    if(!t->isThreadLive)return;
    lock(t);
    t->shouldKillThread = 1;
    step(t);
    pthread_join(t->myThread,NULL);
    t->isThreadLive = 0;
    t->shouldKillThread = 0;
}
void lock(lsthread* t){
    if(pthread_mutex_lock(&t->myMutex))
        puts("\nError locking mutex.");
}

void step(lsthread* t){
    if(pthread_cond_signal(&(t->myCond)))
        puts("\nError signalling condition variable");
    if(pthread_mutex_unlock(&(t->myMutex)))
        puts("\nError unlocking mutex");
}
void* lsthread_func(void* me_void){
    lsthread* me = (lsthread*) me_void;
    int ret;
    if (!me)pthread_exit(NULL);
    if(!me->execute)pthread_exit(NULL);
    while (!(me->shouldKillThread)) {
        ret = pthread_cond_wait(&(me->myCond), &(me->myMutex));
        if(ret)pthread_exit(NULL);
        if (!(me->shouldKillThread) && me->execute)
            me->execute();
    }
    pthread_exit(NULL);
}
void start_lsthread(lsthread* t){
    if(t->isThreadLive)return;
    t->isThreadLive = 1;
    t->shouldKillThread = 0;
    pthread_create(
        &t->myThread,
        NULL,
        lsthread_func,
        (void*)t
    );
}
#endif
#endif

这是我的驱动程序:

#define LOCKSTEPTHREAD_IMPL
#include "include/lockstepthread.h"
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
unsigned char a1, a2;
void JobThread1(){
    unsigned char copy = a1;
    copy++;
    a1 = copy;
}
void JobThread2(){
    unsigned char copy = a2;
    copy--;
    a2 = copy;
}
int main(){
    char inputline[2048];
    inputline[2047] = '\0';
    lsthread t1, t2;
    init_lsthread(&t1);
    init_lsthread(&t2);
    t1.execute = JobThread1;
    t2.execute = JobThread2;
    printf(
    "\nThis program demonstrates threading by having"
    "\nTwo threads \"walk\" toward each other using unsigned chars."
    "\nunsigned Integer overflow guarantees the two will converge."
    );
    printf("\nEnter a number for thread 1 to process: ");
    fgets(inputline, 2047,stdin);
    a1 = (unsigned char)atoi(inputline);
    printf("\nEnter a number for thread 2 to process: ");
    fgets(inputline, 2047,stdin);
    a2 = (unsigned char)atoi(inputline);
    start_lsthread(&t1);
    start_lsthread(&t2);
    unsigned int i = 0;
    lock(&t1);
    lock(&t2);
    do{
        printf("\n%u: a1 = %d, a2 = %d",i++,(int)a1,(int)a2);
        fflush(stdout);
        step(&t1);
        step(&t2);
        lock(&t1);
        lock(&t2);
    }while(a1 < a2);
    kill_lsthread_islocked(&t1);
    kill_lsthread_islocked(&t2);
    destroy_lsthread(&t1);
    destroy_lsthread(&t2);
    return 0;
}

示例程序用法:

Enter a number for thread 1 to process: 5

Enter a number for thread 2 to process: 10

0: a1 = 5, a2 = 10
1: a1 = 5, a2 = 10
2: a1 = 5, a2 = 10
3: a1 = 5, a2 = 10
4: a1 = 5, a2 = 10
5: a1 = 5, a2 = 10
6: a1 = 6, a2 = 9
7: a1 = 6, a2 = 9
8: a1 = 7, a2 = 9
9: a1 = 7, a2 = 9
10: a1 = 7, a2 = 9
11: a1 = 7, a2 = 9
12: a1 = 8, a2 = 9

那么,交易是什么?

【问题讨论】:

  • 您应该阅读pthread_cond_wait()pthread_cond_signal() 的联机帮助页。所谓阅读,我的意思是仔细阅读,就好像你想真正理解它们一样
  • 我已经读了好几遍了。是的,当我编写 C++ 实现时,我花了一段时间盯着手册页。我又盯着它看。当它被调用时,互斥锁被调用线程释放,因此它可以被另一个线程锁定。当它返回时,互斥锁被调用线程锁定。 pthread_cond_signal 允许 pthread_cond_wait 继续进行下一次迭代。我花了很长时间玩它。
  • 阅读理解似乎有点弱。 POSIX documentation "当使用条件变量时,总是有一个布尔谓词涉及与每个条件等待关联的共享变量,如果线程应该继续,则该谓词为真。可能会发生来自 pthread_cond_wait() 或 pthread_cond_timedwait() 函数的虚假唤醒。"
  • 我犯了一个明显的错误吗?你能帮我而不是给我谜语吗?我不明白关于虚假唤醒的一点。是这样吗?
  • 游戏示例正是您使用pthread_barrier_wait() 让线程在帧结束时同步在一起的地方。

标签: c pthreads


【解决方案1】:

一般来说,听起来您真正要寻找的是一个障碍。尽管如此,我还是按照提出的问题回答。

是的,C++ 实现有效。是的,C++ 实现是 几乎和这个一样,但我不得不投 C 中的 PTHREAD_MUTEX_INITIALIZER 和 PTHREAD_COND_INITIALIZER 并通过 this 作为每个函数的第一个参数。我花了一段时间尝试 调试这个(没有淘汰 gdb)无济于事。

这似乎不太可能。呈现的代码中存在数据竞争和未定义的行为,无论是解释为 C 还是 C++。

一般设计

既然您提供了一个显式的lock() 函数,这似乎是合理的,您也应该提供一个显式的unlock() 函数。任何其他希望在互斥锁锁定的情况下调用的函数都应该在互斥锁锁定的情况下返回,以便调用者可以显式地将lock() 调用与unlock() 调用配对。不遵守此模式会导致错误。

特别是,step() 不应该解锁互斥锁,除非它也锁定它,但我认为非锁定版本将适合此目的。

初始化

我必须在 c 中强制转换 PTHREAD_MUTEX_INITIALIZER 和 PTHREAD_COND_INITIALIZER

不,你没有,因为你不能,至少如果pthread_mutex_tpthread_cond_t 是结构类型,则不能。结构类型的初始化器不是值。它们没有类型,也不能强制转换。但是您可以从它们中形成复合文字,而这正是您无意中所做的。 这不是为pthread_mutex_tpthread_cond_t 赋值的一种合规方式* 初始化器宏仅用于在其声明中初始化变量.这就是“初始化器”在此上下文中的含义。

示例:

pthread_mutex_t mutex = PTREAD_MUTEX_INITIALIZER;

示例:

struct {
    pthread_mutex_t mutex;
    pthread_cond_t  cv;
} example = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER };

要在任何其他上下文中初始化互斥锁或条件变量对象,需要使用相应的初始化函数pthread_mutex_init()pthread_cond_init()

数据竞赛

如果任何访问是写访问,则多个并发运行的线程对共享数据的非原子访问必须受到互斥锁或其他同步对象的保护(例外情况适用于对互斥锁和其他同步对象本身的访问)。您示例中的共享数据包括文件范围变量a1a2,以及lsthread 实例的大多数成员。您的lsthread_func 和驱动程序有时都无法在访问这些共享数据之前锁定适当的互斥锁,并且所涉及的一些访问确实是写入,因此会出现未定义的行为。观察到 a1a2 的意外值是这种未定义行为的完全合理的表现。

条件变量用法

调用pthread_cond_wait() 的线程必须在保持指定互斥锁锁定的情况下这样做。您的 lsthread_func() 不符合该要求,因此会出现更多未定义的行为。如果你很幸运,那可能会立即表现为虚假唤醒。

说到虚假唤醒,你并没有防范它们。如果确实发生了,那么lsthread_func() 会愉快地继续执行其循环的另一次迭代。为避免这种情况,您需要在某处共享数据,条件变量的 condition 正是基于该共享数据。 CV 的标准用法是在等待之前检查谓词,并在唤醒后循环返回并再次检查,必要时重复,直到谓词评估为真才继续。

同步步进

工作线程之间不直接同步,因此只有驱动程序才能确保其中一个不会领先于另一个。 但它没有。驱动程序根本不做任何事情来确保两个线程都已完成一个步骤,然后才向两个线程发出信号以执行另一个。条件变量不存储信号,所以如果由于调度的不幸或所涉及的任务的性质,一个线程应该领先另一个线程,它将保持领先,直到并且除非错误碰巧由另一边的失误。

可能你想添加一个lsthread_wait() 函数来等待线程完成一个步骤。这将涉及从相反方向使用 CV。

总体而言,您可以(更好地)提供单步执行

  • 添加成员以键入lsthread 以指示线程是否应该或正在执行步骤vs。是否在步骤之间,是否应该等待。

    typedef struct {
        // ...
        _Bool should_step;
    } lsthread;
    
  • 加上前面提到的lsthread_wait(),大概是这样的:

    // The calling thread must hold t->myMutex locked
    void lsthread_wait(lsthread *t) {
        // Wait, if necessary, for the thread to complete a step
        while (t->should_step) {
            pthread_cond_wait(&t->myCond, &t->myMutex);
        }
        assert(!t->should_step);
    
        // Prepare to perform another step
        t->should_step = 1;
    }
    
  • 这将与 lsthread_func() 的修订版配对:

     void* lsthread_func(void* me_void){
         lsthread* me = (lsthread*) me_void;
         if (!me) pthread_exit(NULL);
    
         lock(me); // needed to protect access to *me members and to globals
         while (!me->shouldKillThread && me->execute) {
             while (!me->should_step && !me->shouldKillThread) {
                 int ret = pthread_cond_wait(&(me->myCond), &(me->myMutex));
                 if (ret) {
                     unlock(me);  // mustn't forget to unlock
                     pthread_exit(NULL);
                 }
             }
             assert(me->should_step || me->shouldKillThread);
    
             if (!me->shouldKillThread && me->execute) {
                 me->execute();
             }
    
             // Mark and signal step completed
             me->should_step = 0;
             ret = pthread_cond_broadcast(me->myCond);
             if (ret) break;
         }
         unlock(me);
    
         pthread_exit(NULL);
     }
    
  • 修改 step() 以避免它解锁互斥锁。

  • 修改驱动循环以适当地使用新的等待函数

    lock(&t1);
    lock(&t2);
    do {
        printf("\n%u: a1 = %d, a2 = %d", i++, (int) a1, (int) a2);
        fflush(stdout);
        step(&t1);
        step(&t2);
        lsthread_wait(&t1);
        lsthread_wait(&t2);
    } while (a1 < a2);  // both locks must be held when this condition is evaluated
    kill_lsthread_islocked(&t2);
    kill_lsthread_islocked(&t1);
    unlock(&t2);
    unlock(&t1);
    

这不一定是所有需要的更改,但我想我已经涵盖了所有关键点。

最后说明

以上建议基于示例程序,其中不同的工作线程不会访问任何相同的共享数据。这使得使用每线程互斥锁来保护它们访问的共享数据成为可能。如果工作人员访问了任何相同的共享数据,并且与他们同时运行的那些或任何线程修改了相同的数据,那么每个工作线程的互斥体将无法提供足够的保护。


* 如果 pthread_mutex_tpthread_cond_t 是指针或整数类型,这是允许的,那么编译器将接受有问题的赋值而不进行强制转换(实际上 在这种情况下是演员),但就 pthreads 而言,这些分配仍然是不合格的。

【讨论】:

  • 谢谢。在 Initializer 位上,是的,我知道我形成了复合文字。但我将任何 (type) 称为“强制转换”,因为它看起来 像 C 风格的强制转换。
  • 一般的设计是这样的:锁应该保证线程返回后不运行。 Step 应该允许线程执行,并且应该在下一次调用 Lock 之前执行这对于游戏引擎很有用,并且通常必须在工作线程之间同步数据,但每个游戏时间只有一次。
  • 最后我写了一个基于 Barriers 的实现。我还将屏障实现反向移植到我的 C++ 版本,因为它似乎更稳定(我做了更多测试,C++ 版本确实 有错误,但它们之前没有出现,因为我从来没有彻底测试)
  • 我的实现已作为免费软件发布
猜你喜欢
  • 2011-03-05
  • 2015-11-12
  • 1970-01-01
  • 2021-10-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多