0
点赞
收藏
分享

微信扫一扫

Linux线程同步(3)——互斥锁进阶

四月Ren间 2023-05-11 阅读 100
linux

pthread_mutex_trylock()函数

        当互斥锁已经被其它线程锁住时,调用 pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁;如果线程不希望被阻塞,可以使用 pthread_mutex_trylock()函数;调用pthread_mutex_ trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么调用 pthread_mutex _trylock()将会锁住互斥锁并立马返回,如果互斥锁已经被其它线程锁住,调用 pthread_mutex _trylock()加锁失败,但不会阻塞,而是返回错误码 EBUSY。

        其函数原型如下所示:

#include <pthread.h>

int pthread_mutex_trylock(pthread_mutex_t *mutex);

        参数 mutex 指向目标互斥锁,成功返回 0,失败返回一个非 0 值的错误码,如果目标互斥锁已经被其它线程锁住,则调用失败返回 EBUSY。

        对上篇文章示例代码进行修改,使用 pthread_mutex_trylock()替换 pthread_mutex_lock()。

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

static pthread_mutex_t mutex;
static int g_count = 0;

static void *new_thread_start(void *arg){
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++) {
        while(pthread_mutex_trylock(&mutex));//以非阻塞方式上锁
        l_count = g_count;
        l_count++;
        g_count = l_count;
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    return (void *)0;
}
static int loops;
int main(int argc, char *argv[]){
    pthread_t tid1, tid2;
    int ret;
    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);

    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex,NULL);
    
    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    exit(0);
}

        整个执行结果跟使用 pthread_mutex_lock()效果是一样的,大家可以自己测试。

销毁互斥锁

        当不再需要互斥锁时,应该将其销毁,通过调用 pthread_mutex_destroy()函数来销毁互斥锁,其函数原型如下所示:

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);

        参数 mutex 指向目标互斥锁;同样在调用成功情况下返回 0, 失败返回一个非0值错误码。

  • 不能销毁还没有解锁的互斥锁,否则将会出现错误;
  • 没有初始化的互斥锁也不能销毁。

        被 pthread_mutex_destroy()销毁之后的互斥锁,就不能再对它进行上锁和解锁了,需要再次调用 pthread_mutex_init()对互斥锁进行初始化之后才能使用。

        使用示例

        对上节代码进行修改,在进程退出之前,使用 pthread_mutex_destroy()函数销毁互斥锁。

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

static pthread_mutex_t mutex;
static int g_count = 0;

static void *new_thread_start(void *arg){
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++) {
        while(pthread_mutex_trylock(&mutex));//以非阻塞方式上锁
        l_count = g_count;
        l_count++;
        g_count = l_count;
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    return (void *)0;
}
static int loops;
int main(int argc, char *argv[]){
    pthread_t tid1, tid2;
    int ret;
    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);

    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex,NULL);
    
    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    
    /* 销毁互斥锁 */
    pthread_mutex_destroy(&mutex);
    exit(0);
}

互斥锁死锁

        试想一下,如果一个线程试图对同一个互斥锁加锁两次,会出现什么情况?情况就是该线程会陷入死锁状态,一直被阻塞永远出不来;这就是出现死锁的一种情况,除此之外,使用互斥锁还有其它很多种方式也能产生死锁。

        有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又由不同的互斥锁管理。当超过一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁;譬如,程序中使用 一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,并且在试图锁住第二个互斥锁时处于阻塞状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞,于是就产生了死锁。如下示例代码中所示:

// 线程 A
pthread_mutex_lock(mutex1);
pthread_mutex_lock(mutex2);

// 线程 B
pthread_mutex_lock(mutex2);
pthread_mutex_lock(mutex1);

        这就好比是 C 语言中两个头文件相互包含的关系,那肯定编译报错!

        在我们的程序当中,如果用到了多个互斥锁,要避免此类死锁的问题,最简单的方式就是定义互斥锁的层级关系,当多个线程对一组互斥锁操作时,总是应该按照相同的顺序对该组互斥锁进行锁定。譬如在上述场景中,如果两个线程总是先锁定 mutex1 再锁定 mutex2,死锁就不会出现。有时,互斥锁之间的层级关系逻辑不够清晰,即使是这样,依然可以设计出所有线程都必须遵循的强制层级顺序。

        但有时候,应用程序的结构使得对互斥锁进行排序是很困难的,程序复杂、其中所涉及到的互斥锁以及共享资源比较多,程序设计实在无法按照相同的顺序对一组互斥锁进行锁定,那么就必须采用另外的方法。 譬如使用 pthread_mutex_trylock()以不阻塞的方式尝试对互斥锁进行加锁,在这种方案中,线程先使用函数 pthread_mutex_lock()锁定第一个互斥锁,然后使用 pthread_ mutex_trylock()来锁定其余的互斥锁。如果任一pthread_mutex_trylock()调用失败(返回 EBUSY),那么该线程释放所有互斥锁,可以经过一段时间之后从头再试。与第一种按照层级关系来避免死锁的方法变比,这种方法效率要低一些,因为可能需要经历多次循环。

互斥锁的属性

        如前所述,调用 pthread_mutex_init()函数初始化互斥锁时可以设置互斥锁的属性,通过参数 attr 指定。 参数 attr 指向一个 pthread_mutexattr_t 类型对象,该对象对互斥锁的属性进行定义,当然,如果将参数 attr 设置为 NULL,则表示将互斥锁属性设置为默认值。

        如果不使用默认属性,在调用 pthread_mutex_init()函数时,参数 attr 必须要指向一个 pthr ead_mutexattr_t 对象,而不能使用 NULL。当定义 pthread_mutexattr_t 对象之后,需要使用 pthread_mutexattr_init()函数对该对象进行初始化操作,当对象不再使用时,需要使用 pthread_ mutexattr_destroy()将其销毁,函数原型如下所示:

#include <pthread.h>

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

        参数 attr 指向需要进行初始化的 pthread_mutexattr_t 对象,调用成功返回 0,失败将返回非 0 值的错误码。

        pthread_mutexattr_init()函数将使用默认的互斥锁属性初始化参数 attr 指向的 pthread_ mutexattr_t 对象。 关于互斥锁的属性比较多,譬如进程共享属性、健壮属性、类型属性等。

        互斥锁的类型属性控制着互斥锁的锁定特性,一共有 4 种类型:

  • PTHREAD_MUTEX_NORMAL:一种标准的互斥锁类型,不做任何的错误检查或死锁检测。如果 线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁;互斥锁处于未锁定状态,或者已由其它线程锁定,对其解锁会导致不确定结果。
  • PTHREAD_MUTEX_ERRORCHECK:此类互斥锁会提供错误检查。譬如这三种情况都会导致返 回错误:线程试图对已经由自己锁定的互斥锁再次进行加锁(同一线程对同一互斥锁加锁两次), 返回错误;线程对由其它线程锁定的互斥锁进行解锁,返回错误;线程对处于未锁定状态的互斥锁进行解锁,返回错误。这类互斥锁运行起来比较慢,因为它需要做错误检查,不过可将其作为调试工具,以发现程序哪里违反了互斥锁使用的基本原则。
  • PTHREAD_MUTEX_RECURSIVE:此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁,但是如果解锁次数不等于加速次数,则是不会释放锁的;所以,如果对一个递归互斥锁加锁两次,然后解锁一次,那么这个互斥锁依然处于锁定状态,对它再次进行解锁之前不会释放该锁。
  • PTHREAD_MUTEX_DEFAULT : 此类互斥锁提供默认的行为和特性。使用宏 PTHREAD_ MUTEX_INITIALIZER 初始化的互斥锁 , 或者调用参数 arg 为 NULL 的 pthread_mutexattr _init()函数所创建的互斥锁,都属于此类型。此类锁意在为互斥锁的实现保留最大灵活性, Linux 上 , PTHREAD_MUTEX_DEFAULT 类型互斥锁的行为与 PTHREAD_MUTEX_NO RMAL 类型相仿。

        可以使用 pthread_mutexattr_gettype()函数得到互斥锁的类型属性,使用 pthread_mutexattr_ settype()修改 /设置互斥锁类型属性,其函数原型如下所示:

#include <pthread.h>

int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

       参数 attr 指向 pthread_mutexattr_t 类型对象;对于 pthread_mutexattr_gettype()函数,函数调用成功会将互斥锁类型属性保存在参数 type 所指向的内存中,通过它返回出来;而对于 pthread_mutexattr_settype()函数,会将参数 attr 指向的 pthread_mutexattr_t 对象的类型 属性设置为参数 type 指定的类型。使用方式如下:

pthread_mutex_t mutex;
pthread_mutexattr_t attr;

/* 初始化互斥锁属性对象 */
pthread_mutexattr_init(&attr);

/* 将类型属性设置为 PTHREAD_MUTEX_NORMAL */
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);

/* 初始化互斥锁 */
pthread_mutex_init(&mutex, &attr);

......

/* 使用完之后 */
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
举报

相关推荐

0 条评论