• Linux原子操作与锁实现


    多线程的使用

    线程的创建

    函数原型:

    #include <pthread.h>
    
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                       void *(*start_routine) (void *), void *arg);
    // Compile and link with -pthread.
    
    • 1
    • 2
    • 3
    • 4
    • 5

    描述:
    pthread_create()函数在调用进程中启动一个新线程。新线程通过调用start_routine()开始执行;arg作为start_routine()的唯一参数传递。

    新线程以以下方式之一终止:
    (1)它调用pthread_exit(),指定一个退出状态值,该值可用于调用pthrread_join()的同一进程中的另一个线程,即pthrread_join()可以接收pthread_exit()返回的值。
    (2)它从start_routine()返回。这相当于使用return语句中提供的值调用pthread_exit()。
    (3)它被pthread_cancel()取消。
    (4)进程中的任何线程都调用exit(),或者主线程执行main()的返回。这将导致进程中所有线程的终止。

    参数介绍:

    参数含义
    attrattr参数指向pthread_attr_t结构,其内容在线程创建时用于确定新线程的属性;使用pthread_attr_init()和相关函数初始化该结构。如果attr为空,则使用默认属性创建线程。
    thread在返回之前,成功调用pthread_create()将新线程的ID存储在thread指向的缓冲区中;此标识符用于在后续调用其他pthreads函数时引用线程。
    start_routine线程入口函数
    arg线程入口函数的参数

    返回值:
    成功时,返回0;出错时,它返回一个错误号,并且*thread的内容未定义。

    错误号:

    错误号含义
    EAGAIN资源不足,无法创建另一个线程。
    AGAIN A遇到系统对线程数量施加的限制。可能触发此错误的限制有很多:已达到RLIMIT_NPROC软资源限制【通过setrlimit()设置】,该限制限制了真实用户ID的进程和线程数;已达到内核对进程和线程数的系统范围限制,即/proc/sys/kernel/threads max【请参阅proc()】;或者达到最大pid数/proc/sys/kernel/pid_max【见proc()】。
    EINVAL属性中的设置无效。
    EPERM没有设置attr中指定的调度策略和参数的权限。

    其他:
    新线程继承创建线程的信号掩码【pthread_sigmask()】的副本。新线程的挂起信号集为空【sigpending()】。新线程不继承创建线程的备用信号堆栈【sigaltstack()】。
    新线程的CPU时间时钟的初始值为0【参见pthread_getcpuclockid()】。

    示例代码:

    #include <pthread.h>
    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <ctype.h>
    
    #define handle_error_en(en, msg) \
            do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
    
    #define handle_error(msg) \
            do { perror(msg); exit(EXIT_FAILURE); } while (0)
    
    struct thread_info {    /* Used as argument to thread_start() */
        pthread_t thread_id;        /* ID returned by pthread_create() */
        int       thread_num;       /* Application-defined thread # */
        char     *argv_string;      /* From command-line argument */
    };
    
    /* Thread start function: display address near top of our stack,
       and return upper-cased copy of argv_string */
    
    static void *
    thread_start(void *arg)
    {
        struct thread_info *tinfo = arg;
        char *uargv, *p;
    
        printf("Thread %d: top of stack near %p; argv_string=%s\n",
                tinfo->thread_num, &p, tinfo->argv_string);
    
        uargv = strdup(tinfo->argv_string);
        if (uargv == NULL)
            handle_error("strdup");
    
        for (p = uargv; *p != '\0'; p++)
            *p = toupper(*p);
    
        return uargv;
    }
    
    int
    main(int argc, char *argv[])
    {
        int s, tnum, opt, num_threads;
        struct thread_info *tinfo;
        pthread_attr_t attr;
        int stack_size;
        void *res;
    
        /* The "-s" option specifies a stack size for our threads */
    
        stack_size = -1;
        while ((opt = getopt(argc, argv, "s:")) != -1) {
            switch (opt) {
            case 's':
                stack_size = strtoul(optarg, NULL, 0);
                break;
    
            default:
                fprintf(stderr, "Usage: %s [-s stack-size] arg...\n",
                        argv[0]);
                exit(EXIT_FAILURE);
            }
        }
    
        num_threads = argc - optind;
    
        /* Initialize thread creation attributes */
    
        s = pthread_attr_init(&attr);
        if (s != 0)
            handle_error_en(s, "pthread_attr_init");
    
        if (stack_size > 0) {
            s = pthread_attr_setstacksize(&attr, stack_size);
            if (s != 0)
                handle_error_en(s, "pthread_attr_setstacksize");
        }
    
        /* Allocate memory for pthread_create() arguments */
    
        tinfo = calloc(num_threads, sizeof(struct thread_info));
        if (tinfo == NULL)
            handle_error("calloc");
    
        /* Create one thread for each command-line argument */
    
        for (tnum = 0; tnum < num_threads; tnum++) {
            tinfo[tnum].thread_num = tnum + 1;
            tinfo[tnum].argv_string = argv[optind + tnum];
    
            /* The pthread_create() call stores the thread ID into
               corresponding element of tinfo[] */
    
            s = pthread_create(&tinfo[tnum].thread_id, &attr,
                               &thread_start, &tinfo[tnum]);
            if (s != 0)
                handle_error_en(s, "pthread_create");
        }
    
        /* Destroy the thread attributes object, since it is no
           longer needed */
    
        s = pthread_attr_destroy(&attr);
        if (s != 0)
            handle_error_en(s, "pthread_attr_destroy");
    
        /* Now join with each thread, and display its returned value */
    
        for (tnum = 0; tnum < num_threads; tnum++) {
            s = pthread_join(tinfo[tnum].thread_id, &res);
            if (s != 0)
                handle_error_en(s, "pthread_join");
    
            printf("Joined with thread %d; returned value was %s\n",
                    tinfo[tnum].thread_num, (char *) res);
            free(res);      /* Free memory allocated by thread */
        }
    
        free(tinfo);
        exit(EXIT_SUCCESS);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125

    线程的终止

    新线程以以下方式之一终止:
    (1)它调用pthread_exit(),指定一个退出状态值,该值可用于调用pthrread_join()的同一进程中的另一个线程,即pthrread_join()可以接收pthread_exit()返回的值。
    (2)它从start_routine()返回。这相当于使用return语句中提供的值调用pthread_exit()。
    (3)它被pthread_cancel()取消。
    (4)进程中的任何线程都调用exit(),或者主线程执行main()的返回。这将导致进程中所有线程的终止。

    pthread_exit()

    函数原型:

    #include <pthread.h>
    
    void pthread_exit(void *retval);
    
    // Compile and link with -pthread.
    
    • 1
    • 2
    • 3
    • 4
    • 5

    描述:
    (1)pthread_exit()函数终止调用线程并通过retval返回一个值,该值(如果线程是可连接的)可用于调用pthrea_join()的同一进程中的另一个线程,即可被pthrea_join()接收返回值。

    (2)任何由pthread_cleanup_push()建立的尚未弹出的清理处理程序都会弹出(与它们被推送的顺序相反)并执行。如果线程具有任何特定于线程的数据,则在执行清理处理程序后,将以未指定的顺序调用相应的析构函数。

    (3)当线程终止时,进程共享资源(例如互斥体、条件变量、信号量和文件描述符)不会被释放,使用atexit()注册的函数也不会被调用。

    (4)进程中的最后一个线程终止后,进程通过调用exit()终止,退出状态为零;因此,释放进程共享资源并调用使用atexit()注册的函数。

    返回值:
    此函数不返回调用方。

    错误:
    此函数始终成功。

    注意:
    (1)从除主线程之外的任何线程的start函数执行返回将导致隐式调用pthread_exit(),使用函数的返回值作为线程的退出状态。
    (2)为了允许其他线程继续执行,主线程应该通过调用pthread_exit()而不是exit()来终止。
    (3)retval指向的值不应位于调用线程的堆栈上,因为该堆栈的内容在线程终止后未定义。

    pthread_cancel()

    函数原型:

    #include <pthread.h>
    
    int pthread_cancel(pthread_t thread);
    
    // Compile and link with -pthread.
    
    • 1
    • 2
    • 3
    • 4
    • 5

    描述:
    pthread_cancel()函数向线程thread发送取消请求。目标线程是否以及何时响应取消请求取决于该线程控制的两个属性:其可取消性state和type。

    由pthread_setcancelstate()设置线程的可取消状态可以启用(新线程的默认状态)或禁用。如果线程已禁用取消,则取消请求将保持排队状态,直到线程启用取消。如果线程已启用取消,则其可取消性类型决定何时取消。

    由pthread_setcanceltype()确定的线程的取消类型可以是异步的或延迟的(新线程的默认值)。异步可取消性意味着线程可以随时取消(通常是立即取消,但系统不保证)。延迟可取消性意味着取消将被延迟,直到线程下一次调用作为取消点的函数。pthreads()中提供了作为或可能是取消点的函数列表。

    执行取消请求时,线程将执行以下步骤(按顺序):

    pthread_cancel()
    取消清理处理程序被弹出(与它们被推送的顺序相反)并被调用。参见pthread_cleanup_push()
    以未指定的顺序调用线程特定的数据析构函数。参见pthread_key_create()
    终止线程。参见pthread_exit()

    上述步骤相对于pthread_cancel()调用异步发生;pthread_cancel()的返回状态仅通知调用方取消请求是否已成功排队。

    被取消的线程终止后,使用pthread_join()与该线程的连接将获得pthrea_canceled作为线程的退出状态。(使用线程连接是知道取消已完成的唯一方法。)

    返回值:
    成功时,返回0;出错时,返回非零错误号。

    错误:
    ESRCH,找不到ID为thread的线程。

    线程的等待

    函数原型:

    #include <pthread.h>
    
    int pthread_join(pthread_t thread, void **retval);
    
    // Compile and link with -pthread.
    
    • 1
    • 2
    • 3
    • 4
    • 5

    描述:
    pthread_join()函数等待线程指定的线程终止。如果该线程已经终止,则pthread_join()立即返回。thread指定的线程必须是可连接的。

    如果retval不为空,则pthread_join()将目标线程的退出状态(即,目标线程提供给pthrea_exit()的值)复制到retval所指向的位置。如果目标线程被取消,则PTHREAD_CANCELED被置于retval中。

    如果多个线程同时尝试与同一线程联接,则结果是未定义的。如果调用pthread_join()的线程被取消,那么目标线程将保持可连接状态(即,它不会被分离)。

    返回值:
    成功时,返回0;出错时,它返回错误号。

    错误号:

    错误号含义
    EDEADLK检测到死锁(例如,两个线程试图彼此连接);或thread指定调用线程。
    EINVAL线程不是可连接线程。
    EINVAL另一个线程已在等待加入此线程。
    ESRCH找不到ID为线程的线程。

    线程的属性

    函数原型:

    #include <pthread.h>
    
    int pthread_attr_init(pthread_attr_t *attr);
    int pthread_attr_destroy(pthread_attr_t *attr);
    
    // Compile and link with -pthread.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    描述:
    pthread_attr_init()函数使用默认属性值初始化attr指向的线程属性对象。在这个调用之后,可以使用各种相关函数(下方列出)设置对象的各个属性,然后可以在创建线程的一个或多个pthread_create()调用中使用该对象。

    pthread_attr_setaffinity_np(), 
    pthread_attr_setdetachstate(), 
    pthread_attr_setguardsize(), 
    pthread_attr_setinheritsched(), 
    pthread_attr_setschedparam(), 
    pthread_attr_setschedpolicy(),
    pthread_attr_setscope(), 
    pthread_attr_setstack(), 
    pthread_attr_setstackaddr(), 
    pthread_attr_setstacksize(), 
    pthread_create(), 
    pthread_getattr_np(), 
    pthreads()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    对已初始化的线程属性对象调用pthread_attr_init()会导致未定义的行为。

    当不再需要线程属性对象时,应使用pthread_attr_destroy()函数将其销毁。 销毁线程属性对象对使用该对象创建的线程没有影响。

    线程属性对象被销毁后,可以使用pthread_attr_init()对其重新初始化。任何其他使用已销毁线程属性对象的方法都会产生未定义的结果。

    返回值:
    成功时,这些函数返回0;出错时,它们返回一个非零错误号。

    错误:
    在Linux上,这些函数总是成功的(但可移植和未来验证的应用程序应该处理可能的错误返回)。

    pthread_attr_t类型应被视为不透明的:除通过pthreads函数外,对对象的任何访问都是不可移植的,并产生未定义的结果。

    示例代码:

    #define _GNU_SOURCE     /* To get pthread_getattr_np() declaration */
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    
    #define handle_error_en(en, msg) \
            do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
    
    static void
    display_pthread_attr(pthread_attr_t *attr, char *prefix)
    {
        int s, i;
        size_t v;
        void *stkaddr;
        struct sched_param sp;
    
        s = pthread_attr_getdetachstate(attr, &i);
        if (s != 0)
            handle_error_en(s, "pthread_attr_getdetachstate");
        printf("%sDetach state        = %s\n", prefix,
                (i == PTHREAD_CREATE_DETACHED) ? "PTHREAD_CREATE_DETACHED" :
                (i == PTHREAD_CREATE_JOINABLE) ? "PTHREAD_CREATE_JOINABLE" :
                "???");
    
        s = pthread_attr_getscope(attr, &i);
        if (s != 0)
            handle_error_en(s, "pthread_attr_getscope");
        printf("%sScope               = %s\n", prefix,
                (i == PTHREAD_SCOPE_SYSTEM)  ? "PTHREAD_SCOPE_SYSTEM" :
                (i == PTHREAD_SCOPE_PROCESS) ? "PTHREAD_SCOPE_PROCESS" :
                "???");
    
        s = pthread_attr_getinheritsched(attr, &i);
        if (s != 0)
            handle_error_en(s, "pthread_attr_getinheritsched");
        printf("%sInherit scheduler   = %s\n", prefix,
                (i == PTHREAD_INHERIT_SCHED)  ? "PTHREAD_INHERIT_SCHED" :
                (i == PTHREAD_EXPLICIT_SCHED) ? "PTHREAD_EXPLICIT_SCHED" :
                "???");
    
        s = pthread_attr_getschedpolicy(attr, &i);
        if (s != 0)
            handle_error_en(s, "pthread_attr_getschedpolicy");
        printf("%sScheduling policy   = %s\n", prefix,
                (i == SCHED_OTHER) ? "SCHED_OTHER" :
                (i == SCHED_FIFO)  ? "SCHED_FIFO" :
                (i == SCHED_RR)    ? "SCHED_RR" :
                "???");
    
        s = pthread_attr_getschedparam(attr, &sp);
        if (s != 0)
            handle_error_en(s, "pthread_attr_getschedparam");
        printf("%sScheduling priority = %d\n", prefix, sp.sched_priority);
    
        s = pthread_attr_getguardsize(attr, &v);
        if (s != 0)
            handle_error_en(s, "pthread_attr_getguardsize");
        printf("%sGuard size          = %d bytes\n", prefix, v);
    
        s = pthread_attr_getstack(attr, &stkaddr, &v);
        if (s != 0)
            handle_error_en(s, "pthread_attr_getstack");
        printf("%sStack address       = %p\n", prefix, stkaddr);
        printf("%sStack size          = 0x%zx bytes\n", prefix, v);
    }
    
    static void *
    thread_start(void *arg)
    {
        int s;
        pthread_attr_t gattr;
    
        /* pthread_getattr_np() is a non-standard GNU extension that
           retrieves the attributes of the thread specified in its
           first argument */
    
        s = pthread_getattr_np(pthread_self(), &gattr);
        if (s != 0)
            handle_error_en(s, "pthread_getattr_np");
    
        printf("Thread attributes:\n");
        display_pthread_attr(&gattr, "\t");
    
        exit(EXIT_SUCCESS);         /* Terminate all threads */
    }
    
    int
    main(int argc, char *argv[])
    {
        pthread_t thr;
        pthread_attr_t attr;
        pthread_attr_t *attrp;      /* NULL or &attr */
        int s;
    
        attrp = NULL;
    
        /* If a command-line argument was supplied, use it to set the
           stack-size attribute and set a few other thread attributes,
           and set attrp pointing to thread attributes object */
    
        if (argc > 1) {
            int stack_size;
            void *sp;
    
            attrp = &attr;
    
            s = pthread_attr_init(&attr);
            if (s != 0)
                handle_error_en(s, "pthread_attr_init");
    
            s = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
            if (s != 0)
                handle_error_en(s, "pthread_attr_setdetachstate");
    
            s = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
            if (s != 0)
                handle_error_en(s, "pthread_attr_setinheritsched");
    
            stack_size = strtoul(argv[1], NULL, 0);
    
            s = posix_memalign(&sp, sysconf(_SC_PAGESIZE), stack_size);
            if (s != 0)
                handle_error_en(s, "posix_memalign");
    
            printf("posix_memalign() allocated at %p\n", sp);
    
            s = pthread_attr_setstack(&attr, sp, stack_size);
            if (s != 0)
                handle_error_en(s, "pthread_attr_setstack");
        }
    
        s = pthread_create(&thr, attrp, &thread_start, NULL);
        if (s != 0)
            handle_error_en(s, "pthread_create");
    
        if (attrp != NULL) {
            s = pthread_attr_destroy(attrp);
            if (s != 0)
                handle_error_en(s, "pthread_attr_destroy");
        }
    
        pause();    /* Terminates when other thread calls exit() */
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146

    无原子操作

    在多个线程中,对一个变量不断操作,如果没有原子操作会怎么样?
    示例代码:

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    
    #define THREAD_SIZE		10
    
    // 10 * 100000
    void *func(void *arg) {
    
    	int *pcount = (int *)arg;
    
    	int i = 0;
    	while (i++ < 100000) {
    		(*pcount)++;
    		usleep(1);
    
    	}
    
    
    }
    
    int main(int argc, char **argv)
    {
    	pthread_t threadid[THREAD_SIZE] = { 0 };
    
    	int i = 0;
    	int count = 0;
    	for (i = 0; i < THREAD_SIZE; i++) {
    		pthread_create(&threadid[i], NULL, func, &count);
    	}
    
    	// 1000w 
    	for (i = 0; i < 10; i++) {
    		printf("count = %d\n", count);
    		sleep(1);
    	}
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    上述代码执行结果理论上是1000000,但是最后结果是994656。也就是无原子操作下的执行结果小于理论值。
    原因在于,执行idx++时汇编代码是:

    Mov [idx], %eax
    Inc %eax
    Mov %eax,[idx]
    
    • 1
    • 2
    • 3

    也就是c语言是一条语句,但真正执行时是三条命令。在无原子操作时,就可能出现如下情况:

    线程1 线程2 Mov [idx], %eax 切换到 Mov [idx], %eax Inc %eax Mov %eax,[idx] 切换到 Inc %eax Mov %eax,[idx] 线程1 线程2

    原意要自增两次,然而实际只自增了一次,因此无原子操作下的执行结果小于理论值。

    互斥锁

    让临界资源只允许在一个线程中执行。

    pthread_mutex_init()

    函数原型:

    #include <pthread.h>
    int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
    
    • 1
    • 2

    函数描述:
    互斥锁的初始化。
    pthread_mutex_init() 函数是以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性。如果参数attr为空(NULL),则使用默认的互斥锁属性,默认属性为快速互斥锁 。
    互斥锁的属性在创建锁的时候指定,在实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。

    返回:
    成功会返回零,其他任何返回值都表示出现了错误。
    成功后,互斥锁被初始化为未锁住态。

    pthread_mutex_destroy()

    用于注销一个互斥锁,函数原型:

    #include <pthread.h>
    int pthread_mutex_destroy(pthread_mutex_t *mutex)
    
    • 1
    • 2

    销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此pthread_mutex_destroy()仅仅检查锁状态(锁定状态则返回EBUSY)。

    pthread_mutex_lock()和pthread_mutex_trylock()

    函数原型:

    #include <pthread.h>
    
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    
    • 1
    • 2
    • 3
    • 4

    描述:
    互斥引用的互斥对象通过调用 pthread_mutex_lock()被锁定。如果互斥锁已被锁定,则调用线程将阻塞,直到互斥体变为可用。此操作将返回由处于锁定状态的互斥所引用的互斥对象,其中调用线程是其所有者。
    函数 pthread_mutex_trylock()与 pthread_mutex_lock()相同,只是如果互斥引用的互斥对象当前被锁定(由任何线程,包括当前线程锁定),则调用将立即返回。

    互斥类型含义
    PTHREAD_MUTEX_NORMAL不提供死锁检测。尝试重新锁定互斥锁会导致死锁。如果线程尝试解锁它尚未锁定的互斥锁或已解锁的互斥体,则会导致未定义的行为。
    PTHREAD_MUTEX_ERRORCHECK提供错误检查。如果线程尝试重新锁定已锁定的互斥锁,则会返回错误。如果线程尝试解锁尚未锁定的互斥体或已解锁的互斥体,则将返回错误。
    PTHREAD_MUTEX_RECURSIVE互斥锁将保留锁定计数的概念。当线程首次成功获取互斥锁时,锁定计数将设置为 1。每次线程重新锁定此互斥锁时,锁定计数都会递增 1。每次线程解锁互斥体时,锁定计数都会减少 1。当锁定计数达到零时,互斥锁将可供其他线程获取。如果线程尝试解锁尚未锁定的互斥体或已解锁的互斥体,则将返回错误。
    PTHREAD_MUTEX_DEFAULT尝试递归锁定互斥会导致未定义的行为。如果互斥体未被调用线程锁定,则尝试解锁该互斥体会导致未定义的行为。如果互斥体未锁定,则尝试解锁互斥体会导致未定义的行为。

    返回值:
    如果成功,pthread_mutex_lock()和 pthread_mutex_unlock() 函数返回零。否则,将返回一个错误号以指示错误。
    如果获取了互斥引用的互斥对象上的锁,则函数 pthread_mutex_trylock() 返回零。否则,将返回一个错误号以指示错误。

    如果出现以下情况,pthread_mutex_lock()和pthread_mutex_trylock()函数将失败:

    错误代码含义
    EINVAL互斥体是使用具有值PTHREAD_PRIO_PROTECT的协议属性创建的,并且调用线程的优先级高于互斥体的当前优先级上限。
    EBUSY无法获取互斥体,因为它已被锁定。
    EINVAL互斥体指定的值不引用初始化的互斥体对象。
    EAGAIN无法获取互斥锁,因为已超过互斥锁的最大递归锁数。
    EDEADLK当前线程已拥有互斥体。
    EPERM当前线程不拥有互斥体。

    这些函数不会返回错误代码EINTR。

    pthread_mutex_unlock()

    函数原型:

    #include <pthread.h>
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    
    • 1
    • 2

    描述:
    pthread_mutex_unlock() 函数释放互斥引用的互斥对象。释放互斥体的方式取决于互斥体的 type 属性。如果在调用 pthread_mutex_unlock()时,互斥所引用的互斥对象上存在阻塞的线程,从而导致互斥体变为可用,则调度策略用于确定哪个线程应获取互斥。(在PTHREAD_MUTEX_RECURSIVE互斥锁的情况下,当计数达到零并且调用线程不再对此互斥锁时,互斥锁将变为可用)。

    如果信号被传递到等待互斥体的线程,则在信号处理程序返回时,线程将恢复等待互斥体,就好像它没有被中断一样。

    返回值:
    如果成功,返回零。否则,将返回一个错误号以指示错误。

    示例代码

    #include 
    #include 
    #include 
    
    #define THREAD_SIZE		10
    
    #define ADD_MUTEX_LOCK	1
    
    #if ADD_MUTEX_LOCK
    pthread_mutex_t mutex;
    #endif
    
    // 10 * 100000
    void *func(void *arg) {
    
    	int *pcount = (int *)arg;
    
    	int i = 0;
    	while (i++ < 100000) {
    #if 0
    		(*pcount)++;
    #elif ADD_MUTEX_LOCK
    		pthread_mutex_lock(&mutex);
    		(*pcount)++;
    		pthread_mutex_unlock(&mutex);
    #endif
    		usleep(1);
    
    	}
    
    
    }
    
    int main(int argc, char **argv)
    {
    	pthread_t threadid[THREAD_SIZE] = { 0 };
    
    #if ADD_MUTEX_LOCK
    	pthread_mutex_init(&mutex, NULL);
    #endif
    
    	int i = 0;
    	int count = 0;
    	for (i = 0; i < THREAD_SIZE; i++) {
    		pthread_create(&threadid[i], NULL, func, &count);
    	}
    
    	// 1000w 
    	for (i = 0; i < 50; i++) {
    		printf("count = %d\n", count);
    		sleep(1);
    	}
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    上述代码执行结果是1000000。也就是互斥锁下的执行结果等于理论值。

    自旋锁

    自旋锁的接口和mutex类似。
    函数原型:

    #include <pthread.h>
    // 1. 销毁自旋锁
    int   pthread_spin_destroy(pthread_spinlock_t *lock);
    // 2. 初始化自旋锁
    int   pthread_spin_init(pthread_spinlock_t *lock, int attr);
    // 3. 自旋锁上锁(阻塞)
    int   pthread_spin_lock(pthread_spinlock_t *lock);
    // 4. 自旋锁上锁(非阻塞)
    int   pthread_spin_trylock(pthread_spinlock_t *lock);
    // 5. 自旋锁解锁
    int   pthread_spin_unlock(pthread_spinlock_t *lock);
    以上函数成功都返回0.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    示例代码

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    
    #define THREAD_SIZE		10
    
    #define ADD_MUTEX_LOCK	0
    #define ADD_SPIN_LOCK	1
    
    #if ADD_MUTEX_LOCK
    pthread_mutex_t mutex;
    #endif
    #if ADD_SPIN_LOCK
    pthread_spinlock_t spinlock;
    #endif
    
    // 10 * 100000
    void *func(void *arg) {
    
    	int *pcount = (int *)arg;
    
    	int i = 0;
    	while (i++ < 100000) {
    #if 0
    		(*pcount)++;
    #elif ADD_MUTEX_LOCK
    		pthread_mutex_lock(&mutex);
    		(*pcount)++;
    		pthread_mutex_unlock(&mutex);
    #elif ADD_SPIN_LOCK
    		pthread_spin_lock(&spinlock);
    		(*pcount)++;
    		pthread_spin_unlock(&spinlock);
    #endif
    		usleep(1);
    
    	}
    
    
    }
    
    int main(int argc, char **argv)
    {
    	pthread_t threadid[THREAD_SIZE] = { 0 };
    
    #if ADD_MUTEX_LOCK
    	pthread_mutex_init(&mutex, NULL);
    #elif ADD_SPIN_LOCK
    	pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
    #endif
    
    	int i = 0;
    	int count = 0;
    	for (i = 0; i < THREAD_SIZE; i++) {
    		pthread_create(&threadid[i], NULL, func, &count);
    	}
    
    	// 1000w 
    	for (i = 0; i < 50; i++) {
    		printf("count = %d\n", count);
    		sleep(1);
    	}
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    上述代码执行结果是1000000。也就是自旋锁下的执行结果等于理论值。

    互斥锁与自旋锁的区别

    互斥锁与自旋锁的接口类似,但是底层实现有一定差异。
    mutex在发现锁已经被占用时,会让出CPU资源,然后等待有解锁时唤醒去抢锁。
    spin在发现锁已经被占用时,会一直等着,直到抢到锁。

    死锁

    死锁的两种情况:
    (1)如果两个线程先后调用两次lock,第二次调用lock时,由于锁已被占用,该线程会挂起等待别的线程释放锁,然后锁正是被自己占用着的,该线程又被挂起不能释放锁,因此就永远处于挂起等待状态了,进入死锁。
    (2)线程1和线程2。线程1获得锁1,线程2获得锁2,此时线程1调用lock企图获得锁2,结果是需要挂起等待线程2释放锁2,而此时线程2也调用了lock企图获得锁1,结果是线程2挂起等待线程1释放锁1,进入死锁。

    避免死锁:
    (1)共享资源操作前一定要获得锁。
    (2)完成操作以后一定要释放锁。
    (3)尽量短时间地占用锁。
    (4)有多锁, 如获得顺序是abc连环扣, 释放顺序也应该是abc。
    (5)线程错误返回时应该释放它所获得的锁。
    (6)写程序是尽量避免同时获得多个锁。如果一定要这么做,所有线程在需要多个锁时都按相同的先后顺序获得锁,则不会出现死锁。

    原子操作

    原子操作就是用一条指令解决问题;多条执行命令变为一条执行命令,使其不可分割。

    CAS,全称Compare And Swap。翻译过来就是先比较再赋值,顺序不可变;也就是先对比,如果值一致再赋值,如果不一致就不赋值。

    常见的原子操作:
    (1)加,add
    (2)减,sub
    (3)自增,inc
    (4)自减,dec
    (5)比较赋值,cas

    注意,c语言的一条语句执行可能有副作用,但原子操作是没有副作用的。
    示例代码:

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    
    #define THREAD_SIZE		10
    
    #include <sys/time.h>
    #define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)
    
    // 原子操作
    int inc(int *value,int add) 
    {
    	int old;
    	__asm__ volatile(
    		"lock; xaddl %2, %1;"
    		: "=a" (old)
    		: "m" (*value),"a"(add)
    		: "cc","memory"
    		);
    
    	return old;
    }
    
    
    // 10 * 1000000
    void *func(void *arg) {
    
    	int *pcount = (int *)arg;
    
    	int i = 0;
    	while (i++ < 1000000) {
    	
    		inc(pcount, 1);
    		//usleep(1);
    
    	}
    }
    
    int main(int argc, char **argv)
    {
    	pthread_t threadid[THREAD_SIZE] = { 0 };
    
    	// 统计执行时间
    	struct timeval tv_start;
    	gettimeofday(&tv_start, NULL);
    
    	int i = 0;
    	int count = 0;
    	for (i = 0; i < THREAD_SIZE; i++) {
    		pthread_create(&threadid[i], NULL, func, &count);
    	}
    
    #if 0
    	// 1000w 
    	for (i = 0; i < 50; i++) {
    		printf("count = %d\n", count);
    		sleep(1);
    	}
    #else
    	for (i = 0; i < THREAD_SIZE; i++) {
    		pthread_join(threadid[i], NULL); //
    	}
    #endif
    
    	struct timeval tv_end;
    	gettimeofday(&tv_end, NULL);
    	int time_used = TIME_SUB_MS(tv_end, tv_start);
    	printf("time_used: %d\n", time_used);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    总结

    对临界资源操作时,常用原子操作和锁。
    锁有互斥锁、自旋锁、读写锁等,其他应用程序实现的业务锁如悲观锁、乐观锁等。
    在两种情况下容易陷入死锁:
    (1)线程调用两次lock,第一次已经获得锁,第二次发现锁已占用进入等待,而锁是被自己占用,进入无线等待的死锁。
    (2)多个线程多个锁的情况,线程1获得锁1然后请求锁2,线程2获得锁2然后请求锁1,互相等待,进入锁。

    原子操作就是通过一条指令解决问题,封装的CAS将多条执行命令变为一条执行命令,使其不可分割。

    后言

    本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux系统提升感兴趣的读者,可以点击链接,详细查看详细的服务:C/C++服务器

  • 相关阅读:
    开发调试:提高代码质量与开发效率的关键步骤
    java计算机毕业设计晨光文具店进销存系统设计与开发源码+系统+lw文档+mysql数据库+部署
    C语言实例_调用SQLITE数据库完成数据增删改查
    1 两数之和
    iOS开发 - 抛开表面看本质之iOS常用架构(MVC,MVP,MVVM)
    栈的多种C语言实现 编程
    Linux操作(查询日志)
    异常检测 | MATLAB实现基于支持向量机和孤立森林的数据异常检测(结合t-SNE降维和DBSCAN聚类)
    ECCV 2022 | MVDG:一种用于域泛化的统一多视图框架
    景联文科技提供4D-BEV标注工具:提升自动驾驶感知能力的精准数据支持
  • 原文地址:https://blog.csdn.net/Long_xu/article/details/126915661