linux线程锁(互斥锁,自旋锁,读写锁)和条件变量

概述

在多线程编程中,锁和条件变量是两个常用的同步机制,用于协调多个线程之间的访问和操作。下面分别介绍锁和条件变量的基本原理、使用场景和简单代码实例。

锁(Lock)

原理

锁是一种同步机制,用于防止多个线程同时访问共享资源,以保证数据的一致性和完整性。当一个线程获取了锁之后,其他线程就无法再获取锁,只有当锁被释放后,其他线程才能获取锁并访问共享资源。

使用场景

锁的使用场景很广泛,例如:

多个线程访问共享资源时,需要保证数据的一致性和完整性;
多个线程之间需要同步,例如生产者消费者模型、读写锁等。

在 Linux 系统中,线程使用锁是保证多线程访问共享资源安全的一种方法。Linux 系统提供了多种不同类型的锁,常见的有互斥锁、读写锁和自旋锁。

1. 互斥锁(Mutex)

互斥锁也叫互斥量,是最常见的一种锁。它通过互斥的方式来保证同一时间只有一个线程能够访问被保护的共享资源。当一个线程加锁后,其他线程就必须等待该线程解锁后才能访问共享资源。

1.1 代码实例


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

#define THREAD_NUM 10

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int global_counter = 0;

void *thread_func(void *arg)
{
    
    
    int i;
    for (i = 0; i < 10000; ++i) {
    
    
        pthread_mutex_lock(&mutex);
        global_counter++;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main()
{
    
    
    int i;
    pthread_t threads[THREAD_NUM];

    for (i = 0; i < THREAD_NUM; ++i) {
    
    
        if (pthread_create(&threads[i], NULL, thread_func, NULL) != 0) {
    
    
            perror("pthread_create error");
            exit(1);
        }
    }

    for (i = 0; i < THREAD_NUM; ++i) {
    
    
        if (pthread_join(threads[i], NULL) != 0) {
    
    
            perror("pthread_join error");
            exit(1);
        }
    }

    printf("global_counter = %d\n", global_counter);
    return 0;
}

1.2 原理介绍

在 Linux 系统中,互斥锁是通过 pthread_mutex_t 结构体来实现的。当一个线程想要获取互斥锁时,它会调用 pthread_mutex_lock 函数。如果该锁没有被其他线程占用,则该线程可以获得锁,并将互斥锁的状态标记为已锁定。如果该锁已经被其他线程占用,则该线程就会被阻塞,直到锁被释放为止。当线程释放锁时,它会调用 pthread_mutex_unlock 函数,将互斥锁的状态标记为未锁定。

1.3 使用场景

互斥锁适用于对临界区进行独占式访问的情况。例如,在多个线程中访问同一份共享资源时,可以使用互斥锁来保证同一时间只有一个线程能够访问该资源。

2. 读写锁(ReadWrite Lock)

读写锁是一种特殊的锁,它将访问共享资源的线程分为两类:读线程和写线程。读线程可以同时访问共享资源,而写线程只能单独访问共享资源,即写线程独占式地访问共享资源。

2.1 代码实例


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

#define THREAD_NUM 10

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int global_counter = 0;

void *reader_func(void *arg)
{
    
    
    int i;
    for (i = 0; i < 10000; ++i) {
    
    
        pthread_rwlock_rdlock(&rwlock);
        printf("Reader %d: global_counter = %d\n", (int)arg, global_counter);
        pthread_rwlock_unlock(&rwlock);
    }
    return NULL;
}

void *writer_func(void *arg)
{
    
    
    int i;
    for (i = 0; i < 10000; ++i) {
    
    
        pthread_rwlock_wrlock(&rwlock);
        global_counter++;
        printf("Writer: global_counter = %d\n", global_counter);
        pthread_rwlock_unlock(&rwlock);
    }
    return NULL;
}

int main()
{
    
    
    int i;
    pthread_t threads[THREAD_NUM];

    // Create reader threads
    for (i = 0; i < THREAD_NUM - 1; ++i) {
    
    
        if (pthread_create(&threads[i], NULL, reader_func, (void *)(long)i) != 0) {
    
    
            perror("pthread_create error");
            exit(1);
        }
    }

    // Create writer thread
    if (pthread_create(&threads[THREAD_NUM - 1], NULL, writer_func, NULL) != 0) {
    
    
        perror("pthread_create error");
        exit(1);
    }

    // Join threads
    for (i = 0; i < THREAD_NUM; ++i) {
    
    
        if (pthread_join(threads[i], NULL) != 0) {
    
    
            perror("pthread_join error");
            exit(1);
        }
    }

    return 0;
}

2.2 原理介绍

在 Linux 系统中,读写锁是通过 pthread_rwlock_t 结构体来实现的。当一个线程想要获取读写锁时,它会调用 pthread_rwlock_rdlock 或 pthread_rwlock_wrlock 函数。如果该锁没有被其他写线程占用,则该读线程或写线程可以获得锁,并将读写锁的状态标记为已锁定。如果该锁已经被写线程占用,则该读线程或写线程就会被阻塞,直到锁被释放为止。当线程释放锁时,它会调用 pthread_rwlock_unlock 函数,将读写锁的状态标记为未锁定。

读写锁的特殊之处在于,当一个线程获得读锁时,其他读线程可以继续获得读锁,但写线程必须等待所有读线程释放锁后才能获得写锁。这样做可以有效地提高读操作的并发性,从而提高程序的性能。

2.3 使用场景

读写锁适用于读操作比写操作频繁的情况。例如,在多个线程中读取同一份共享资源时,可以使用读写锁来提高程序的并发性,
减少写线程对共享资源的争用,从而提高程序的性能。读写锁还可以用于实现一些需要读写分离的场景,如数据库的读写操作等。

下面是一些适合使用读写锁的场景:

  • 缓存:在多个线程中读取同一个缓存时,使用读写锁可以提高程序的并发性,从而提高程序的性能。
  • 数据库:在数据库中,读操作通常比写操作频繁。因此,使用读写锁可以提高并发性,从而提高程序的性能。
  • 日志:在日志系统中,写操作的频率通常比读操作高得多。因此,使用读写锁可以防止读线程被写线程长时间阻塞,从而提高程序的性能。
    配置文件:在读取配置文件时,使用读写锁可以提高程序的并发性,从而提高程序的性能。

总之,读写锁是一种非常有用的线程同步机制,适用于读操作比写操作频繁的场景,可以提高程序的并发性和性能。

3.自旋锁(Spin Lock)

自旋锁是一种忙等待锁,它不会把线程挂起,而是让线程一直占用CPU时间片,等待锁的释放。自旋锁适用于锁的持有时间很短的情况,因为如果锁的持有时间很长,那么自旋的线程就会一直占用CPU,影响系统的性能。在Linux中,自旋锁由pthread_spinlock_t类型的变量表示,可以使用pthread_spin_init()函数进行初始化,使用pthread_spin_lock()和pthread_spin_unlock()函数进行加锁和解锁。

代码示例:

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

pthread_spinlock_t spinlock;
int shared_data = 0;

void* thread_func(void* arg) {
    
    
    pthread_spin_lock(&spinlock);
    shared_data++;
    printf("Thread %d is modifying the shared data: %d\n", *(int*)arg, shared_data);
    pthread_spin_unlock(&spinlock);
    return NULL;
}

int main() {
    
    
    pthread_t threads[5];
    int args[5] = {
    
    1, 2, 3, 4, 5};
    pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);
    for (int i = 0; i < 5; i++) {
    
    
        pthread_create(&threads[i], NULL, thread_func, &args[i]);
    }
    for (int i = 0; i < 5; i++) {
    
    
        pthread_join(threads[i], NULL);
    }
    pthread_spin_destroy(&spinlock);
    return 0;
}

在这个例子中,我们创建了5个线程,它们都会访问一个共享数据,即一个整数。我们使用自旋锁来保护这个共享数据,确保同一时间只有一个线程可以访问它。在每个线程函数中,我们使用pthread_spin_lock()函数来获得自旋锁,在修改共享数据后输出信息,并使用pthread_spin_unlock()函数来释放锁。

应用场景

自旋锁的应用场景:

多个线程访问共享资源,且访问时间短;
多个线程访问共享资源,且线程数不多,避免上下文切换带来的开销。

条件变量(Condition Variable)

原理

条件变量是一种同步机制,用于在多个线程之间传递信息,以便协调它们的行为。条件变量通常与锁一起使用,以便在共享资源上等待特定条件的出现。

当一个线程需要等待某个条件成立时,它可以调用条件变量的等待函数(如 pthread_cond_wait()),将当前线程阻塞,并将锁释放,以便其他线程能够访问共享资源。当条件成立时,其他线程会通过条件变量的通知函数(如 pthread_cond_signal() 或 pthread_cond_broadcast())来通知等待的线程,从而唤醒它们并重新获取锁。

使用场景

条件变量通常与锁一起使用,用于解决多个线程之间的同步问题。例如:

  • 多个线程需要等待某个事件的发生,例如生产者消费者模型;
  • 多个线程需要协调执行某个任务,例如线程池;
  • 多个线程需要按照某种顺序执行,例如读写锁。
    简单代码实例
    在 Linux 系统中,使用 pthread 库提供的 pthread_cond_t 结构体来实现条件变量。下面是一个简单的代码实例,用于演示条件变量的基本用法:
#include <stdio.h>
#include <pthread.h>

pthread_mutex_t mutex; // 定义互斥锁
pthread_cond_t cond; // 定义条件变量
int count = 0; // 共享变量

void *producer(void *arg) {
    
    
    while (1) {
    
    
        pthread_mutex_lock(&mutex); // 加锁
        count++;
        printf("produce one item, total %d items\n", count);
        pthread_cond_signal(&cond); // 发送信号
        pthread_mutex_unlock(&mutex); // 解锁
        sleep(1); // 模拟生产过程
    }
    return NULL;
}

void *consumer(void *arg) {
    
    
    while (1) {
    
    
        pthread_mutex_lock(&mutex); // 加锁
        while (count == 0) {
    
     // 检查条件
            pthread_cond_wait(&cond, &mutex); // 等待信号
        }
        count--;
        printf("consume one item, total %d items\n", count);
        pthread_mutex_unlock(&mutex); // 解锁
        sleep(1); // 模拟消费过程
    }
    return NULL;
}

int main() {
    
    
    pthread_t tid1, tid2; // 定义线程 ID
    pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
    pthread_cond_init(&cond, NULL); // 初始化条件变量
    pthread_create(&tid1, NULL, producer, NULL); // 创建生产者线程
    pthread_create(&tid2, NULL, consumer, NULL); // 创建消费者线程
    pthread_join(tid1, NULL); // 等待生产者线程结束
    pthread_join(tid2, NULL); // 等待消费者线程结束
    pthread_mutex_destroy(&mutex); // 销毁互斥锁
    pthread_cond_destroy(&cond); // 销毁条件变量
    return 0;
}

在这个例子中,我们定义了一个互斥锁 mutex 和一个条件变量 cond,并分别在生产者线程和消费者线程中使用。在生产者线程中,我们不断地生产物品,并发送信号通知消费者线程;在消费者线程中,我们首先获取互斥锁,然后检查共享变量 count 是否为 0,如果是,则等待信号通知;如果不是,则消费一个物品,并释放互斥锁。

需要注意的是,我们使用了 pthread_cond_wait 函数来等待条件变量的信号,该函数会自动释放互斥锁,并将线程置于等待状态。在信号到达后,该函数会自动重新获取互斥锁,然后继续执行下面的代码。

流程

使用条件变量的一般流程如下:

1、初始化互斥锁和条件变量,例如 pthread_mutex_init 和 pthread_cond_init 函数;
2、在需要等待条件变量的线程中获取互斥锁,并使用 pthread_cond_wait 函数等待条件变量的信号;
3、在需要发送条件变量信号的线程中获取互斥锁,并使用 pthread_cond_signal 或 pthread_cond_broadcast 函数发送信号;
4、在线程退出前,使用 pthread_mutex_destroy 和 pthread_cond_destroy 函数销毁互斥锁和条件变量。

注意事项

使用条件变量需要注意以下几点:

1、线程使用条件变量等待信号时会自动释放互斥锁,因此需要在条件判断之前获取互斥锁,并在条件满足之后重新获取互斥锁;
2、发送条件变量信号时需要获取互斥锁,并在发送信号之后释放互斥锁,以便等待线程能够获取互斥锁并检查条件;
3、在使用条件变量之前,需要先初始化互斥锁和条件变量,并在使用结束后销毁它们;
4、在使用条件变量时需要确保共享变量的一致性,避免出现竞态条件。

参考资料

Linux 多线程编程实战
Pthread Condition Variables

猜你喜欢

转载自blog.csdn.net/qq_46017342/article/details/129779113
今日推荐