Linux线程同步中的锁机制

竞态条件

        当多个线程并发访问和修改同一个共享资源(如全局变量)时,如果没有适当的同步措施,就会遇到线程同步问题。这种情况下,程序最终的结果依赖于线程执行的具体时序,导致了竞态条件。

        竞态条件(race condition)是一种特定的线程同步问题,指的是两个或者以上进程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。它会导致程序的行为和输出超出预期,因为共享资源的最终状态取决于线程执行的顺序和时机。为了确保程序执行结果的正确性和预期一致,需要通过适当的线程同步机制来避免竞态条件。

互斥锁

        避免竞态条件,可以通过避免多个线程同时操作一个变量或者通过加锁使同一时间只有一个线程可以操作特定资源。互斥锁就是一种常见的线程同步的方法。

        互斥锁的数据类型由pthread_mutex_t定义,用作线程之间的互斥锁。互斥锁是一种同步机制,用来控制对共享资源的访问。在任何时刻,最多只能有一个线程持有特定的互斥锁。如果一个线程试图获取一个已经被其他线程持有的锁,那么请求锁的线程将被阻塞,直到锁被释放。

        互斥锁的使用过程包括初始化,锁定,尝试锁定,解锁,销毁等过程。

互斥锁的初始化

        PTHREAD_MUTEX_INITIALIZER是POSIX线程(Pthreads)库中定义的一个宏,用于静态初始化互斥锁(mutex)。这个宏为互斥锁提供了一个初始状态,使其准备好被锁定和解锁,而不需要在程序运行时显式调用初始化函数。对于静态初始化,则在清理时不必显示销毁(调用pthread_mutex_destroy)。如果互斥锁是动态分配的(使用pthread_mutex_init函数初始化),确实需要显式销毁互斥锁资源。或者互斥锁会被跨多个函数或文件使用,不再需要时必须显式销毁它。

        当我们使用PTHREAD_MUTEX_INITIALIZER初始化互斥锁时,实际上是将互斥锁设置为默认属性和未锁定状态。这种初始化方式适用于简单的同步问题,我们可以通过以下代码初始化互斥锁。static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock函数

        函数原型为int pthread_mutex_lock(pthread_mutex_t *mutex);

        作用是获取锁,如果此时锁被其他线程占用则会阻塞直到成功获取锁并返回获取锁的结果,成功获取返回0,失败返回对应错误码。

pthread_mutex_trylock函数

        函数原型为int pthread_mutex_trylock(pthread_mutex_t *mutex);

        作用是非阻塞式获取锁,如果锁此时被其他线程占用也不会阻塞等待,而是立即返回EBUSY。获取成功返回0

pthread_mutex_unlock函数

        函数原型为int pthread_mutex_unlock(pthread_mutex_t *mutex);

        该函数用于解锁指定的互斥锁。调用线程必须是当前持有互斥锁的线程;否则,解锁操作可能会失败。

例程展示

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *add(void *argv)
{
    pthread_mutex_lock(&mutex);
    int* tmp;
    tmp = (int *)argv;
    (*tmp)++;
    pthread_mutex_unlock(&mutex);
}

int main(int argc, char const *argv[])
{
    int tmp = 0;
    pthread_t add_id[5000];
    
    for (int i = 0; i < 5000; i++)
    {
        pthread_create(&add_id[i],NULL,add,&tmp);
    }

    for (int i = 0; i < 5000; i++)
    {
        pthread_join(add_id[i],NULL);
    }
    
    printf("%d\n",tmp);

    return 0;
}

        创建五千个进程对同一个变量进行累加,为确保多个线程对其累加的操作不会重叠,需要采用阻塞获取互斥锁的方法避免同时操作同一个变量,最终对tmp累加五千次,结果为5000。

读写锁 

        读写锁是专门为读写操作创建的一种锁机制。在读写锁的控制下,多个线程可以同时获得读锁。这些线程可以并发地读取共享资源,但它们的存在阻止了写锁的授予。如果至少有一个读操作持有读锁,写操作就无法获得写锁。写操作将会阻塞,直到所有的读锁都被释放。也就是可以很多个线程同时读取,但是不能一边读取一边写入,不能有多个线程同时写入。读写锁的存在防止了

pthread_rwlock_init函数

        函数原型为int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

        作用是rwlock指向的读写锁分配所有需要的资源,并将锁初始化为未锁定状态。读写锁的属性由attr参数指定,如果attr为NULL,则使用默认属性。当锁的属性为默认时,可以通过宏PTHREAD_RWLOCK_INITIALIZER初始化,即

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; 效果和调用当前方法并为attr传入NULL是一样的,这样初始化和互斥锁中提到的静态初始化方式相同,同样也不需要调用摧毁函数

        参数一:读写锁的地址

        参数二:读写锁的属性

        成功创建则返回0,否则返回错误码

pthread_rwlockattr_init函数

        函数原型为int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);

        作用是用所有属性的默认值初始化attr指向的属性对象

        attr 读写锁属性对象指针

        return int 成功返回0,失败返回错误码

pthread_rwlockattr_destroy函数

        函数原型为int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

        作用是销毁读写锁属性对象

        attr 读写锁属性对象指针

        return int 成功返回0,失败返回错误码

pthread_rwlockattr_setkind_np函数

        函数原型为int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);

        作用将attr指向的属性对象中的"锁类型"属性设置为pref规定的值

        attr 读写锁属性对象指针

        pref 希望设置的锁类型,可以被设置为以下三种取值的其中一种

        PTHREAD_RWLOCK_PREFER_READER_NP: 默认值,读线程拥有更高优先级。当存在阻塞的写线程时,读线程仍然可以获得读写锁。只要不断有新的读线程,写线程将一直保持"饥饿"。

        PTHREAD_RWLOCK_PREFER_WRITER_NP: 写线程拥有更高优先级。这一选项被glibc忽略。

        PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP: 写线程拥有更高优先级,在当前系统环境下,它是有效的,将锁类型设置为该值以避免写饥饿。

        return int 成功返回0,失败返回非零的错误码

pthread_rwlock_destroy函数

        函数原型为int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

        作用是销毁rwlock指向的读写锁对象,并释放它使用的所有资源。当任何线程持有锁的时候销毁锁,或尝试销毁一个未初始化的锁,结果是未定义的。静态创建的锁不需要显式摧毁。

pthread_rwlock_rdlock函数

        函数原型为int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

        作用是应用一个读锁到rwlock指向的读写锁上,并使调用线程获得读锁。如果写线程持有锁,调用线程无法获得读锁,它会阻塞直至获得锁。

        成功获取锁则返回0,失败返回错误码

pthread_rwlock_wrlock函数

        函数原型为int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

        作用是释放调用线程锁持有的rwlock指向的读写锁,不必在调用时区分要释放的是读还是写锁。成功释放返回0,否则返回错误码。

例程展示

        没有加读写锁时

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <bits/pthreadtypes.h>

int data = 0;

//写线程的线程函数
void *thread_write(void *argv)
{
    int tmp;
    tmp = data + 1;

    sleep(1);

    data = tmp;

    printf("现在是%s,data的值为%d\n",(char *)argv,data);
}

//读线程的线程函数
void *thread_read(void *argv)
{
    printf("现在是%s,data的值为%d\n",(char *)argv,data);
}

int main(int argc, char const *argv[])
{
    pthread_t write_id[5];
    pthread_t read_id[5];

    pthread_create(&write_id[0],NULL,thread_write,"write0");
    pthread_create(&write_id[1],NULL,thread_write,"write1");
    pthread_create(&write_id[2],NULL,thread_write,"write2");
    pthread_create(&write_id[3],NULL,thread_write,"write3");
    pthread_create(&write_id[4],NULL,thread_write,"write4");

    sleep(3);

    pthread_create(&read_id[0],NULL,thread_read,"read0");
    pthread_create(&read_id[1],NULL,thread_read,"read1");
    pthread_create(&read_id[2],NULL,thread_read,"read2");
    pthread_create(&read_id[3],NULL,thread_read,"read3");
    pthread_create(&read_id[4],NULL,thread_read,"read4");

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

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

              

        写线程中使用了tmp先获取data的值,睡眠之后再对data进行更新,由于发生竞态条件,多个写线程同时执行,近乎同时获取了data的值,此时获取的data值均为0,之后对data进行更新,所以data值始终为0.如果写线程是按照严格的顺序执行,那么结果就会为5.

        如果写线程函数替换为以下内容就会读取到结果为5

void *thread_write(void *argv)
{
    int *tmp;
    tmp = &data;

    sleep(1);

    (*tmp)++;

    printf("现在是%s,data的值为%d\n",(char *)argv,data);
}

         此处使用了tmp获取data的地址,之后对其进行解引用累加,因为tmp获取的是data的地址,所以data更新之后,对tmp进行解引用会获得最新的值。

 加入读写锁时

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

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int data = 0;

void *thread_write(void *argv)
{
    int tmp = 0;
    pthread_rwlock_wrlock(&rwlock);
    tmp = data + 1;

    data = tmp;

    printf("%s,%d\n",(char *)argv,data);

    pthread_rwlock_unlock(&rwlock);
}

void *thread_read(void *argv)
{
    pthread_rwlock_rdlock(&rwlock);

    printf("%s,%d\n",(char *)argv,data);

    pthread_rwlock_unlock(&rwlock);
}

int main(int argc, char const *argv[])
{
    pthread_t write_id[5];
    pthread_t read_id[5];

    pthread_create(&write_id[0],NULL,thread_write,"write0");
    pthread_create(&write_id[1],NULL,thread_write,"write1");
    pthread_create(&write_id[2],NULL,thread_write,"write2");
    pthread_create(&write_id[3],NULL,thread_write,"write3");
    pthread_create(&write_id[4],NULL,thread_write,"write4");

    sleep(3);

    pthread_create(&read_id[0],NULL,thread_read,"read0");
    pthread_create(&read_id[1],NULL,thread_read,"read1");
    pthread_create(&read_id[2],NULL,thread_read,"read2");
    pthread_create(&read_id[3],NULL,thread_read,"read3");
    pthread_create(&read_id[4],NULL,thread_read,"read4");

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

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

    return 0;
}

        运行结果如下

        上述代码通过加入读写锁,保证了同时只有一个线程可以进行写操作,避免了多个线程同时进行写操作造成的竞态条件问题。

        读写操作随机执行

        删除写操作的sleep()操作,删除主线程中创建写线程之后的睡眠操作,读写线程的创建交叉进行。这样做的目的是尽可能让读写操作间隔执行,但要注意的是,线程的执行顺序是由操作系统内核调度的,其运行规律并不简单地为“先创建先执行”。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <bits/pthreadtypes.h>

int data = 0;

//写线程的线程函数
void *thread_write(void *argv)
{
    int tmp;
    tmp = data + 1;

    data = tmp;

    printf("现在是%s,data的值为%d\n",(char *)argv,data);
}

//读线程的线程函数
void *thread_read(void *argv)
{
    printf("现在是%s,data的值为%d\n",(char *)argv,data);
}

int main(int argc, char const *argv[])
{
    pthread_t write_id[5];
    pthread_t read_id[5];

    pthread_create(&write_id[0],NULL,thread_write,"write0");
    pthread_create(&read_id[1],NULL,thread_read,"read1");
    pthread_create(&read_id[2],NULL,thread_read,"read2");
    pthread_create(&write_id[3],NULL,thread_write,"write3");
    pthread_create(&read_id[4],NULL,thread_read,"read4");

    pthread_create(&read_id[0],NULL,thread_read,"read0");
    pthread_create(&write_id[1],NULL,thread_write,"write1");
    pthread_create(&write_id[2],NULL,thread_write,"write2");
    pthread_create(&read_id[3],NULL,thread_read,"read3");
    pthread_create(&write_id[4],NULL,thread_write,"write4");

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

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

        可以从运行结果看到,线程执行的顺序是不确定的,读取到的值也是不确定的。

写饥饿

        读写锁的写饥饿问题(Writer Starvation)是指在使用读写锁时,写线程可能无限期地等待获取写锁,因为读线程持续地获取读锁而不断地推迟写线程的执行。这种情况通常在读操作远多于写操作时出现。

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

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int data = 0;

void *pthread_write(void *argv)
{
    pthread_rwlock_wrlock(&rwlock);

    int tmp = data + 1;

    data = tmp;

    printf("现在是%s,%d\n",(char *)argv,data);

    pthread_rwlock_unlock(&rwlock);
}

void *pthread_read(void *argv)
{
    pthread_rwlock_rdlock(&rwlock);

    sleep(1);

    printf("现在是%s,%d\n",(char *)argv,data);

    pthread_rwlock_unlock(&rwlock);
}

int main(int argc, char const *argv[])
{
    pthread_t write_id[5];
    pthread_t read_id[5];

    pthread_create(&write_id[0],NULL,pthread_write,"write0");
    pthread_create(&write_id[1],NULL,pthread_write,"write1");
    pthread_create(&write_id[2],NULL,pthread_write,"write2");
    pthread_create(&write_id[3],NULL,pthread_write,"write3");
    pthread_create(&write_id[4],NULL,pthread_write,"write4");

    pthread_create(&read_id[0],NULL,pthread_read,"read0");
    pthread_create(&read_id[1],NULL,pthread_read,"read1");
    pthread_create(&read_id[2],NULL,pthread_read,"read2");
    pthread_create(&read_id[3],NULL,pthread_read,"read3");
    pthread_create(&read_id[4],NULL,pthread_read,"read4");

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

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

    return 0;
}

        在读线程中加入了sleep,延长读锁的占有时间。此时读操作总是连续执行的,且读操作休眠未结束时,写操作会被阻塞。与工作原理相符,读操作可以并发执行,相互之间不必争抢锁,多个读操作可以同时获得读锁;只要有一个线程持有读写锁,写操作就会被阻塞。我们在读操作中加了1s休眠,只要有一个读线程获得锁,在1s内写操作是无法执行的,其它读操作就可以有充足的时间执行,因此读操作就会连续发生,写操作必须等待所有读操作执行完毕方可获得读写锁执行写操作。这就是使用读写锁时存在的潜在问题:写饥饿。

        Linux提供了可以修改的属性pthread_rwlockattr_t,默认情况下,属性中指定的策略为“读优先”,上述代码中就是使用了静态初始化的方法,是以默认属性创建的读写锁。当写操作阻塞时,读线程依然可以获得读锁,从而在读操作并发较高时导致写饥饿问题。我们可以尝试将策略更改为“写优先”,当写操作阻塞时,读线程无法获取锁,避免了写线程持有锁的时间持续延长,使得写线程获取锁的等待时间显著降低,从而避免写饥饿问题。

        解决写饥饿的例程:

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

pthread_rwlock_t rwlock;
int data = 0;

void *pthread_write(void *argv)
{
    pthread_rwlock_wrlock(&rwlock);

    int tmp = data + 1;

    data = tmp;

    printf("现在是%s,%d\n",(char *)argv,data);

    pthread_rwlock_unlock(&rwlock);
}

void *pthread_read(void *argv)
{
    pthread_rwlock_rdlock(&rwlock);

    sleep(1);

    printf("现在是%s,%d\n",(char *)argv,data);

    pthread_rwlock_unlock(&rwlock);
}

int main(int argc, char const *argv[])
{
    pthread_t write_id[5];
    pthread_t read_id[5];

    pthread_rwlockattr_t attr;

    pthread_rwlockattr_init(&attr);
    pthread_rwlockattr_setkind_np(&attr,PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
    pthread_rwlock_init(&rwlock,&attr);
    pthread_rwlockattr_destroy(&attr);

    pthread_create(&write_id[0],NULL,pthread_write,"write0");
    pthread_create(&write_id[1],NULL,pthread_write,"write1");
    pthread_create(&write_id[2],NULL,pthread_write,"write2");
    pthread_create(&write_id[3],NULL,pthread_write,"write3");
    pthread_create(&write_id[4],NULL,pthread_write,"write4");

    pthread_create(&read_id[0],NULL,pthread_read,"read0");
    pthread_create(&read_id[1],NULL,pthread_read,"read1");
    pthread_create(&read_id[2],NULL,pthread_read,"read2");
    pthread_create(&read_id[3],NULL,pthread_read,"read3");
    pthread_create(&read_id[4],NULL,pthread_read,"read4");

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

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

    return 0;
}

        根据运行结果,可以看到写线程可以优先获得写锁,写线程不会出现长期堵塞的情况,避免了写饥饿现象。

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值