Linux线程的同步与互斥(一) 互斥锁+读写锁

Linux线程的同步与互斥

同步概念误区

所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。

先看个样例:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>

#define MAX 50
// 全局变量
int number;

// 线程处理函数
void* funcA_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        int cur = number;
        cur++;
        usleep(10);
        number = cur;
        printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
    }

    return NULL;
}

void* funcB_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        int cur = number;
        cur++;
        number = cur;
        printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
        usleep(5);
    }

    return NULL;
}

int main(int argc, const char* argv[])
{
    pthread_t p1, p2;

    // 创建两个子线程
    pthread_create(&p1, NULL, funcA_num, NULL);
    pthread_create(&p2, NULL, funcB_num, NULL);

    // 阻塞,资源回收
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);

    return 0;
}

执行结果:
在这里插入图片描述
出现这个错误的原因:我们数的这个数字,在某个线程计数过程中,还存储在CPU的寄存器中,没有来得及更新到物理内存中
解决方法:采用同步的方式(加锁)

为什么需要同步和互斥?

因为多个线程共享地址空间,也就是很多资源都是共享的
优点:通信方便
缺点:缺乏访问控制
线程安全:因为一个线程的操作问题,给其他线程造成了不可控、引起崩溃、异常、逻辑不正确等问题的现象。

创建一个函数,它没有线程安全问题的的话,不要使用全局变量、STL、malloc、new等等会在全局有效的数据(会有访问控制的问题)
我们之前写的函数为什么没有线程安全问题呢 我们之前所学的都是使用的局部变量!线程都有自己的独立栈结构!

互斥同步相关的概念

  1. 临界资源:凡是被线程共享访问的资源都是临界资源(多线程、多进程打印数据到显示器[临界资源])
  2. 临界区:我的代码中访问临界资源的代码(在我的代码中,不是所有的代码都是进行访问临界资源的。而访问临界资源的代码区域我们称之为临界区)
  3. 对临界区进行保护的功能,本质:就是对临界资源的保护。方式:互斥或者同步
  4. 互斥:在任意时刻,只允许- 一个执行流访问某段代码(访问某部分资源),就可以称之为互斥!
  5. 原子性:一个事情要么不执行,要么就执行完毕。比如: printf(“hello world”) → lock(): printf(); unlock();
  6. 同步:一般般而言,让访问临界资源的过程在安全的前提下(一般都是互斥和原子的),让访问资源具有一定的顺序性、合理性!

同步方式

对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁读写锁条件变量信号量

一、互斥量(锁)mutex

互斥锁提供了对于临界资源以互斥的方式进行访问的同步机制,即以排他的方式防止临界资源被破坏。简单来说,互斥锁类似于一个布尔变量,它只有“锁定”和“打开”两个状态,在使用临界资源时,线程先申请互斥锁,如果此时互斥锁处于“打开”状态,则该线程立即占有该互斥锁,将该锁的状态设置为“锁定”。此时如果还有其他线程使用该临界资源时,发现互斥锁处于“锁定”状态,则阻塞该线程,直到持有互斥锁的线程释放该锁。——通过这样的机制,保证了在使用临界资源时数据不会被另外一个线程破坏。

在 Linux 中互斥锁的类型为 pthread_mutex_t,创建一个这种类型的变量就得到了一把互斥锁:

pthread_mutex_t  mutex;

在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程 ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关。

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

// 抢票逻辑:1000张票,5个线程同时在抢
int tickets = 1000;
void *ThreadRoutine(void *args)
{
    int id = *(int *)args;
    delete (int*)args;

    while(true)
    {
        if (tickets > 0) //抢票
        {
            usleep(10000); // 0.1s
            std::cout << "我是[" << id << "] 我要抢的票是:" << tickets <<std::endl;
            tickets--;
        }
        else //没有票了
        {
            break;
        }
    }
}
int main()
{
    pthread_t tid[5];
    for (int i = 0; i < 5; i++)
    {
       int *id=new int(i);
        pthread_create(tid+i, nullptr, ThreadRoutine,id);
    }
    for (int i = 0; i < 5; i++)
    {
        pthread_join(tid[i], nullptr); // nullptr阻塞式等待
    }
    return 0;
}

结果展示:
在这里插入图片描述

为什么会出现这种结果呢?

这个样例中的tickets就是临界资源,usleep模拟漫长的业务过程,在这个过程中,可能有多个线程进行切换,进入该代码段。
所以tickets——并不是原子操作(言下之意这个操作并不安全)
在这里插入图片描述
tickets--表面上是一行C/C++代码。但是他在汇编级别是多行代码。
当有多个线程在访问临界资源时,就可能会出现安全问题。例如thread A和thread B两个线程访问,他们都会将自己的上下文加载到CPU中运行,当时间片到了,都会保留上下文数据进入自己的PCB中,他们所修改的tickets数值都不一致,数据写入内存的究竟先是888,后来又被改成999。这就会引发安全问题。
在这里插入图片描述

要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量(互斥锁)。

互斥锁的基本操作

1、互斥锁的初始化

pthread_mutex_init

函数名称pthread_mutex_init
函数功能初始化互斥锁
头文件#include<pthread.h>
函数原型int pthread_mutex_init (pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数restrict:需要被初始化的互斥锁指针
attr:指向描述互斥锁属性的指针
返回值0:成功
!0:失败

说明:
pthread_mutex_init 函数的第二个参数attr是指向描述互斥锁属性的指针,如果该参数为NULL,则表示使用默认属性。

2、互斥锁的销毁

pthread_mutex_destroy

函数名称pthread_mutex_destroy
函数功能销毁互斥锁
头文件#include<pthread.h>
函数原型int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数mutex:需要销毁的互斥锁对象的指针
返回值0:成功
!0:失败

利用C++封装的抢票逻辑如下:

class Ticket
{
private:
    int tickets;

public:
    Ticket() : tickets(10000)
    {
        //不用调用pthread_mutex_init
    }

    // 抢票函数
    bool GetTicket()
    {
        // 静态的锁
        static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // 使用这种宏的方式初始化的互斥量不需要销毁
        bool res = true;                                        // 这个bool变量是每个线程私有的,因为是局部变量,需要在栈上开辟,而栈是线程私有的,所以每个线程都有独立的res变量
        pthread_mutex_lock(&mtx);                               // 申请锁
        if (tickets > 0)                                        // 抢票
        {
            usleep(1000); // 0.1s
            std::cout << "我是[" << pthread_self() << "] 我要抢的票是:" << tickets << std::endl;
            tickets--;
        }

        else //没有票了
        {
            std::cout << "票已经被抢空了!" << std::endl;
            res = false;
        }
        pthread_mutex_unlock(&mtx); // 释放锁
        return res;
    }
    ~Ticket()
    {
        // 不用调用pthread_mutex_destroy
    }
};

void *ThreadRoutine(void *args)
{
    Ticket *t = (Ticket *)args;

    //  每个线程都调用GetTick函数一直抢票
    while (true)
    {
        if (!t->GetTicket()) //票抢完了就退出
        {
            break;
        }
    }
}
int main()
{
    Ticket *t = new Ticket();
    pthread_t tid[5];
    for (int i = 0; i < 5; i++)
    {
        pthread_create(tid + i, nullptr, ThreadRoutine, (void *)t); // 每一个线程都有一个t对象
    }
    for (int i = 0; i < 5; i++)
    {
        pthread_join(tid[i], nullptr); // nullptr阻塞式等待
    }
    return 0;
}

3、申请互斥锁

pthread_mutex_lock
在需要使用临界资源时,我们需要先申请锁,保证当前线程抢到锁以后再对临界资源进行操作。使用该函数进行加锁时,如果mutex已经被锁住,当前尝试加锁的线程就会被阻塞,直到该互斥锁被其他线程释放,申请到锁为止。

函数名称pthread_mutex_lock
函数功能申请互斥锁
头文件#include<pthread.h>
函数原型int pthread_mutex_lock(pthread_mutex_t *mutex);
参数mutex:指向申请互斥锁对象的指针
返回值0:成功
!0:失败

注意:加锁时,无论加什么类型的锁,都不可能被两个不同的线程同时获得,在同一进程中的线程,如果加锁后没有解锁,则其他线程无法咱获得该锁。

4、释放互斥锁

pthread_mutex_unlock
当线程离开临界区的时候,需要释放已经获得的互斥锁,以便需要使用该临界资源的其他线程能够正常使用。

函数名称pthread_mutex_unlock
函数功能释放互斥锁
头文件#include<pthread.h>
函数原型int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数mutex:指向互斥锁对象的指针
返回值0:成功
!0:失败
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <mutex>// C++11
// 抢票逻辑:1000张票,5个线程同时在抢
// 对临界区进行加锁
class Ticket
{
private:
    int tickets;
    pthread_mutex_t mtx;// 原生系统级别
    std::mutex mymtx;// 语言级别的锁——C++11

public:
    Ticket() : tickets(1000)
    {
        pthread_mutex_init(&mtx, nullptr);// 初始化
    }

    // 抢票函数
    bool GetTicket()
    {
        bool res=true;  // 这个bool变量是每个线程私有的,因为局部变量,都在线程的私有栈结构中
        //pthread_mutex_lock(&mtx);// 申请锁
        mymtx.lock();
        if (tickets > 0) //抢票
        {
            usleep(1000); // 0.1s
            std::cout << "我是[" << pthread_self() << "] 我要抢的票是:" << tickets << std::endl;
            tickets--;
        }

        else //没有票了
        {
            std::cout<<"票已经被抢空了!"<<std::endl;
            res=false;
        }
        //pthread_mutex_unlock(&mtx);// 释放锁
        mymtx.unlock();
        return res;
    }
    ~Ticket()
    {
        pthread_mutex_destroy(&mtx);
    }
};

void *ThreadRoutine(void *args)
{
    Ticket *t = (Ticket *)args;

    //  每个线程都调用GetTick函数一直抢票
    while (true)
    {
       if(!t->GetTicket())//票抢完了就退出
       {
           break;
       } 
    }
}
int main()
{
    Ticket *t = new Ticket();
    pthread_t tid[5];
    for (int i = 0; i < 5; i++)
    {
        pthread_create(tid + i, nullptr, ThreadRoutine, (void *)t); // 每一个线程都有一个t对象
    }
    for (int i = 0; i < 5; i++)
    {
        pthread_join(tid[i], nullptr); // nullptr阻塞式等待
    }
    return 0;
}

这样就不会出现票被抢到负数的情况了
在这里插入图片描述

如何确定咱们加锁,解锁的范围?

因为锁是用来保护共享资源的,咱们找寻操作了共享资源的范围,在其间加锁和解锁即可。

sleep函数,让这个A线程睡着了,就是让其强制放弃CPU的时间片,此时会让其他线程B拥有CPU的时间片,但是B线程发现锁还在A线程中,所以B他会被阻塞住,直到A把锁解开,B才能获得锁。

互斥锁实现原理

我要访问临界资源tickets,需要先访问mtx,前提是所有线程必须得先看到它!
那么锁本身,是不是也是临界资源!

你如何保证锁本身是安全的!

原理:lock, unlock–> 是原子的! (加锁和解锁为什么需要是原子性的? )

为了实现互斥锁操作,大多数体系结构都提供了swap或者exchange指令,该指令的作用是,用一条汇编完成内存和CPU内寄存器数据的交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。现在我们把lock和unlock的伪代码改一下:

在这里插入图片描述
我们认为一行代码是原子的:只有一条汇编的情况
为了更好的理解和说明原理,我们以A和B两个线程在CPU中切换为例
mutex我们可以设初值为1,%al可以认为是CPU中的寄存器,当不同的线程申请锁的时候,可以分为三步:

1、thread A执行movb $0,%al,将0移入%al中
在这里插入图片描述

2、thread A执行xchgb %al,mutex,将内存和CPU内寄存器数据的交换,0和1互换
在这里插入图片描述

3、if判断语句的执行,A竞争锁成功,return 0,继续向后执行代码。某时刻,thread A时间片完了,它会保留自己的上下文数据,接下来thread B运行进入,它也是从第一步开始,move 0覆盖al,再交换,0和0交换无所谓,判断发现,al中的值是小于等于0的,所以走else语句,thread B就要挂起等待
在这里插入图片描述
thread B在挂起等待过程中,如果被唤醒,就会goto lock重新申请锁

ps:在临界区中,正在运行的线程有可能会被切走!线程被切走时,要做上下文数据的保护,而锁数据也是在上下文当中的。拥有锁,被切走的线程,是“抱着锁”走的。在此期间,其他线程休想申请成功从而进入临界区。站在其他线程的视角,是不是对其他线程有意义的状态,是不是就是A线程要么没有申请,要么A使用完毕锁——线程A访问临界资源的原子性

可重入 VS 线程安全

1、可重入与线程安全概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

2、常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

3、常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

4、常见的不可重入情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

5、常见的可重入情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

6、可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

7、可重入与线程安全区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

死锁

1、死锁的概念

  • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态

2、死锁的4个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

3、避免死锁的方法

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

对共享资源访问完毕之后,一定要解锁,或者在加锁的使用 trylock

避免多次锁定,多检查

如果程序中有多把锁,可以控制对锁的访问顺序 (顺序访问共享资源,但在有些情况下是做不到的),另外也可以在对其他互斥锁做加锁操作之前,先释放当前线程拥有的互斥锁。

项目程序中可以引入一些专门用于死锁检测的模块(第三方的)

4、避免死锁的算法

  • 死锁检测算法(了解)
  • 银行家算法(了解)

5、常见的造成死锁的场景

  • 加锁之后忘记解锁
// 场景1
void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功, 当前循环完毕没有解锁, 在下一轮循环的时候自己被阻塞了
        // 其余的线程也被阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        // 忘记解锁
    }
}

// 场景2
void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程被阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        if(xxx)
        {
            // 函数退出, 没有解锁(解锁函数无法被执行了)
            return ;
        }
        
        pthread_mutex_lock(&mutex);
    }
}

场景1中必须要调用unlock才能把锁给解开,但是下次循环再次调用lock函数,发现锁已经被占用了,其余线程就得等待。
在这里插入图片描述

场景2中,是比较隐晦的,若进入了if语句,则直接return了,那么也是没有解锁的。所以在return前一定要把锁给解开。
在这里插入图片描述

  • 重复加锁,造成死锁
// 场景1
void func()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
        // 锁被锁住了, A线程阻塞
        pthread_mutex_lock(&mutex);
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

// 场景2  隐藏的比较深的情况
void funcA()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

void funcB()
{
    for(int i=0; i<6; ++i)
    {
        // 当前线程A加锁成功
        // 其余的线程阻塞
    	pthread_mutex_lock(&mutex);
        funcA();		// 重复加锁
    	....
    	.....
        pthread_mutex_unlock(&mutex);
    }
}

场景1一般很少出现,极可能是因为手误,多拷贝了一行才会出现这种。

场景2,先调用funB(),再调用funcA(),
在这里插入图片描述

  • 在程序中有多个共享资源,因此有很多把锁,随意加锁,导致相互被阻塞
    一般有几个共享资源,咱就配几把锁!!!
场景描述:
  1. 有两个共享资源:X, Y,X对应锁A, Y对应锁B
     - 线程A访问资源X, 加锁A
     - 线程B访问资源Y, 加锁B
  2. 线程A要访问资源Y, 线程B要访问资源X,因为资源X和Y已经被对应的锁锁住了,因此这个两个线程被阻塞
     - 线程A被锁B阻塞了, 无法打开A锁
     - 线程B被锁A阻塞了, 无法打开B锁

在这里插入图片描述

在这里插入图片描述
但是此时两人想要出来? 这是无解的!!!进也不是,出也出不来!

读写锁

1、读写锁介绍

读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作, 那么读是并行的,但是使用互斥锁,读操作也是串行的。

读写锁是一把锁,锁的类型为 pthread_rwlock_t,有了类型之后就可以创建一把互斥锁了:

pthread_rwlock_t rwlock;

之所以称其为读写锁,是因为这把锁既可以锁定读操作,也可以锁定写操作。为了方便理解,可以大致认为在这把锁中记录了这些信息:

  • 锁的状态:锁定 / 打开
  • 锁定的是什么操作:读操作 / 写操作,使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之亦然
  • 哪个线程将这把锁锁上了

读写锁的使用方式也互斥锁的使用方式是完全相同的:找共享资源,确定临界区,在临界区的开始位置加锁(读锁 / 写锁),临界区的结束位置解锁。

因为通过一把读写锁可以锁定读或者写操作,下面介绍一下关于读写锁的特点:

  • 使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的
  • 使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的
  • 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高

如果说程序中所有的线程都对共享资源做写操作,使用读写锁没有优势,和互斥锁是一样的,如果说程序中所有的线程都对共享资源有写也有读操作,并且对共享资源读的操作越多,读写锁更有优势。

2、读写锁函数

①初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           const pthread_rwlockattr_t *restrict attr);
②释放读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

参数说明:

  • rwlock: 读写锁的地址,传出参数
  • attr: 读写锁属性,一般使用默认属性,指定为 NULL
③加读锁,锁定读操作

rd=read

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
④以非阻塞方式在读写锁上获得读锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数加锁失败,对应的线程不会被阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。

⑤加写锁,锁定写操作

wr=write

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
⑥以非阻塞方式在读写锁上获得写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数加锁失败,但是线程不会阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。

⑦解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

3、读写锁的使用

题目要求:8 个线程操作同一个全局变量,3 个线程不定时写同一全局资源(每一个线程加50次),5 个线程不定时读同一全局资源。

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

#define MAX 50
// 全局变量
int number = 0;

// 定义读写锁
pthread_rwlock_t rwlock;

// 写的线程的处理函数
void *writeNum(void *arg)
{
    for (int i = 0; i < MAX; i++)
    {
        pthread_rwlock_wrlock(&rwlock);
        int cur = number;
        cur++;
        number = cur;
        printf("++写操作完毕, number : %d, tid = %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlock);
        // 添加sleep目的是要看到多个线程交替工作
        usleep(rand() % 100);
    }

    return NULL;
}

// 读线程的处理函数
// 多个线程可以如果处理动作相同, 可以使用相同的处理函数
// 每个线程中的栈资源是独享
void *readNum(void *arg)
{
    for (int i = 0; i < MAX; i++)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("read 全局变量number = %d, tid = %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlock);
        usleep(rand() % 100);
    }
    return NULL;
}

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

    // 3个写线程, 5个读的线程
    pthread_t wtid[3];
    pthread_t rtid[5];
    for (int i = 0; i < 3; ++i)
    {
        pthread_create(&wtid[i], NULL, writeNum, NULL);
    }

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

    // 释放资源
    for (int i = 0; i < 3; ++i)
    {
        pthread_join(wtid[i], NULL);
    }

    for (int i = 0; i < 5; ++i)
    {
        pthread_join(rtid[i], NULL);
    }

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

    return 0;
}

makefile:

rwlock:rwlock.c
	gcc -o $@ $^ -lpthread -std=gnu99
.PHONY:clean
	rm -rf rwlock

在这里插入图片描述

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值