Linux系统编程-线程同步详解

线程同步是指多个线程协调工作,以便在共享资源的访问和操作过程中保持数据一致性和正确性。在多线程环境中,线程是并发执行的,因此如果多个线程同时访问和修改共享资源,可能会导致数据不一致、竞态条件(race condition)等问题。线程同步通过协调线程的执行顺序和共享资源的访问来避免这些问题。

在多线程编程中,需要线程同步的主要原因包括:

  • 共享资源的安全访问:多个线程可能同时访问和修改共享的数据或资源,如果没有同步机制,可能会导致数据不一致或损坏。

  • 避免竞态条件:竞态条件是指多个线程以不受控制的顺序访问共享资源,从而导致程序执行结果不确定的情况。通过同步机制可以避免竞态条件的发生。

  • 保证程序正确性:线程同步确保多线程程序的行为是可预测和正确的,避免因并发访问而引入的错误。

1.互斥量(Mutex)

互斥量(互斥锁)是一种最基本的同步机制,用于保护临界区(Critical Section),确保在同一时间只有一个线程可以访问共享资源。

它提供了两个主要操作:

  • 加锁(Locking):线程通过加锁操作获取互斥量,如果互斥量已经被其他线程锁定,则当前线程会阻塞,直到获取到锁。
  • 解锁(Unlocking):线程使用解锁操作释放互斥量,允许其他线程获取锁。

通过这种机制,互斥量确保了临界区中的代码在同一时间只能被一个线程执行,从而避免了数据竞争和不一致的问题。

1.1初始化和销毁互斥量

1.1.1pthread_mutex_init()

初始化互斥量,并可选地设置其属性。

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
参数:
pthread_mutex_t *mutex:指向互斥量变量的指针,用于初始化该互斥量。
const pthread_mutexattr_t *attr:可选参数,指向 pthread_mutexattr_t 类型的指针,用于设置互斥量的属性。如果为 NULL,使用默认属性。
返回值:
成功:返回 0。
失败:返回错误号(例如 EINVAL 表示参数无效)。
  • 初始化互斥量后需要调用 pthread_mutex_destroy 函数来释放其占用的资源。
  • 通常在使用互斥量前调用,确保互斥量的准备和设置。

1.1.2pthread_mutex_destroy()

销毁互斥量,释放其占用的资源。

int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
pthread_mutex_t *mutex:指向要销毁的互斥量变量的指针。
返回值:
成功:返回 0。
失败:返回错误号。
  • 在互斥量不再需要时调用,以释放其占用的资源。
  • 仅当确保没有线程在使用该互斥量时才能安全地调用该函数。

1.2加锁和解锁操作

1.2.1pthread_mutex_lock()

加锁操作,阻塞当前线程直到获取锁。

int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:
pthread_mutex_t *mutex:指向要加锁的互斥量变量的指针。
返回值:
成功:返回 0。
失败:返回错误号。
  • 当互斥量已经被其他线程锁定时,当前线程会阻塞直到获取锁。
  • 必须成对使用 pthread_mutex_lockpthread_mutex_unlock 来保护临界区。

1.2.2pthread_mutex_trylock()

尝试加锁操作,非阻塞式,立即返回结果。

int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数:
pthread_mutex_t *mutex:指向要加锁的互斥量变量的指针。
返回值:
成功:返回 0。
失败:返回 EBUSY(互斥量已被锁定)或其他错误号。
  • 如果互斥量已被锁定,则立即返回错误。
  • 适用于需要非阻塞尝试加锁的情况,可以用于避免线程阻塞。

1.2.3pthread_mutex_unlock()

解锁操作,释放互斥量。

int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
pthread_mutex_t *mutex:指向要解锁的互斥量变量的指针。
返回值:
成功:返回 0。
失败:返回错误号。
  • 解锁后允许其他线程获取互斥量。
  • 必须在每次成功调用 pthread_mutex_lock 后调用 pthread_mutex_unlock 来释放锁。

示例代码:

创建两个线程来共享一个全局变量 int number,然后每个线程分别对其进行5000次递增操作,同时使用互斥量来保证线程同步。(如果没有锁大家可以看看会发生什么情况)

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

#define NUM_THREADS 2
#define NUM_INCREMENTS 5000

int number = 0;
pthread_mutex_t mutex;  // 互斥量变量

void* thread_function(void* arg) {
    int thread_id = *(int*)arg;

    for (int i = 0; i < NUM_INCREMENTS; ++i) {
        // 加锁
        pthread_mutex_lock(&mutex);

        // 临界区:对共享变量 number 执行操作
        number++;

        // 解锁
        pthread_mutex_unlock(&mutex);
    }

    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];

    // 初始化互斥量
    if (pthread_mutex_init(&mutex, NULL) != 0) {
        fprintf(stderr, "Mutex initialization failed\n");
        return 1;
    }

    // 创建两个线程
    for (int i = 0; i < NUM_THREADS; ++i) {
        thread_ids[i] = i;
        if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
            fprintf(stderr, "Thread creation failed\n");
            return 1;
        }
    }

    // 等待所有线程结束
    for (int i = 0; i < NUM_THREADS; ++i) {
        pthread_join(threads[i], NULL);
    }

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

    // 输出最终的 number 值
    printf("Final value of number: %d\n", number);

    return 0;
}

1.3死锁(Deadlock)

死锁并不是linux提供给用户的一种使用方法,而是由于用户使用互斥锁不当引起的一种现象。死锁发生在多个并发执行的线程(或进程)之间,主要由于彼此竞争资源而造成。典型的死锁情况涉及两个或多个线程或进程,每个都在等待对方释放其持有的资源,导致所有线程都被阻塞,无法继续执行下去。

常见的死锁有两种:

第一种:自己锁自己,如下图代码片段

第二种 线程A拥有A锁,请求获得B锁;线程B拥有B锁,请求获得A锁,这样造成线程A和线程B都不释放自己的锁,而且还想得到对方的锁,从而产生死锁,如下图所示:

  • 如何解决死锁:
  • 让线程按照一定的顺序去访问共享资源
  • 在访问其他锁的时候,需要先将自己的锁解开
  • 调用pthread_mutex_trylock,如果加锁不成功会立刻返回

2. 条件变量(Condition Variable)

条件变量(Condition Variable) 是一种线程间同步的机制,通常与互斥锁结合使用,用于在某个条件满足时通知其他线程。条件变量允许线程在等待某个特定条件的同时阻塞,直到另一个线程显式地通知条件已经满足或者超时。条件本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。

  • 使用互斥量保护共享数据。
  • 使用条件变量可以使线程阻塞, 等待某个条件的发生, 当条件满足的时候解除阻塞。

主要作用包括:

  • 等待条件:使线程能够在满足特定条件之前进入休眠状态,节省系统资源。
  • 条件满足时通知:一旦其他线程改变了条件,可以通过条件变量通知正在等待的线程继续执行。

2.1相关操作函数

2.1.1pthread_cond_init()

初始化条件变量 cond

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参数:
pthread_cond_t *restrict cond:指向要初始化的条件变量的指针。
const pthread_condattr_t *restrict attr:可选参数,指向 pthread_condattr_t 类型的指针,用于设置条件变量的属性。通常可以设置为 NULL,表示使用默认属性。
返回值:
成功:返回 0。
失败:返回错误号(例如 EINVAL 表示参数无效)。
  • 初始化条件变量后,应当使用 pthread_cond_destroy 函数来释放其占用的资源。
  • 通常在创建条件变量后立即调用该函数进行初始化。

2.1.2pthread_cond_destroy()

销毁条件变量 cond,释放其占用的资源。

int pthread_cond_destroy(pthread_cond_t *cond);
参数:
pthread_cond_t *cond:指向要销毁的条件变量的指针。
返回值:
成功:返回 0。
失败:返回错误号。
  • 在条件变量不再需要时调用,以释放其占用的资源。
  • 确保没有线程在使用该条件变量时才能安全地调用该函数。

2.1.3pthread_cond_wait()

阻塞当前线程,等待条件变量 cond 被其他线程信号唤醒。

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数:
pthread_cond_t *restrict cond:指向要等待的条件变量的指针。
pthread_mutex_t *restrict mutex:与条件变量相关联的互斥锁,用于避免竞态条件。
返回值:
成功:返回 0。
失败:返回错误号。
  • 调用该函数前必须先获取 mutex 锁,函数内部会自动释放 mutex 锁,并在等待期间阻塞当前线程。
  • 当被唤醒时,函数内部会再次获取 mutex 锁,并从函数返回。

2.1.4pthread_cond_timedwait()

在指定的超时时间内阻塞当前线程,等待条件变量 cond 被其他线程信号唤醒。

int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
参数:
pthread_cond_t *restrict cond:指向要等待的条件变量的指针。
pthread_mutex_t *restrict mutex:与条件变量相关联的互斥锁。
const struct timespec *restrict abstime:指定的超时时间,为绝对时间。
返回值:
成功:返回 0。
失败:返回错误号。
  • pthread_cond_wait 类似,但可以设置超时时间,在超时之后会自动返回并解除阻塞。
  • 如果超时时间设置为 NULL,则函数将无限期地等待,直到条件变量被信号唤醒。

2.1.5pthread_cond_signal()

唤醒等待在条件变量 cond 上的一个线程。

int pthread_cond_signal(pthread_cond_t *cond);
参数:
pthread_cond_t *cond:指向要唤醒的条件变量的指针。
返回值:
成功:返回 0。
失败:返回错误号。
  • 唤醒等待在条件变量上的一个线程,如果有多个线程等待,则可能唤醒任意一个。
  • 唤醒后,被唤醒的线程将尝试重新获取与条件变量关联的互斥锁。

示例代码:

使用条件变量实现生产者与消费者模型

3. 读写锁(Read-Write Lock)

读写锁允许多个线程同时对共享资源进行读取操作,但是写操作时需要排他性,即同一时刻只能有一个线程进行写操作。这种区分读和写的方式能够有效地提高系统的并发性能,特别适用于数据结构中读操作远远多于写操作的情况。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。

读写锁特性:

  • 读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
  • 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
  • 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高

3.1初始化和销毁读写锁

3.1.1pthread_rwlock_init()

初始化读写锁 rwlock,可以选择性地设置其属性。

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
参数:
pthread_rwlock_t *restrict rwlock:指向要初始化的读写锁变量的指针。
const pthread_rwlockattr_t *restrict attr:可选参数,指向 pthread_rwlockattr_t 类型的指针,用于设置读写锁的属性。通常可以设置为 NULL,表示使用默认属性。
返回值:
成功:返回 0。
失败:返回错误号(例如 EINVAL 表示参数无效)。
  • 初始化读写锁后,应当使用 pthread_rwlock_destroy 函数来释放其占用的资源。
  • 通常在创建读写锁后立即调用该函数进行初始化。

3.1.2pthread_rwlock_destroy()

销毁读写锁 rwlock,释放其占用的资源。

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数:
pthread_rwlock_t *rwlock:指向要销毁的读写锁变量的指针。
返回值:
成功:返回 0。
失败:返回错误号。
  • 在读写锁不再需要时调用,以释放其占用的资源。
  • 确保没有线程在使用该读写锁时才能安全地调用该函数。

3.2读写锁加锁和解锁操作:

3.2.1pthread_rwlock_rdlock()

加读锁,允许多个线程同时对共享资源进行读取操作。

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
参数:
pthread_rwlock_t *rwlock:指向要加读锁的读写锁变量的指针。
返回值:
成功:返回 0。
失败:返回错误号。
  • 如果有其他线程持有写锁或者有线程在请求写锁,则当前线程将被阻塞,直到获取读锁为止。
  • 多个线程可以同时获取读锁,不会相互阻塞。

3.2.2pthread_rwlock_wrlock()

加写锁,确保只有一个线程可以对共享资源进行写操作,期间禁止其他线程的读或写操作。

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
参数:
pthread_rwlock_t *rwlock:指向要加写锁的读写锁变量的指针。
返回值:
成功:返回 0。
失败:返回错误号。
  • 如果有其他线程持有读锁或写锁,则当前线程将被阻塞,直到获取写锁为止。
  • 只能有一个线程可以同时持有写锁,其他线程无法同时获取读锁或写锁。

3.2.3pthread_rwlock_tryrdlock()

尝试加读锁,非阻塞方式。如果不能立即获得锁,则立即返回。

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
参数:
pthread_rwlock_t *rwlock:指向要加读锁的读写锁变量的指针。
返回值:
成功:返回 0。
失败:返回 EBUSY(读写锁已被写锁占用)或其他错误号。
  • 如果不能立即获取读锁,则该函数立即返回失败。
  • 适用于需要检测是否可以立即读取共享资源的场景。

3.2.4pthread_rwlock_trywrlock()

尝试加写锁,非阻塞方式。如果不能立即获得锁,则立即返回。

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
参数:
pthread_rwlock_t *rwlock:指向要加写锁的读写锁变量的指针。
返回值:
成功:返回 0。
失败:返回 EBUSY(读写锁已被其他线程占用)或其他错误号。
  • 如果不能立即获取写锁,则该函数立即返回失败。
  • 适用于需要检测是否可以立即写入共享资源的场景。

3.2.5pthread_rwlock_unlock()

解锁操作,释放之前加的读锁或写锁。

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
参数:
pthread_rwlock_t *rwlock:指向要解锁的读写锁变量的指针。
返回值:
成功:返回 0。
失败:返回错误号。
  • 解锁后允许其他线程获取读锁或写锁。
  • 必须在每次成功调用 pthread_rwlock_rdlockpthread_rwlock_wrlock 后调用该函数来释放锁。

示例代码:

其中包括3个写线程和5个读线程对同一个全局资源进行操作。每个线程都会不定时地访问和修改这个全局资源,并通过读写锁确保线程之间的同步。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h> // 用于随机睡眠时间

#define NUM_READERS 5
#define NUM_WRITERS 3

pthread_rwlock_t rwlock;
int global_resource = 0;

void* writer(void* arg) {
    int thread_id = *(int*)arg;
    while (1) {
        // 模拟写操作
        sleep(rand() % 3); // 随机睡眠时间

        // 加写锁
        pthread_rwlock_wrlock(&rwlock);

        // 写操作
        global_resource++;
        printf("Writer %d writes: global_resource = %d\n", thread_id, global_resource);

        // 解锁
        pthread_rwlock_unlock(&rwlock);
    }
    return NULL;
}

void* reader(void* arg) {
    int thread_id = *(int*)arg;
    while (1) {
        // 模拟读操作
        sleep(rand() % 2); // 随机睡眠时间

        // 加读锁
        pthread_rwlock_rdlock(&rwlock);

        // 读操作
        printf("Reader %d reads: global_resource = %d\n", thread_id, global_resource);

        // 解锁
        pthread_rwlock_unlock(&rwlock);
    }
    return NULL;
}

int main() {
    pthread_t writers[NUM_WRITERS], readers[NUM_READERS];
    int writer_ids[NUM_WRITERS], reader_ids[NUM_READERS];
    int i;

    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);

    // 创建写线程
    for (i = 0; i < NUM_WRITERS; ++i) {
        writer_ids[i] = i + 1;
        pthread_create(&writers[i], NULL, writer, &writer_ids[i]);
    }

    // 创建读线程
    for (i = 0; i < NUM_READERS; ++i) {
        reader_ids[i] = i + 1;
        pthread_create(&readers[i], NULL, reader, &reader_ids[i]);
    }

    // 等待所有写线程结束
    for (i = 0; i < NUM_WRITERS; ++i) {
        pthread_join(writers[i], NULL);
    }

    // 等待所有读线程结束
    for (i = 0; i < NUM_READERS; ++i) {
        pthread_join(readers[i], NULL);
    }

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

    return 0;
}

4. 信号量(Semaphore)

信号量是由一个整型变量和相关的操作集合组成,用于控制对共享资源的访问。信号量可以看作是一个计数器,用于表示可用的资源数量,线程或进程在访问资源前必须首先获取信号量,访问结束后释放信号量。

主要作用包括:

  • 同步:控制多个线程或进程的执行顺序,保证在某些条件下的有序执行。
  • 互斥:保证对共享资源的访问是排他的,避免多个线程或进程同时修改资源造成的数据不一致性问题。

4.2相关操作函数

4.2.1sem_init()

初始化一个信号量 sem

int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem_t *sem:指向要初始化的信号量的指针。
int pshared:指定信号量是进程共享(非零)还是线程共享(零)。
unsigned int value:信号量的初始值,表示可用资源的数量。
返回值:
成功:返回 0。
失败:返回 -1,并设置 errno 来指示错误原因。
  • 信号量初始化后需要通过 sem_destroy 函数进行清理。
  • 如果 pshared 是非零,则信号量可以在多个进程间共享,通常使用在进程间通信(IPC)的场景中。

4.2.2sem_destroy()

销毁一个已经初始化的信号量 sem。释放由信号量占用的资源,确保在信号量不再需要时调用。

int sem_destroy(sem_t *sem);
参数:
sem_t *sem:指向要销毁的信号量的指针。
返回值:
成功:返回 0。
失败:返回 -1,并设置 errno 来指示错误原因。

4.2.3.sem_wait()

对信号量 sem 进行 P 操作(等待操作)。

int sem_wait(sem_t *sem);
参数:
sem_t *sem:指向要操作的信号量的指针。
返回值:
成功:返回 0。
失败:返回 -1,并设置 errno 来指示错误原因。
  • 如果信号量的值大于 0,将其递减;否则阻塞当前线程,直到信号量的值大于 0
  • 该函数执行时需要保证线程安全,通常与互斥锁结合使用以避免竞态条件。

4.2.4sem_trywait()

尝试对信号量 sem 进行 P 操作的非阻塞版本。与 sem_wait 不同的是,如果信号量的值为 0,立即返回而不阻塞线程。

int sem_trywait(sem_t *sem);
参数:
sem_t *sem:指向要操作的信号量的指针。
返回值:
成功:返回 0。
如果信号量的值为 0,表示资源不可用,立即返回 -1(不阻塞),并设置 errno 为 EAGAIN。
其他失败情况返回 -1,并设置 errno 来指示错误原因。

4.2.5sem_post()

对信号量 sem 进行 V 操作(释放操作)。

int sem_post(sem_t *sem);
参数:
sem_t *sem:指向要操作的信号量的指针。
返回值:
成功:返回 0。
失败:返回 -1,并设置 errno 来指示错误原因。
  • 将信号量的值递增,如果有线程因等待该信号量而被阻塞,将会唤醒其中一个线程。
  • 释放操作通常在资源使用完毕后调用,通知其他线程可以继续访问该资源。

4.2.6sem_getvalue()

获取信号量 sem 的当前值。sem_getvalue 函数可以获取信号量的当前值,而无需对其进行修改。

int sem_getvalue(sem_t *restrict sem, int *restrict sval);
参数:
sem_t *restrict sem:指向要获取值的信号量的指针。
int *restrict sval:用于存储信号量当前值的整型指针。
返回值:
成功:返回 0,并将当前信号量的值存储在 sval 中。
失败:返回 -1,并设置 errno 来指示错误原因。

示例代码:

使用信号量实现哲学家就餐问题

信号量演示代码:

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

#define NUM_THREADS 5

int shared_variable = 0;  // 共享的变量
sem_t mutex;  // 互斥信号量

void *thread_function(void *arg) {
    int thread_id = *((int *)arg);
    int local_variable = 0;
    
    while (1) {
        // 临界区开始
        sem_wait(&mutex);  // 等待互斥信号量,保证互斥访问共享资源
        local_variable = shared_variable;  // 读取共享变量到本地变量
        local_variable++;  // 修改本地变量
        sleep(rand() % 2);  // 模拟其他操作
        shared_variable = local_variable;  // 写回共享变量
        printf("线程 %d 执行后,共享变量的值为: %d\n", thread_id, shared_variable);
        sem_post(&mutex);  // 释放互斥信号量
        // 临界区结束
        
        sleep(rand() % 2 + 1);  // 模拟一段时间后再次操作
    }
    
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];
    
    // 初始化互斥信号量
    sem_init(&mutex, 0, 1);  // 初始值为1,表示资源未被占用
    
    // 创建线程
    for (int i = 0; i < NUM_THREADS; ++i) {
        thread_ids[i] = i;
        pthread_create(&threads[i], NULL, thread_function, (void *)&thread_ids[i]);
    }
    
    // 等待线程结束
    for (int i = 0; i < NUM_THREADS; ++i) {
        pthread_join(threads[i], NULL);
    }
    
    // 销毁信号量
    sem_destroy(&mutex);
    
    return 0;
}

上一篇:北京青蓝智慧科技CCRC-DCO数据合规官:国际间对人工智能治理已形成广泛共识。


下一篇:聊一聊前后端权限控制 RBAC(完整流程)-介绍