MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言多线程并发控制的策略

2022-09-296.0k 阅读

多线程并发基础概念

线程与进程

在Linux系统中,进程是资源分配的基本单位,而线程是CPU调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。相比进程间的切换,线程间的切换开销更小,因此在需要大量并发执行任务的场景下,多线程编程具有更高的效率。

例如,在一个Web服务器程序中,每个请求可以由一个线程来处理,这样可以在同一进程内同时处理多个请求,提高服务器的并发处理能力。

并发与并行

并发(Concurrency)是指在同一时间段内,多个任务交替执行,宏观上看起来像是同时进行。而并行(Parallelism)是指在同一时刻,多个任务真正地同时执行,这需要多核CPU的支持。在多线程编程中,通过合理的调度,可以实现并发执行任务。

多线程并发控制的必要性

资源竞争问题

当多个线程同时访问和修改共享资源时,就会出现资源竞争问题。例如,多个线程同时对一个全局变量进行累加操作,如果没有适当的控制,最终的结果可能是错误的。

下面是一个简单的代码示例,展示资源竞争问题:

#include <stdio.h>
#include <pthread.h>

int count = 0;

void* increment(void* arg) {
    int i;
    for (i = 0; i < 1000000; i++) {
        count++;
    }
    return NULL;
}

int main() {
    pthread_t tid1, tid2;

    if (pthread_create(&tid1, NULL, increment, NULL) != 0) {
        printf("\n ERROR creating thread 1");
        return 1;
    }
    if (pthread_create(&tid2, NULL, increment, NULL) != 0) {
        printf("\n ERROR creating thread 2");
        return 2;
    }

    if (pthread_join(tid1, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 3;
    }
    if (pthread_join(tid2, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 4;
    }

    if (count != 2000000) {
        printf("\n BOOM! count is [%d], should be 2000000\n", count);
    } else {
        printf("\n OK! count is [%d]\n", count);
    }

    return 0;
}

在这个例子中,两个线程同时对count变量进行累加操作。由于没有并发控制,每次运行程序,count的值可能都不等于2000000,这就是资源竞争导致的结果。

死锁问题

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。例如,线程A持有资源1并等待资源2,而线程B持有资源2并等待资源1,这样就形成了死锁。

以下是一个死锁的代码示例:

#include <pthread.h>
#include <stdio.h>

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

void* thread1(void* arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread 1 has locked mutex 1\n");
    sleep(1);
    pthread_mutex_lock(&mutex2);
    printf("Thread 1 has locked mutex 2\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}

void* thread2(void* arg) {
    pthread_mutex_lock(&mutex2);
    printf("Thread 2 has locked mutex 2\n");
    sleep(1);
    pthread_mutex_lock(&mutex1);
    printf("Thread 2 has locked mutex 1\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return NULL;
}

int main() {
    pthread_t tid1, tid2;

    if (pthread_create(&tid1, NULL, thread1, NULL) != 0) {
        printf("\n ERROR creating thread 1");
        return 1;
    }
    if (pthread_create(&tid2, NULL, thread2, NULL) != 0) {
        printf("\n ERROR creating thread 2");
        return 2;
    }

    if (pthread_join(tid1, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 3;
    }
    if (pthread_join(tid2, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 4;
    }

    pthread_mutex_destroy(&mutex1);
    pthread_mutex_destroy(&mutex2);

    return 0;
}

在这个例子中,线程1先锁定mutex1,然后尝试锁定mutex2,而线程2先锁定mutex2,然后尝试锁定mutex1。由于两个线程互相等待对方释放资源,就会导致死锁。

互斥锁(Mutex)实现并发控制

互斥锁的基本原理

互斥锁(Mutex,即Mutual Exclusion的缩写)是一种简单的并发控制机制。它只有两种状态:锁定(locked)和解锁(unlocked)。当一个线程获取到互斥锁(将其锁定)时,其他线程就不能再获取,直到该线程释放互斥锁(将其解锁)。这样就可以保证在同一时刻只有一个线程能够访问共享资源,从而避免资源竞争问题。

互斥锁的使用方法

在Linux C语言中,使用pthread_mutex_t类型来表示互斥锁。以下是使用互斥锁解决前面资源竞争问题的代码示例:

#include <stdio.h>
#include <pthread.h>

int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* increment(void* arg) {
    int i;
    for (i = 0; i < 1000000; i++) {
        pthread_mutex_lock(&mutex);
        count++;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t tid1, tid2;

    if (pthread_create(&tid1, NULL, increment, NULL) != 0) {
        printf("\n ERROR creating thread 1");
        return 1;
    }
    if (pthread_create(&tid2, NULL, increment, NULL) != 0) {
        printf("\n ERROR creating thread 2");
        return 2;
    }

    if (pthread_join(tid1, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 3;
    }
    if (pthread_join(tid2, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 4;
    }

    if (count != 2000000) {
        printf("\n BOOM! count is [%d], should be 2000000\n", count);
    } else {
        printf("\n OK! count is [%d]\n", count);
    }

    pthread_mutex_destroy(&mutex);

    return 0;
}

在这个例子中,通过在对count变量进行操作前后分别调用pthread_mutex_lockpthread_mutex_unlock函数,确保了同一时刻只有一个线程能够修改count,从而解决了资源竞争问题。

互斥锁的注意事项

  1. 初始化:互斥锁在使用前需要进行初始化,可以使用PTHREAD_MUTEX_INITIALIZER进行静态初始化,或者使用pthread_mutex_init函数进行动态初始化。动态初始化适用于互斥锁的属性需要定制的情况。
  2. 销毁:当互斥锁不再使用时,需要调用pthread_mutex_destroy函数进行销毁,以释放相关资源。
  3. 死锁风险:虽然互斥锁可以解决资源竞争问题,但如果使用不当,仍然可能导致死锁。例如,在嵌套锁的情况下,如果获取锁的顺序不一致,就容易出现死锁。

读写锁(Read - Write Lock)实现并发控制

读写锁的基本原理

读写锁允许在同一时刻有多个线程进行读操作,但只允许一个线程进行写操作。这是因为读操作不会修改共享资源,多个线程同时读不会产生资源竞争问题,而写操作会修改共享资源,必须保证同一时刻只有一个线程进行写操作。

读写锁的使用方法

在Linux C语言中,使用pthread_rwlock_t类型来表示读写锁。以下是一个使用读写锁的示例代码:

#include <stdio.h>
#include <pthread.h>

int data = 0;
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

void* reader(void* arg) {
    int i;
    for (i = 0; i < 5; i++) {
        pthread_rwlock_rdlock(&rwlock);
        printf("Reader %ld reads data: %d\n", (long)pthread_self(), data);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return NULL;
}

void* writer(void* arg) {
    int i;
    for (i = 0; i < 3; i++) {
        pthread_rwlock_wrlock(&rwlock);
        data++;
        printf("Writer %ld writes data: %d\n", (long)pthread_self(), data);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t tid1, tid2, tid3;

    if (pthread_create(&tid1, NULL, reader, NULL) != 0) {
        printf("\n ERROR creating reader thread 1");
        return 1;
    }
    if (pthread_create(&tid2, NULL, reader, NULL) != 0) {
        printf("\n ERROR creating reader thread 2");
        return 2;
    }
    if (pthread_create(&tid3, NULL, writer, NULL) != 0) {
        printf("\n ERROR creating writer thread");
        return 3;
    }

    if (pthread_join(tid1, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 4;
    }
    if (pthread_join(tid2, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 5;
    }
    if (pthread_join(tid3, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 6;
    }

    pthread_rwlock_destroy(&rwlock);

    return 0;
}

在这个例子中,读者线程通过调用pthread_rwlock_rdlock获取读锁,写者线程通过调用pthread_rwlock_wrlock获取写锁。读锁允许多个读者线程同时进入临界区读取数据,而写锁会阻止其他线程(包括读者和写者)进入临界区,直到写操作完成并释放写锁。

读写锁的注意事项

  1. 优先级问题:读写锁的实现可能存在读优先或写优先的情况。读优先可能导致写操作长时间等待,而写优先可能导致读操作长时间等待。在实际应用中,需要根据具体需求选择合适的实现或进行优先级调整。
  2. 死锁风险:与互斥锁类似,读写锁在嵌套使用或获取锁顺序不当的情况下,也可能导致死锁。

条件变量(Condition Variable)实现并发控制

条件变量的基本原理

条件变量是一种线程同步机制,它允许线程等待某个条件满足后再继续执行。条件变量通常与互斥锁配合使用。线程在等待条件变量时,会先释放互斥锁,进入睡眠状态,当条件满足时,由其他线程唤醒等待的线程,唤醒后的线程会重新获取互斥锁,然后继续执行。

条件变量的使用方法

在Linux C语言中,使用pthread_cond_t类型来表示条件变量。以下是一个使用条件变量的示例代码:

#include <stdio.h>
#include <pthread.h>

int ready = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void* waiter(void* arg) {
    pthread_mutex_lock(&mutex);
    while (!ready) {
        printf("Waiter is waiting...\n");
        pthread_cond_wait(&cond, &mutex);
    }
    printf("Waiter condition met, proceeding...\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* signaler(void* arg) {
    sleep(3);
    pthread_mutex_lock(&mutex);
    ready = 1;
    printf("Signaler setting ready to 1\n");
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t tid1, tid2;

    if (pthread_create(&tid1, NULL, waiter, NULL) != 0) {
        printf("\n ERROR creating waiter thread");
        return 1;
    }
    if (pthread_create(&tid2, NULL, signaler, NULL) != 0) {
        printf("\n ERROR creating signaler thread");
        return 2;
    }

    if (pthread_join(tid1, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 3;
    }
    if (pthread_join(tid2, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 4;
    }

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}

在这个例子中,waiter线程在ready条件不满足时,调用pthread_cond_wait等待条件变量,同时释放mutex互斥锁。signaler线程在睡眠3秒后,设置ready为1,并调用pthread_cond_signal唤醒waiter线程。waiter线程被唤醒后,重新获取mutex互斥锁,然后继续执行。

条件变量的注意事项

  1. 使用while循环等待:在使用pthread_cond_wait等待条件变量时,应该使用while循环来检查条件,而不是if语句。这是因为可能存在虚假唤醒的情况,即条件变量被意外唤醒,而实际条件并未满足。
  2. 与互斥锁配合使用:条件变量必须与互斥锁配合使用,否则可能会出现竞态条件。在调用pthread_cond_wait之前,必须先获取互斥锁,并且pthread_cond_wait会自动释放互斥锁并进入等待状态,当被唤醒时,会重新获取互斥锁。

信号量(Semaphore)实现并发控制

信号量的基本原理

信号量是一个整型变量,它通过计数器来控制对共享资源的访问。信号量的值表示可用资源的数量。当一个线程获取信号量时,信号量的值减1;当一个线程释放信号量时,信号量的值加1。如果信号量的值为0,那么获取信号量的线程会被阻塞,直到信号量的值大于0。

信号量的使用方法

在Linux C语言中,可以使用sem_t类型来表示信号量,相关函数有sem_init(初始化信号量)、sem_wait(获取信号量)、sem_post(释放信号量)和sem_destroy(销毁信号量)。以下是一个使用信号量控制线程访问共享资源数量的示例代码:

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem;
int shared_resource = 0;

void* thread_function(void* arg) {
    sem_wait(&sem);
    shared_resource++;
    printf("Thread %ld entered critical section, shared resource value: %d\n", (long)pthread_self(), shared_resource);
    sleep(1);
    shared_resource--;
    printf("Thread %ld left critical section, shared resource value: %d\n", (long)pthread_self(), shared_resource);
    sem_post(&sem);
    return NULL;
}

int main() {
    pthread_t tid1, tid2, tid3;

    if (sem_init(&sem, 0, 2) != 0) {
        printf("\n ERROR initializing semaphore");
        return 1;
    }

    if (pthread_create(&tid1, NULL, thread_function, NULL) != 0) {
        printf("\n ERROR creating thread 1");
        return 2;
    }
    if (pthread_create(&tid2, NULL, thread_function, NULL) != 0) {
        printf("\n ERROR creating thread 2");
        return 3;
    }
    if (pthread_create(&tid3, NULL, thread_function, NULL) != 0) {
        printf("\n ERROR creating thread 3");
        return 4;
    }

    if (pthread_join(tid1, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 5;
    }
    if (pthread_join(tid2, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 6;
    }
    if (pthread_join(tid3, NULL) != 0) {
        printf("\n ERROR joining thread");
        return 7;
    }

    sem_destroy(&sem);

    return 0;
}

在这个例子中,通过sem_init将信号量初始化为2,表示最多允许两个线程同时访问共享资源。每个线程在进入临界区(访问共享资源)前调用sem_wait获取信号量,离开临界区时调用sem_post释放信号量。

信号量的注意事项

  1. 初始化值的选择:信号量的初始值决定了同时允许访问共享资源的线程数量。需要根据实际需求合理设置初始值。
  2. 死锁风险:与其他并发控制机制类似,如果信号量的获取和释放顺序不当,也可能导致死锁。

多线程并发控制策略的选择

根据应用场景选择

  1. 资源竞争场景:如果主要问题是多个线程对共享资源的竞争,互斥锁是一个简单有效的选择。例如,在对全局变量进行读写操作时,使用互斥锁可以确保数据的一致性。
  2. 读写频繁场景:当读操作远多于写操作时,读写锁可以提高并发性能。比如在一个数据库查询频繁但更新较少的应用中,读写锁可以让多个查询线程同时进行,而写操作时能保证数据的一致性。
  3. 条件等待场景:如果线程需要等待某个条件满足后才能继续执行,条件变量是合适的选择。例如,在生产者 - 消费者模型中,消费者线程需要等待生产者线程生产数据后才能消费,这时可以使用条件变量来实现线程间的同步。
  4. 资源数量控制场景:当需要控制同时访问共享资源的线程数量时,信号量是一个好的选择。比如在连接池的实现中,使用信号量可以控制同时获取连接的线程数量。

综合使用多种策略

在复杂的多线程应用中,往往需要综合使用多种并发控制策略。例如,在一个网络服务器程序中,可能会使用互斥锁来保护共享的连接池资源,使用条件变量来实现线程间的任务同步,同时使用读写锁来优化对配置文件等共享数据的读写操作。

在实际编程中,需要仔细分析应用场景的特点,选择合适的并发控制策略,以实现高效、稳定的多线程程序。同时,要注意避免死锁、资源泄漏等常见问题,确保程序的正确性和可靠性。通过合理运用这些并发控制策略,可以充分发挥多线程编程的优势,提高程序的性能和响应能力。