MST

星途 面试题库

面试题:C语言大型项目中的多线程与内存同步

假设你正在开发一个基于C语言的多线程大型项目,其中多个线程需要同时访问和修改共享内存中的数据。请描述如何使用POSIX线程库(pthread)来实现线程同步,防止数据竞争。并说明在实际项目中,当面对复杂数据结构和高并发场景时,可能会遇到哪些挑战,以及你会如何解决这些挑战。
44.5万 热度难度
编程语言C

知识考点

AI 面试

面试题答案

一键面试

使用POSIX线程库(pthread)实现线程同步防止数据竞争的方法

  1. 互斥锁(Mutex)
    • 原理:互斥锁是一种二元信号量,它只有两种状态:锁定(locked)和解锁(unlocked)。当一个线程获取了互斥锁(将其锁定),其他线程在试图获取该互斥锁时就会被阻塞,直到该互斥锁被解锁。
    • 代码示例
#include <pthread.h>
#include <stdio.h>

// 共享数据
int shared_variable = 0;
// 互斥锁
pthread_mutex_t mutex;

void* increment(void* arg) {
    // 加锁
    pthread_mutex_lock(&mutex);
    shared_variable++;
    // 解锁
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    // 创建线程
    pthread_create(&thread1, NULL, increment, NULL);
    pthread_create(&thread2, NULL, increment, NULL);

    // 等待线程结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);

    printf("Final value of shared_variable: %d\n", shared_variable);
    return 0;
}
  1. 读写锁(Read - Write Lock)
    • 原理:读写锁允许多个线程同时进行读操作,但只允许一个线程进行写操作。当有线程进行写操作时,其他读线程和写线程都将被阻塞。这样可以提高读操作的并发性能,因为读操作不会修改数据,所以多个读操作可以同时进行而不会引发数据竞争。
    • 代码示例
#include <pthread.h>
#include <stdio.h>

// 共享数据
int shared_variable = 0;
// 读写锁
pthread_rwlock_t rwlock;

void* reader(void* arg) {
    // 读锁
    pthread_rwlock_rdlock(&rwlock);
    printf("Reader read: %d\n", shared_variable);
    // 解锁
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

void* writer(void* arg) {
    // 写锁
    pthread_rwlock_wrlock(&rwlock);
    shared_variable++;
    // 解锁
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

int main() {
    pthread_t reader1, reader2, writer1;
    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);

    // 创建线程
    pthread_create(&reader1, NULL, reader, NULL);
    pthread_create(&reader2, NULL, reader, NULL);
    pthread_create(&writer1, NULL, writer, NULL);

    // 等待线程结束
    pthread_join(reader1, NULL);
    pthread_join(reader2, NULL);
    pthread_join(writer1, NULL);

    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}
  1. 条件变量(Condition Variable)
    • 原理:条件变量用于线程间的同步,它允许线程等待某个条件满足。通常与互斥锁一起使用,一个线程获取互斥锁后,检查条件是否满足,如果不满足则在条件变量上等待,同时释放互斥锁。当另一个线程改变了条件并通知条件变量时,等待的线程被唤醒,重新获取互斥锁并检查条件。
    • 代码示例
#include <pthread.h>
#include <stdio.h>

// 共享数据
int shared_variable = 0;
// 互斥锁
pthread_mutex_t mutex;
// 条件变量
pthread_cond_t cond;

void* waiter(void* arg) {
    pthread_mutex_lock(&mutex);
    while (shared_variable == 0) {
        // 等待条件变量,会释放互斥锁
        pthread_cond_wait(&cond, &mutex);
    }
    printf("Waiter woke up, shared_variable: %d\n", shared_variable);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* signaler(void* arg) {
    pthread_mutex_lock(&mutex);
    shared_variable = 1;
    // 通知条件变量
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t waiter_thread, signaler_thread;
    // 初始化互斥锁和条件变量
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    // 创建线程
    pthread_create(&waiter_thread, NULL, waiter, NULL);
    pthread_create(&signaler_thread, NULL, signaler, NULL);

    // 等待线程结束
    pthread_join(waiter_thread, NULL);
    pthread_join(signaler_thread, NULL);

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}

复杂数据结构和高并发场景下的挑战及解决方法

  1. 死锁
    • 挑战描述:死锁是指两个或多个线程相互等待对方释放资源,从而导致所有线程都无法继续执行的情况。例如,线程A持有锁1并等待锁2,而线程B持有锁2并等待锁1。
    • 解决方法
      • 避免嵌套锁:尽量减少锁的嵌套使用,如果必须使用,确保所有线程以相同的顺序获取锁。
      • 资源分配图算法:使用资源分配图算法(如银行家算法)来检测和预防死锁,但这种方法在实际应用中复杂度较高,通常适用于资源数量有限且可预测的场景。
      • 超时机制:为获取锁设置超时时间,如果在规定时间内未能获取到锁,则放弃并释放已获取的锁,然后重新尝试获取锁。
  2. 性能问题
    • 挑战描述:在高并发场景下,频繁的加锁和解锁操作会带来性能开销,尤其是当锁的粒度较大时,会导致线程竞争激烈,降低系统的并发性能。
    • 解决方法
      • 减小锁的粒度:将大锁分解为多个小锁,使得不同的线程可以同时访问不同部分的共享数据,减少线程竞争。例如,对于一个包含多个元素的数组,可以为每个元素或每组元素设置单独的锁。
      • 使用无锁数据结构:对于一些简单的数据结构,如栈、队列等,可以使用无锁数据结构来提高并发性能。无锁数据结构通过使用原子操作来避免锁的使用,从而减少线程竞争。
      • 读写分离:如果共享数据的读操作远多于写操作,可以使用读写锁(如前面提到的)来提高读操作的并发性能。
  3. 复杂数据结构同步
    • 挑战描述:当共享数据是复杂数据结构(如链表、树等)时,同步操作变得更加复杂。例如,在链表的插入或删除操作中,需要保证操作的原子性,同时要考虑锁的粒度和线程安全。
    • 解决方法
      • 设计线程安全的数据结构:在设计复杂数据结构时,考虑如何使其线程安全。例如,对于链表,可以使用细粒度锁,每个节点都有自己的锁,在进行插入或删除操作时,只需要获取相关节点的锁。
      • 使用事务机制:借鉴数据库事务的思想,将对复杂数据结构的一系列操作作为一个原子事务,要么全部成功,要么全部失败。在事务开始时获取所有需要的锁,在事务结束时释放锁。不过,实现事务机制需要一定的设计和编程技巧。