线程同步(二)

目录

死锁

加锁之后忘记解锁:

重复加锁,造成死锁

在程序中有多个共享资源,因此有很多把锁,随意加锁,导致相互被阻塞

在使用多线程编程的时候,如何避免死锁呢?

读写锁

在程序中对读写锁加读锁, 锁定的是读操作

在程序中对读写锁加写锁, 锁定的是写操作

解锁

读写锁使用


死锁

        线程死锁是指两个或多个线程互相等待对方释放自己所需资源的情况。这种情况下,所有的线程都被阻塞,无法继续执行,程序就会停滞不前。线程死锁通常发生在多个线程同时访问共享资源时,如果使用不当,就会引发竞争条件,进而导致死锁。

造成死锁的场景有如下几种:

加锁之后忘记解锁:

// 场景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);
    }
}

重复加锁,造成死锁

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

// 隐藏的比较深的情况
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. 有两个共享资源: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.避免多次锁定,多检查

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

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

4.项目程序中可以引入一些专门用于死锁检测的模块

读写锁

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

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

pthread_rwlock_t rwlock;

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

锁的状态:锁定 / 打开
        锁定的是什么操作:读操作 / 写操作,使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之亦然。
哪个线程将这把锁锁上了
        读写锁的使用方式也互斥锁的使用方式是完全相同的:找共享资源,确定临界区,在临界区的开始位置加锁(读锁 / 写锁),临界区的结束位置解锁。

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

1.使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
2.使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
3.使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。
        如果说程序中所有的线程都对共享资源做写操作,使用读写锁没有优势,和互斥锁是一样的,如果说程序中所有的线程都对共享资源有写也有读操作,并且对共享资源读的操作越多,读写锁更有优势。

Linux 提供的读写锁操作函数原型如下,如果函数调用成功返回 0,失败返回对应的错误号:

#include 
pthread_rwlock_t rwlock;
// 初始化读写锁
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

在程序中对读写锁加读锁, 锁定的是读操作
 

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

        调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞。

在程序中对读写锁加写锁, 锁定的是写操作
 

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

        调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞。

// 这个函数可以有效的避免死锁
// 如果加读锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

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

// 这个函数可以有效的避免死锁
// 如果加写锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

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

解锁

// 解锁, 不管锁定了读还是写都可用解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

读写锁使用

        8 个线程操作同一个全局变量,3 个线程不定时写同一全局资源,5 个线程不定时读同一全局资源。

#include 
#include 
#include 
#include 
#include 

// 全局变量
int number = 0;

// 定义读写锁
pthread_rwlock_t rwlock;

// 写的线程的处理函数
void* writeNum(void* arg)
{
    while (1)
    {
        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)
{
    while (1)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("--全局变量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;
}


线程同步(二)_第1张图片

 相关文章:

线程同步(一)_小梁今天敲代码了吗的博客-CSDN博客

你可能感兴趣的:(Linux,开发语言,linux,c++)