线程同步--互斥锁,读写锁

线程同步

基本概念

线程的能力在于能够方便地通过全局变量或共享内存来交换信息,但这也带来了并发控制的复杂性,主要表现在如何安全地管理多个线程对共享资源的访问。这里涉及到几个关键的概念和技术:

临界区(Critical Section)

临界区是指那些访问共享资源(如全局变量、文件等)的代码段。为保证程序的正确性和避免数据竞争,这些代码段的执行必须是原子的,即在执行完整个代码段之前,不应被其他线程打断管理临界区的方法主要包括使用互斥锁(mutexes)、信号量(semaphores)、读写锁(read-write locks)等同步机制

线程同步

线程同步是指使线程以一种安全和确定的方式访问共享资源的各种机制。主要同步技术包括:

  1. 互斥锁(Mutex)

    • 用于保证同时只有一个线程可以进入临界区。线程在进入临界区之前必须获取锁,退出临界区时释放锁。如果锁已被其他线程占用,线程将阻塞直到锁被释放。
  2. 信号量(Semaphores)

    • 用于控制对共享资源的访问。信号量有一个计数器,表示可用资源的数量。线程在访问资源前需通过调用wait()操作来减少计数器,若计数器为零,则线程阻塞直到资源变为可用。完成资源访问后,线程调用signal()操作增加计数器。
  3. 条件变量(Condition Variables)

    • 允许线程在某些条件下阻塞,并在这些条件改变时被唤醒。通常与互斥锁配合使用,用于线程之间的协调和状态同步。
  4. 读写锁(Read-Write Locks)

    • 允许多个读操作同时进行,但写操作需要独占访问。这种锁是优化读取操作频繁而写入操作较少的场景。

实践中的应用

使用这些同步技术时,必须注意避免死锁、活锁和饥饿等问题。死锁发生时,多个线程相互等待对方持有的锁,从而无法继续执行。活锁是指线程虽然没有阻塞,但仍然无法向前推进,因为不断重试失败的操作。饥饿发生在某些线程无法获得必需的系统资源。

合理地使用线程同步技术可以显著提高程序在多核处理器上的性能,并发确保数据的一致性和完整性。在设计多线程程序时,开发者应当充分考虑这些因素,确保应用程序的稳定性和效率。如果需要具体的示例或进一步的解释,随时欢迎提问!

案例
/*
    使用多线程实现买票的案例。
    有3个窗口,一共是100张票。
*/

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

// 全局变量,所有的线程都共享这一份资源。
int tickets = 100;

void * sellticket(void * arg) {
    // 卖票
    while(tickets > 0) {
        usleep(6000);
        printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
        tickets--;
    }
    return NULL;
}

int main() {

    // 创建3个子线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, sellticket, NULL);
    pthread_create(&tid2, NULL, sellticket, NULL);
    pthread_create(&tid3, NULL, sellticket, NULL);

    // 回收子线程的资源,阻塞
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    // 设置线程分离。
    // pthread_detach(tid1);
    // pthread_detach(tid2);
    // pthread_detach(tid3);

    pthread_exit(NULL); // 退出主线程

    return 0;
}
daic@daic:~/Linux/linuxwebserver/part03Multithreading/lesson30$ gcc selltickets.c -lpthread
daic@daic:~/Linux/linuxwebserver/part03Multithreading/lesson30$ ./a.out 
139630428940032 正在卖第 100 张门票
139630420547328 正在卖第 100 张门票
139630437332736 正在卖第 100 张门票
139630420547328 正在卖第 97 张门票
139630428940032 正在卖第 97 张门票
139630437332736 正在卖第 95 张门票
139630420547328 正在卖第 94 张门票
139630428940032 正在卖第 93 张门票
139630437332736 正在卖第 92 张门票
139630420547328 正在卖第 91 张门票
139630437332736 正在卖第 90 张门票
139630428940032 正在卖第 91 张门票
139630420547328 正在卖第 88 张门票
139630437332736 正在卖第 87 张门票
139630428940032 正在卖第 86 张门票
139630420547328 正在卖第 85 张门票
139630437332736 正在卖第 84 张门票
139630428940032 正在卖第 83 张门票
139630420547328 正在卖第 82 张门票
139630428940032 正在卖第 81 张门票
139630437332736 正在卖第 80 张门票
139630420547328 正在卖第 79 张门票
139630428940032 正在卖第 78 张门票
139630437332736 正在卖第 77 张门票
139630420547328 正在卖第 76 张门票
139630428940032 正在卖第 75 张门票
139630437332736 正在卖第 74 张门票
139630420547328 正在卖第 73 张门票
139630428940032 正在卖第 72 张门票
139630437332736 正在卖第 71 张门票
139630420547328 正在卖第 70 张门票
139630428940032 正在卖第 69 张门票
139630437332736 正在卖第 68 张门票
139630420547328 正在卖第 67 张门票
139630428940032 正在卖第 66 张门票
139630437332736 正在卖第 65 张门票
139630420547328 正在卖第 64 张门票
139630437332736 正在卖第 63 张门票
139630428940032 正在卖第 62 张门票
139630420547328 正在卖第 61 张门票
139630437332736 正在卖第 60 张门票
139630428940032 正在卖第 59 张门票
139630420547328 正在卖第 58 张门票
139630428940032 正在卖第 57 张门票
139630437332736 正在卖第 56 张门票
139630420547328 正在卖第 55 张门票
139630428940032 正在卖第 54 张门票
139630437332736 正在卖第 53 张门票
139630420547328 正在卖第 52 张门票
139630428940032 正在卖第 51 张门票
139630437332736 正在卖第 50 张门票
139630420547328 正在卖第 49 张门票
139630428940032 正在卖第 48 张门票
139630437332736 正在卖第 47 张门票
139630420547328 正在卖第 46 张门票
139630428940032 正在卖第 45 张门票
139630437332736 正在卖第 44 张门票
139630420547328 正在卖第 43 张门票
139630428940032 正在卖第 42 张门票
139630437332736 正在卖第 41 张门票
139630420547328 正在卖第 40 张门票
139630428940032 正在卖第 39 张门票
139630437332736 正在卖第 38 张门票
139630420547328 正在卖第 37 张门票
139630428940032 正在卖第 36 张门票
139630437332736 正在卖第 35 张门票
139630420547328 正在卖第 34 张门票
139630428940032 正在卖第 33 张门票
139630437332736 正在卖第 32 张门票
139630420547328 正在卖第 31 张门票
139630428940032 正在卖第 30 张门票
139630437332736 正在卖第 29 张门票
139630420547328 正在卖第 28 张门票
139630428940032 正在卖第 27 张门票
139630437332736 正在卖第 26 张门票
139630428940032 正在卖第 25 张门票
139630420547328 正在卖第 24 张门票
139630437332736 正在卖第 23 张门票
139630420547328 正在卖第 22 张门票
139630428940032 正在卖第 21 张门票
139630437332736 正在卖第 20 张门票
139630420547328 正在卖第 19 张门票
139630428940032 正在卖第 19 张门票
139630437332736 正在卖第 17 张门票
139630420547328 正在卖第 16 张门票
139630428940032 正在卖第 16 张门票
139630437332736 正在卖第 14 张门票
139630420547328 正在卖第 13 张门票
139630428940032 正在卖第 12 张门票
139630437332736 正在卖第 11 张门票
139630420547328 正在卖第 10 张门票
139630428940032 正在卖第 9 张门票
139630437332736 正在卖第 8 张门票
139630420547328 正在卖第 7 张门票
139630428940032 正在卖第 6 张门票
139630437332736 正在卖第 5 张门票
139630420547328 正在卖第 4 张门票
139630428940032 正在卖第 3 张门票
139630437332736 正在卖第 2 张门票
139630420547328 正在卖第 1 张门票
139630428940032 正在卖第 0 张门票
139630437332736 正在卖第 -1 张门票

互斥锁

互斥量

在多线程编程中,互斥量是同步原语的一种,非常关键于防止所谓的“竞态条件”(race conditions),即多个线程同时访问和修改同一共享资源而引起的不可预测结果。

互斥量的工作原理

  • 状态: 互斥量有两种状态,已锁定(locked)和未锁定(unlocked)。
  • 操作: 主要操作包括锁定(lock)和解锁(unlock)。
  • 所有权: 只有锁定了互斥量的线程才能解锁它,这意味着锁的操作是关联线程所有权的。

使用互斥量的基本模式

当线程需要访问受互斥量保护的共享资源时,它会:

  1. 锁定互斥量: 确保如果其他线程已经锁定了互斥量,当前线程将等待(或阻塞)直到互斥量变为未锁定状态。
  2. 访问共享资源: 在安全的环境中进行操作,因为其他线程将无法同时访问这些资源。
  3. 解锁互斥量: 允许其他线程可以锁定互斥量并访问同一资源。

如果多个线程试图执行这一块代码(一个临界区),事实上只有一个线程能够持有该互斥

量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如下图所示:

image-20240511061940228

互斥量相关操作函数

互斥量的类型 pthread_mutex_t
pthread_mutex_init

函数 pthread_mutex_init 用于初始化互斥量,是 POSIX 线程库中的一个重要函数。初始化是在互斥量使用之前必需的步骤,以确保互斥量在首次使用时处于已知的状态。

函数原型
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
参数说明
  • mutex:指向将要被初始化的 pthread_mutex_t 结构的指针。
  • attr:指向互斥量属性的指针。这个属性可以用来设置互斥量的类型(如普通、递归、错误检查等),如果传递 NULL,则使用默认属性。
返回值
  • 0:成功。
  • 错误码:在失败时返回一个错误码,而不是设置 errno
错误码
  • EINVAL:传递了无效的属性。
  • ENOMEM:没有足够的内存来初始化互斥量。
使用示例

下面是一个简单的示例,展示如何初始化一个默认属性的互斥量,并使用它来同步两个线程的操作:

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

pthread_mutex_t lock;

void* function(void* arg) {
    pthread_mutex_lock(&lock);
    printf("Thread %ld is running\n", (long)arg);
    pthread_mutex_unlock(&lock);
    return NULL;
}

int main() {
    pthread_t t1, t2;

    // 初始化互斥量
    pthread_mutex_init(&lock, NULL);

    // 创建两个线程
    pthread_create(&t1, NULL, function, (void*)1);
    pthread_create(&t2, NULL, function, (void*)2);

    // 等待线程结束
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

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

    return 0;
}

在这个示例中,我们创建了一个互斥量 lock,并在两个线程中使用它来同步对 printf 函数的调用,确保在同一时刻只有一个线程可以执行 printf

注意事项

在使用完互斥量后,应调用 pthread_mutex_destroy 来释放任何可能由 pthread_mutex_init 分配的资源。

如果互斥量正在被使用(即处于锁定状态),则尝试初始化互斥量可能会导致未定义行为。因此,初始化应在创建任何使用它的线程之前完成。

正确地初始化互斥量对于确保程序的线程安全至关重要。

pthread_mutex_destroy

函数 pthread_mutex_destroy 用于销毁已经初始化的互斥量,释放它可能占用的资源。在多线程编程中,正确地初始化和销毁互斥量是保证资源正确管理的重要一环。

函数原型
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数说明
  • mutex:指向将要被销毁的 pthread_mutex_t 结构的指针。
返回值
  • 0:成功。
  • 错误码:如果函数调用失败,将返回一个非零的错误码。
错误码
  • EBUSY:尝试销毁一个正在被锁定或正在被其他线程等待的互斥量。在销毁互斥量之前,必须确保没有线程正在使用它。
  • EINVAL:传递给函数的互斥量指针无效。
注意事项
  • 在销毁互斥量之前,确保互斥量没有被锁定。如果互斥量被某个线程持有或其他线程正在等待锁定该互斥量,那么尝试销毁互斥量可能导致 EBUSY 错误。
  • 通常,pthread_mutex_destroy() 应该在所有使用互斥量的线程结束后调用,以确保所有线程均不会再访问该互斥量。
  • 正确的互斥量管理(初始化、使用、销毁)对于避免资源泄露和潜在的死锁至关重要。

确保遵循这些最佳实践可以帮助维持程序的稳定性和可靠性,尤其是在涉及多线程操作的环境中。

pthread_mutex_lock

pthread_mutex_lock 是 POSIX 线程库中的一个函数,用于在多线程程序中锁定互斥量(mutex)。当一个线程调用此函数时,它将尝试获取指定互斥量的所有权。如果互斥量已经被另一个线程锁定,调用线程将被阻塞,直到互斥量变为可用。这是确保对共享资源访问的互斥(排他性访问)的基本机制。

函数原型
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数说明
  • mutex:指向互斥量的指针,该互斥量之前应已通过 pthread_mutex_init 初始化。
返回值
  • 0:成功锁定了互斥量。
  • 错误码:如果函数调用失败,将返回一个非零的错误码。
常见的错误码
  • EINVAL:传递给函数的互斥量指针无效。
  • EDEADLK:如果互斥量已经被当前线程锁定,并且互斥量是不允许递归锁定的,尝试重新锁定可能会返回此错误。
注意事项
  • 使用互斥量时,确保在访问任何受保护的共享资源前锁定互斥量,并在完成访问后释放(解锁)互斥量。
  • 避免在持有互斥量时执行长时间操作或可能导致线程阻塞的调用,以减少对其他线程的影响。
  • 要注意死锁的问题,特别是在多个互斥量涉及的情况下。确保以一致的顺序获取互斥量,避免循环等待条件的出现。

通过这种方式,互斥量帮助实现线程之间的同步,保证程序在并发环境中的正确性和效率。

pthread_mutex_trylock

函数 pthread_mutex_trylock 是 POSIX 线程库中的一个用于尝试锁定互斥量的函数,它与 pthread_mutex_lock 相似,但与后者不同的是,pthread_mutex_trylock 在无法立即获取互斥量时不会阻塞调用线程。这使得它成为避免潜在死锁和减少等待时间的有用工具。

函数原型
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数说明
  • mutex:指向互斥量的指针,该互斥量应已通过 pthread_mutex_init 初始化。
返回值
  • 0:成功锁定了互斥量。
  • EBUSY:互斥量已被另一个线程锁定,函数立即返回。
  • EINVAL:传递给函数的互斥量指针无效。
使用场景

pthread_mutex_trylock 非常适合于那些不希望线程在无法获取互斥量时长时间阻塞的场景,例如:

  • 在实时计算中,线程需要快速做出决策,不能因等待互斥量而延误。
  • 在尝试获取多个互斥量时,用于避免死锁,特别是当你无法保证所有线程都以相同顺序请求互斥量时。
示例代码

下面是一个简单的示例,展示如何使用 pthread_mutex_trylock 来尝试锁定互斥量,并根据返回值处理不同的情况:

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

pthread_mutex_t mutex;

void* trylock_thread(void* arg) {
    int trylock_result = pthread_mutex_trylock(&mutex);
    if (trylock_result == 0) {
        printf("Thread %ld: Got the lock\n", (long)arg);
        // 对共享资源进行操作
        pthread_mutex_unlock(&mutex);
    } else if (trylock_result == EBUSY) {
        printf("Thread %ld: Mutex is already locked by another thread\n", (long)arg);
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;

    // 初始化互斥量
    pthread_mutex_init(&mutex, NULL);

    // 创建两个线程
    pthread_create(&t1, NULL, trylock_thread, (void*)1);
    pthread_create(&t2, NULL, trylock_thread, (void*)2);

    // 等待线程完成
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

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

    return 0;
}
注意事项
  • pthread_mutex_trylock 可以帮助你设计非阻塞的同步策略,但需要谨慎处理返回值,确保程序逻辑的正确性。
  • 使用此函数时,应考虑适当的错误处理和替代逻辑,特别是在获取锁失败时的操作。
  • 和所有互斥量操作一样,保证每次成功的 pthread_mutex_trylock 调用后都要有相应的 pthread_mutex_unlock 调用来释放互斥量。

通过合理使用 pthread_mutex_trylock,可以增加程序的响应速度和灵活性,同时避免因锁等待导致的性能瓶颈。

pthread_mutex_unlock

函数 pthread_mutex_unlock 是 POSIX 线程库中用于解锁互斥量的函数。这个函数通常在一个线程完成对受保护的共享资源的操作后调用,以释放互斥量,允许其他线程可以锁定此互斥量并访问相同的资源。解锁操作是保证多线程程序中资源共享和线程协作的重要环节。

函数原型
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数说明
  • mutex:指向需要被解锁的互斥量的指针。
返回值
  • 0:成功解锁互斥量。
  • 错误码:如果函数调用失败,将返回一个非零的错误码。
常见的错误码
  • EINVAL:传递给函数的互斥量指针无效。
  • EPERM:当前线程不是互斥量的所有者,尝试解锁它将返回这个错误。
注意事项
  • 使用 pthread_mutex_unlock 时必须确保当前线程确实是互斥量的所有者,否则会返回错误。
  • 通常情况下,每一个 pthread_mutex_lock 调用都应该对应一个 pthread_mutex_unlock 调用,以避免造成死锁。
  • 在设计使用互斥量的代码时,需要谨慎处理可能导致提前退出函数(如通过 return 语句或异常处理)的逻辑,确保即使在这些情况下互斥量也能被正确解锁。

通过合理使用互斥量的锁定与解锁操作,可以有效管理线程之间对共享资源的访问,保障数据的完整性和程序的稳定运行。

案例
/*
    互斥量的类型 pthread_mutex_t
    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
        - 初始化互斥量
        - 参数 :
            - mutex : 需要初始化的互斥量变量
            - attr : 互斥量相关的属性,NULL
        - restrict : C语言的修饰符,被修饰的指针,不能由另外的一个指针进行操作。
            pthread_mutex_t *restrict mutex = xxx;
            pthread_mutex_t * mutex1 = mutex;

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
        - 释放互斥量的资源

    int pthread_mutex_lock(pthread_mutex_t *mutex);
        - 加锁,阻塞的,如果有一个线程加锁了,那么其他的线程只能阻塞等待

    int pthread_mutex_trylock(pthread_mutex_t *mutex);
        - 尝试加锁,如果加锁失败,不会阻塞,会直接返回。

    int pthread_mutex_unlock(pthread_mutex_t *mutex);
        - 解锁
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 全局变量,所有的线程都共享这一份资源。
int tickets = 100;

// 创建一个互斥量,放在全局区
pthread_mutex_t mutex;

void * sellticket(void * arg) {

    // 卖票
    while(1) {

        // 加锁
        pthread_mutex_lock(&mutex);
        //临界区
        if(tickets > 0) {
            usleep(6000);
            printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
            tickets--;
        }else {
            // 解锁
            pthread_mutex_unlock(&mutex);
            break;
        }

        // 解锁
        pthread_mutex_unlock(&mutex);
    }

    

    return NULL;
}

int main() {

    // 初始化互斥量
    pthread_mutex_init(&mutex, NULL);

    // 创建3个子线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, sellticket, NULL);
    pthread_create(&tid2, NULL, sellticket, NULL);
    pthread_create(&tid3, NULL, sellticket, NULL);

    // 回收子线程的资源,阻塞
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    pthread_exit(NULL); // 退出主线程

    // 释放互斥量资源
    pthread_mutex_destroy(&mutex);

    return 0;
}
daic@daic:~/Linux/linuxwebserver/part03Multithreading/lesson30$ gcc mutex.c -lpthread
daic@daic:~/Linux/linuxwebserver/part03Multithreading/lesson30$ ./a.out 
140100915386112 正在卖第 100 张门票
140100915386112 正在卖第 99 张门票
140100915386112 正在卖第 98 张门票
140100915386112 正在卖第 97 张门票
140100915386112 正在卖第 96 张门票
140100915386112 正在卖第 95 张门票
140100915386112 正在卖第 94 张门票
140100915386112 正在卖第 93 张门票
140100915386112 正在卖第 92 张门票
140100915386112 正在卖第 91 张门票
140100915386112 正在卖第 90 张门票
140100915386112 正在卖第 89 张门票
140100915386112 正在卖第 88 张门票
140100915386112 正在卖第 87 张门票
140100915386112 正在卖第 86 张门票
140100915386112 正在卖第 85 张门票
140100915386112 正在卖第 84 张门票
140100915386112 正在卖第 83 张门票
140100915386112 正在卖第 82 张门票
140100915386112 正在卖第 81 张门票
140100915386112 正在卖第 80 张门票
140100915386112 正在卖第 79 张门票
140100915386112 正在卖第 78 张门票
140100915386112 正在卖第 77 张门票
140100915386112 正在卖第 76 张门票
140100915386112 正在卖第 75 张门票
140100915386112 正在卖第 74 张门票
140100915386112 正在卖第 73 张门票
140100915386112 正在卖第 72 张门票
140100915386112 正在卖第 71 张门票
140100915386112 正在卖第 70 张门票
140100915386112 正在卖第 69 张门票
140100915386112 正在卖第 68 张门票
140100915386112 正在卖第 67 张门票
140100915386112 正在卖第 66 张门票
140100915386112 正在卖第 65 张门票
140100915386112 正在卖第 64 张门票
140100915386112 正在卖第 63 张门票
140100915386112 正在卖第 62 张门票
140100915386112 正在卖第 61 张门票
140100915386112 正在卖第 60 张门票
140100915386112 正在卖第 59 张门票
140100915386112 正在卖第 58 张门票
140100915386112 正在卖第 57 张门票
140100915386112 正在卖第 56 张门票
140100915386112 正在卖第 55 张门票
140100915386112 正在卖第 54 张门票
140100915386112 正在卖第 53 张门票
140100915386112 正在卖第 52 张门票
140100915386112 正在卖第 51 张门票
140100915386112 正在卖第 50 张门票
140100915386112 正在卖第 49 张门票
140100915386112 正在卖第 48 张门票
140100915386112 正在卖第 47 张门票
140100915386112 正在卖第 46 张门票
140100915386112 正在卖第 45 张门票
140100915386112 正在卖第 44 张门票
140100915386112 正在卖第 43 张门票
140100915386112 正在卖第 42 张门票
140100915386112 正在卖第 41 张门票
140100915386112 正在卖第 40 张门票
140100915386112 正在卖第 39 张门票
140100915386112 正在卖第 38 张门票
140100915386112 正在卖第 37 张门票
140100915386112 正在卖第 36 张门票
140100915386112 正在卖第 35 张门票
140100915386112 正在卖第 34 张门票
140100915386112 正在卖第 33 张门票
140100915386112 正在卖第 32 张门票
140100915386112 正在卖第 31 张门票
140100915386112 正在卖第 30 张门票
140100915386112 正在卖第 29 张门票
140100915386112 正在卖第 28 张门票
140100915386112 正在卖第 27 张门票
140100915386112 正在卖第 26 张门票
140100915386112 正在卖第 25 张门票
140100915386112 正在卖第 24 张门票
140100898600704 正在卖第 23 张门票
140100898600704 正在卖第 22 张门票
140100898600704 正在卖第 21 张门票
140100898600704 正在卖第 20 张门票
140100898600704 正在卖第 19 张门票
140100898600704 正在卖第 18 张门票
140100898600704 正在卖第 17 张门票
140100898600704 正在卖第 16 张门票
140100898600704 正在卖第 15 张门票
140100898600704 正在卖第 14 张门票
140100898600704 正在卖第 13 张门票
140100898600704 正在卖第 12 张门票
140100898600704 正在卖第 11 张门票
140100898600704 正在卖第 10 张门票
140100898600704 正在卖第 9 张门票
140100898600704 正在卖第 8 张门票
140100898600704 正在卖第 7 张门票
140100898600704 正在卖第 6 张门票
140100898600704 正在卖第 5 张门票
140100898600704 正在卖第 4 张门票
140100898600704 正在卖第 3 张门票
140100898600704 正在卖第 2 张门票
140100898600704 正在卖第 1 张门票

死锁

死锁的常见场景

  1. 忘记释放锁
    • 描述:一个线程在获取锁之后,由于程序逻辑的问题,忘记释放锁,导致其他线程无限期地等待该锁。
    • 预防:确保每个pthread_mutex_lock调用都在函数结束前有对应的pthread_mutex_unlock调用,无论是通过正常路径还是错误处理路径。
  2. 重复加锁
    • 描述:一个线程试图对同一个非递归锁加锁多次,导致该线程自身陷入等待状态。
    • 预防:避免设计需要重复加锁同一互斥量的逻辑,或使用递归互斥量(允许同一线程多次加锁)。
  3. 多线程多锁
    • 描述:多个线程以不同的顺序获取多个互斥量,导致相互等待对方释放锁。
    • 预防:设计一个固定的锁获取顺序,所有线程必须按此顺序获取锁。例如,总是先锁定互斥量A,然后是互斥量B。

如何解决和预防死锁

  1. 锁的顺序获取
    • 一致的锁获取顺序可以防止环形等待条件的发生,这是死锁的四个必要条件之一。
  2. 检测并避免
    • 在设计时进行仔细的代码审核和测试,以检测可能的死锁情况。使用工具如Valgrind中的Helgrind,或使用静态分析工具来识别死锁风险。
  3. 超时机制
    • 使用带超时的锁获取尝试,如pthread_mutex_trylock或者使用具备超时功能的同步原语,如pthread_mutex_timedlock。如果锁在指定时间内未能获取,线程可以解锁已持有的其他锁,然后重试或回滚操作。
  4. 资源分配图
    • 在复杂的应用中,创建资源分配图来分析资源分配和锁定模式,从而识别潜在的死锁。
  5. 使用锁层次结构
    • 通过定义每个锁的层次,并且在代码中严格按层次获取锁,可以有效防止死锁。
案例1
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 全局变量,所有的线程都共享这一份资源。
int tickets = 1000;

// 创建一个互斥量
pthread_mutex_t mutex;

void * sellticket(void * arg) {

    // 卖票
    while(1) {

        // 加锁
        pthread_mutex_lock(&mutex);
        pthread_mutex_lock(&mutex);

        if(tickets > 0) {
            usleep(6000);
            printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
            tickets--;
        }else {
            // 解锁
            pthread_mutex_unlock(&mutex);
            break;
        }

        // 解锁
        pthread_mutex_unlock(&mutex);
        pthread_mutex_unlock(&mutex);
    }

    return NULL;
}

int main() {

    // 初始化互斥量
    pthread_mutex_init(&mutex, NULL);

    // 创建3个子线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, sellticket, NULL);
    pthread_create(&tid2, NULL, sellticket, NULL);
    pthread_create(&tid3, NULL, sellticket, NULL);

    // 回收子线程的资源,阻塞
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    pthread_exit(NULL); // 退出主线程

    // 释放互斥量资源
    pthread_mutex_destroy(&mutex);

    return 0;
}
案例2
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 创建2个互斥量
pthread_mutex_t mutex1, mutex2;

void * workA(void * arg) {

    pthread_mutex_lock(&mutex1);
    sleep(1); 
    pthread_mutex_lock(&mutex2);

    printf("workA....\n");

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}


void * workB(void * arg) {
    pthread_mutex_lock(&mutex2);
    sleep(1);
    pthread_mutex_lock(&mutex1);

    printf("workB....\n");

    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);

    return NULL;
}

int main() {

    // 初始化互斥量
    pthread_mutex_init(&mutex1, NULL);
    pthread_mutex_init(&mutex2, NULL);

    // 创建2个子线程
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, workA, NULL);
    pthread_create(&tid2, NULL, workB, NULL);

    // 回收子线程资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    // 释放互斥量资源
    pthread_mutex_destroy(&mutex1);
    pthread_mutex_destroy(&mutex2);

    return 0;
}

读写锁

为什么在某些应用中使用传统的互斥锁(mutex)可能不是最优选择,特别是在读操作远多于写操作的场合。这种场合下,读写锁(Read-Write Locks)或共享-独占锁(shared-exclusive locks)就显得非常有用。

读写锁的概念和特性

读写锁允许多个线程同时读共享资源,但保证只有一个线程可以写。它们解决了互斥锁在读多写少的应用场景中引起的性能瓶颈。主要特点包括:

  • 共享读:当没有线程持有读写锁用于写入时,多个线程可以同时持有读写锁用于读取。
  • 独占写:写操作需要独占访问,即当一个线程获取读写锁进行写操作时,其他线程无论是读还是写都必须等待。
  • 写优先:通常实现读写锁时会给写操作以较高的优先级,这防止写操作饥饿(长时间等待),尤其是在读操作非常频繁的情况下。

读写锁相关函数

读写锁的类型 pthread_rwlock_t

你提供的函数是 POSIX 线程(Pthreads)库中关于读写锁的操作函数。读写锁允许多个线程同时读取同一个共享资源,但在写入时要求独占访问。以下是各个函数的详细说明和用法:

pthread_rwlock_init
  • 功能:初始化一个读写锁。

  • 原型

    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
    
  • 参数

    • rwlock:指向将要初始化的读写锁的指针。
    • attr:指向读写锁属性的指针,可以设为 NULL 以使用默认属性。
  • 返回值:成功返回0;出错返回错误码。

pthread_rwlock_destroy
  • 功能:销毁一个读写锁。

  • 原型

    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
    
  • 参数

    • rwlock:指向需要销毁的读写锁的指针。
  • 返回值:成功返回0;出错返回错误码。

pthread_rwlock_rdlock
  • 功能:以读模式锁定读写锁。

  • 原型

    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    
  • 参数

    • rwlock:指向要锁定的读写锁的指针。
  • 返回值:成功返回0;出错返回错误码。

pthread_rwlock_tryrdlock
  • 功能:尝试以读模式锁定读写锁。

  • 原型

    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
    
  • 参数

    • rwlock:指向要尝试锁定的读写锁的指针。
  • 返回值:成功返回0;如果锁已经被其他线程以写模式锁定,返回EBUSY;其他错误返回相应的错误码。

pthread_rwlock_wrlock
  • 功能:以写模式锁定读写锁。

  • 原型

    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    
  • 参数

    • rwlock:指向要锁定的读写锁的指针。
  • 返回值:成功返回0;出错返回错误码。

pthread_rwlock_trywrlock
  • 功能:尝试以写模式锁定读写锁。

  • 原型

    int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
    
  • 参数

    • rwlock:指向要尝试锁定的读写锁的指针。
  • 返回值:成功返回0;如果锁已经被其他线程以读模式或写模式锁定,返回EBUSY;其他错误返回相应的错误码。

pthread_rwlock_unlock
  • 功能:解锁读写锁。

  • 原型

    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
    
  • 参数

    • rwlock:指向要解锁的读写锁的指针。
  • 返回值:成功返回0;出错返回错误码。

使用示例

下面是一个使用读写锁的简单示例,展示了如何在多个线程中使用读写锁来同步对共享资源的访问:

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

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int shared_data = 0;

void* reader_function(void* arg) {
    pthread_rwlock_rdlock(&rwlock);
    printf("Reader: shared_data = %d\n", shared_data);
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

void* writer_function(void* arg) {
    pthread_rwlock_wrlock(&rwlock);
    shared_data++;
    printf("Writer: incremented shared_data to %d\n", shared_data);
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

int main

() {
    pthread_t reader_thread, writer_thread;
    
    pthread_rwlock_init(&rwlock, NULL);
    pthread_create(&reader_thread, NULL, reader_function, NULL);
    pthread_create(&writer_thread, NULL, writer_function, NULL);

    pthread_join(reader_thread, NULL);
    pthread_join(writer_thread, NULL);

    pthread_rwlock_destroy(&rwlock);
    return 0;
}

这个示例展示了如何初始化、使用和销毁读写锁,并说明了如何在读者和写者之间同步访问共享数据。通过使用读写锁,可以有效提升程序在多线程环境下处理读多写少情况的性能。

案例
/*
    读写锁的类型 pthread_rwlock_t
    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

    案例:8个线程操作同一个全局变量。
    3个线程不定时写这个全局变量,5个线程不定时的读这个全局变量
*/

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

// 创建一个共享数据
int num = 1;
// pthread_mutex_t mutex;
pthread_rwlock_t rwlock;

void * writeNum(void * arg) {

    while(1) {
        pthread_rwlock_wrlock(&rwlock);
        num++;
        printf("++write, tid : %ld, num : %d\n", pthread_self(), num);
        pthread_rwlock_unlock(&rwlock);
        usleep(100);
    }

    return NULL;
}

void * readNum(void * arg) {

    while(1) {
        pthread_rwlock_rdlock(&rwlock);
        printf("===read, tid : %ld, num : %d\n", pthread_self(), num);
        pthread_rwlock_unlock(&rwlock);
        usleep(100);
    }

    return NULL;
}

int main() {

   pthread_rwlock_init(&rwlock, NULL);

    // 创建3个写线程,5个读线程(线程池)
    pthread_t wtids[3], rtids[5];
    for(int i = 0; i < 3; i++) {
        pthread_create(&wtids[i], NULL, writeNum, NULL);
    }

    for(int i = 0; i < 5; i++) {
        pthread_create(&rtids[i], NULL, readNum, NULL);
    }

    // 设置线程分离
    for(int i = 0; i < 3; i++) {
       pthread_detach(wtids[i]);
    }

    for(int i = 0; i < 5; i++) {
         pthread_detach(rtids[i]);
    }

    pthread_exit(NULL);

    pthread_rwlock_destroy(&rwlock);

    return 0;
}
  • 11
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值