APUE学习之路 (线程的基本操作)


本章主要讲线程的基本操作,线程的创建、取消、终止、同步等等。
实际项目中多线程用得比较多,因为多线程是先有标准后有实现的,所以不会向多进程那样在不同平台上有许多不同的情况。
线程就是一个正在运行的函数。
C 语言线程有很多标准,POSIX 是其中的一种。
POSIX 是一套标准,而不是一种实现。

正因为 POSIX 是一套标准而不是实现,所以 POSIX 只是规定了 pthread_t 作为线程标识符,但是并没有规定它必须是由什么类型组成的,所以在有的平台上它可能是 int,有些平台上它可能是 struct,还有些平台上它可能是 union,所以不要直接操作这个类型,而是要使用 POSIX 规定的各种线程函数来操作它。
类似于标准 IO 里 FILE ,标准制定出来的很多东西都是这种风格的,它为你提供一个数据类型而不让你直接对这个类型操作,要通过它定义的一系列函数来实现对这个类型的操作,这样就在各个平台上实现统一的接口了,所以这样做才能让标准制定出来的东西具有较好的可移植性。
pthread_t 是个很重要的东西,我们所有使用 PSOIX 标准的线程操作都是围绕着它来进行的,通过它配合各种函数就可以对线程进行各种花样操作。

在前面进程的博文中介绍过几种 ps(1) 命令的使用方式,用来观察进程的关系和状态。再补充一个 ps(1) 命令的组合,用来查看线程的情况,方便调试程序。

>$ ps ax -L
PID   LWP TTY      STAT   TIME COMMAND
 1     1   ?        Ss     0:02 /sbin/init
 2     2   ?        S      0:00 [kthreadd]
 3     3   ?        S      0:00 [ksoftirqd/0]
 877   877 ?        Ss     0:06 dbus-daemon --system --fork
 948   948 ?        Ssl    0:00 /usr/sbin/ModemManager
 948   965 ?        Ssl    0:00 /usr/sbin/ModemManager
 948   975 ?        Ssl    0:00 /usr/sbin/ModemManager
 956   956 ?        Ss     0:00 /usr/sbin/bluetoothd
>$

PID 是进程号,LWP 是线程 ID。
这里看到的 PID 为 948 的进程有三个 LWP,它们就是三个线程。

pthread 基本函数

pthread_equal(3)

pthread_equal - compare thread IDs
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
Compile and link with -pthread.

pthread_equal(3) 用于比较两个线程标识符是否相同,为什么不能使用 if (t1 == t2) 的方式比较两个线程标识符呢?如上文所述,因为你不知道 pthread_t 是什么类型的,所以永远不要自己直接操作它。

pthread_self(3)

pthread_self - obtain ID of the calling thread
#include <pthread.h>
pthread_t pthread_self(void);
Compile and link with -pthread.

一个进程可以通过 getpid(2) 函数获得当前进程 ID 号,同理,pthread_self(3) 获得当前线程 ID 。

pthread_create(3)

pthread_create - create a new thread
#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.

pthread_create(3) 函数的作用就是创建一个新线程。
参数列表:
  thread:由函数回填的线程标识符,它来唯一的标识产生的新线程,后面我们只要需要操作新线程就需要用到它;
  attr:线程属性使用 NULL,也就是使用默认属性。
  start_routine:线程的执行函数;入参是 void*,返回值是 void*,这两个值的类型都是百搭的,任何类型都可以在这使用。
  arg:传递给 start_routine 的 void* 参数。
  返回值:成功返回 0;失败返回 errno。为什么线程函数返回的是 errno 呢?因为在一些平台上 error 是全局变量,如果大家都使用同一个全局变量,在多线程的情况下就可能会出现竞争,所以 POSIX 的线程函数一般在失败的时候都是直接返回 errno 的,这样就避免了某些平台 errno 的缺陷了。

新线程和当前的线程是两个兄弟线程,他们是平等的,没有父子关系。
新线程被创建之后,这两个线程哪个先执行是不确定的,由调度器来决定。如果你希望哪个线程一定先执行,那么就在其它线程中使用类似 sleep(3) 的函数让它们等一会儿再运行。

pthread_exit(3)

pthread_exit - terminate calling thread
#include <pthread.h>
void pthread_exit(void *retval);
Compile and link with -pthread.

在线程执行函数中调用,作用是退出当前线程,并将返回值通过 retval 参数返回给调用 pthread_join(3) 函数的地方,如果不需要返回值可以传入 NULL。
pthread_join(3) 是为线程收尸的函数,稍后会详细介绍,先看个例子,了解下线程的创建。

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

static void *func(void *p)
{
    puts("Thread is working.");
    sleep(10); // 延时是为了方便我们使用 ps(1) 命令验证线程是否被创建了
    pthread_exit(NULL);
//    return NULL;
}

int main()
{
    pthread_t tid;
    int err;
    puts("Begin!");
    // 创建线程
    err = pthread_create(&tid,NULL,func,NULL);
    if (err) {
        fprintf(stderr,"pthread_create():%s\n",strerror(err));
        exit(1);
    }
    // 为线程收尸
    pthread_join(tid,NULL);
    puts("End!");
    exit(0);
}

使用 ps(1) 命令查看线程状态

>$ gcc -Wall create.c -o create -pthread
>$ ./create 
Begin!
Thread is working.
End!
>$
// 在线程结束之前打开另一个终端,验证线程的状态
>$ ps ax -L
  PID   LWP TTY      STAT   TIME COMMAND
 4354  4354 pts/1    Sl+    0:00 ./create
 4354  4355 pts/1    Sl+    0:00 ./create
>$

通过 ps(1) 命令的验证,可以看到这两个线程拥有同一个 PID 不同的 LWP,所以可以直观看出来我们的线程创建成功了,注意,编译 POSIX 线程程序的时候需要使用 -pthread 参数,这个其实在 man 手册里已经说得很清楚了。

pthread_cancel(3)

 pthread_cancel - send a cancellation request to a thread
#include <pthread.h>
int pthread_cancel(pthread_t thread);
Compile and link with -pthread.

pthread_cancel(3) 函数的作用是取消同一个进程中的其它线程。
为什么要取消线程呢?当一个线程没有必要继续执行下去时,我们又没法为它收尸,所以就需要先取消这个线程,然后再为它收尸。
比如在使用多线程遍历一个很大的二叉树查找一个数据时,其中某一个线程找到了要查找的数据,那么其它线程就没有必要继续执行了,所以就可以取消它们了。
注意 pthread_cancel(3) 并不等待线程终止,它仅仅提出请求。
而线程收到这个请求也不会立即终止,线程要执行到取消点才能被取消,关于取消点在下一篇博文中会介绍。

pthread_join(3)

pthread_join - join with a terminated thread
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Compile and link with -pthread.

为线程收尸,在上面的栗子中大家已经见到了。不像 wait(2) 函数,线程之间谁都可以为别人收尸,它们之间是没有父子关系的。而 wait(2) 函数只能是由父进程对子进程收尸。
参数列表:
  thread:指定为哪个线程收尸;
  retval:这个二级指针是什么呢?它就是线程在退出的时候的返回值(pthread_exit(3) 的参数),它会把线程的返回值的地址回填到这个参数中。

线程清理处理程序(thread cleanup handler)

pthread_cleanup_push, pthread_cleanup_pop - push and pop thread cancellation clean-up handlers
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
Compile and link with -pthread.

就像在进程级别使用 atexit(3) 函数挂钩子函数一样,线程可能也需要在结束时执行一些清理工作,这时候就需要派出线程清理处理程序上场了。钩子函数的调用顺序也是逆序的,也就是执行顺序与注册顺序相反。

这两个是带参的宏而不是函数,所以必须成对使用,而且必须先使用 pthread_cleanup_push 再使用 pthread_cleanup_pop,否则会报语法错误,括号不匹配。
参数列表:
  routine:钩子函数。
  arg:传递给钩子函数的参数。
  execute:0 不调用该钩子函数;1 调用该钩子函数。

pthread_cleanup_pop 写到哪都行,只要写了让语法不报错就行,就算你把它写到 pthread_exit(3) 下面也没问题,但是 execute 参数就看不到了,所以无论 pthread_cleanup_pop 的参数是什么,所有注册过的钩子函数都会被执行。

#include <pthread.h>
void routine (void *p) {}
void* fun (void *p)
{
        pthread_cleanup_push(routine, NULL);
        这里是其它代码
        pthread_cleanup_pop(1);
}

预编译,查看宏替换的结果:

>$ gcc -E cleanup.c
void routine (void *p) {}

void* fun (void *p)
{
 do { __pthread_unwind_buf_t __cancel_buf; void (*__cancel_routine) (void *) = (routine); void *__cancel_arg = (((void *)0)); int not_first_call = __sigsetjmp ((struct __jmp_buf_tag *) (void *) __cancel_buf.__cancel_jmp_buf, 0); if (__builtin_expect (not_first_call, 0)) { __cancel_routine (__cancel_arg); __pthread_unwind_next (&__cancel_buf); } __pthread_register_cancel (&__cancel_buf); do {;
 这里是其它代码
 do { } while (0); } while (0); __pthread_unregister_cancel (&__cancel_buf); if (1) __cancel_routine (__cancel_arg); } while (0);
}

通过预编译可以看出来 pthread_cleanup_push 和 pthread_cleanup_pop 两个宏被替换了,并且每个宏仅定义了一半,如果不成对写另一个宏编译的时候就会报括号不匹配的错误。

pthread_detach(3)

pthread_detach - detach a thread
#include <pthread.h>
int pthread_detach(pthread_t thread);
Compile and link with -pthread.

pthread_detach(3) 函数用于分离线程,被分离的线程是不能被收尸的。

互斥量(pthead_mutex_t)

多线程就是为了充分利用硬件资源,使程序可以并发的运行,但是只要是并发就会遇到竞争的问题,互斥量就是解决竞争的多种手段之一。
在介绍互斥量之前我们先思考一个问题:如何让 20 个线程同时从一个文件中读取数字,累加 1 然后再写入回去,并且保证程序运行之后文件中的数值比运行程序之前大 20?

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

#define BUFSIZE 32

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

static void *fun (void *p)
{
    int fd = -1;
    long long n = 0;
    char buf[BUFSIZE] = "";
    fd = open(p, O_RDWR | O_CREAT, 0664);
    /* if err */
    pthread_mutex_lock(&mutex);
    read(fd, buf, BUFSIZE);
    lseek(fd, 0, SEEK_SET);
    n = atoll(buf);
    snprintf(buf, BUFSIZE, "%lld\n", ++n);
    write(fd, buf, strlen(buf));
    close(fd);
    pthread_mutex_unlock(&mutex);
    pthread_exit(NULL);
}

int main (int argc, char **argv)
{
    pthread_t tids[20];
    int i = 0;
    if (argc < 2) {
        fprintf(stderr, "Usage %s <filename>\n", argv[0]);
        return -1;
    }
    for (i = 0; i < 20; i++) {
        pthread_create(&tids[i], NULL, fun, argv[1]);
        /* if err */
    }
    for (i = 0; i < 20; i++) {
        pthread_join(tids[i], NULL);
    }
    pthread_mutex_destroy(&mutex);
    return 0;
}

程序中每一个线程都要做:读取文件 -> 累加 1 -> 写入文件 的动作,如果 20 个线程同时做这件事,那么就很有可能多个线程读到的数据是相同的,这样累加的结果也就是相同的了,没办法保证 20 个线程每个人读到的数据都是独一无二的。
怎么样才能让 20 个线程读到独一无二的数值呢?很简单,让 读取文件 -> 累加 1 -> 写入文件 的这个动作同一时刻只能有一个线程来做,这样每个线程读取到的数值都是上一个线程写入的数值了。那么 读取文件 -> 累加 1 -> 写入文件 这段代码(也就是发生竞争的这段区域)就叫做“临界区”。

互斥量正如它的名字描述的一般,可以使各个线程实现互斥的效果。由它来保护临界区每次只能由一个线程进入,当一个线程想要进入临界区之前需要先抢锁(加锁),如果能抢到锁就进入临界区工作,并且要在离开的时候解锁以便让其它线程可以抢到锁进入临界区;如果没有抢到锁则进入阻塞状态等待锁被释放然后再抢锁。

要在进入临界区之前加锁,在退出临界区的时候解锁。
与 ptread_t 一样,互斥量也使用一种数据类型来表示,它使用 pthread_mutex_t 类型来表示。
初始化互斥量有两种方式:
  1)用宏初始化:如同使用默认属性;
  2)使用 pthread_mutex_init(3) 函数初始化,可以为互斥量指定属性。

pthread_mutex_t 使用完成之后需要使用 pthread_mutex_destroy(3) 函数销毁,否则会导致内存泄漏
一般什么情况使用宏初始化,什么情况使用函数初始化互斥量呢?请看下面的伪代码:

/* 定义并赋值 */
type name = value; 
// 定义并赋值。使用 PTHREAD_MUTEX_INITIALIZER 宏初始化互斥量必须在这种情况时。

/* 先定义再赋值 */
type name; 
// 定义
name = valu; 
// 赋值。这种情况不允许使用 PTHREAD_MUTEX_INITIALIZER 宏初始化互斥量,只能使用 
// pthread_mutex_init(3) 函数初始化互斥量。

前面说了,要在进入临界区之前加锁,在退出临界区的时候解锁,了解一下加锁和解锁的函数。

pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock -  lock and unlock a mutex
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

_lock() 是阻塞加锁,当抢锁的时候没有抢到就死等,直到别人通过 _unlock() 把锁解锁再抢。
_trylock() 是尝试加锁,无论能否抢到锁都返回。
临界区是每个线程要单独执行的,所以临界区中的代码执行时间越短越好。
了解了互斥量之后,看一道经典的面试题:用 4 个线程疯狂的打印 abcd 持续 5 秒钟,但是要按照顺序打印,不能乱序。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#define THRNUM        4
static pthread_mutex_t mut[THRNUM];

static int next(int a)
{
    if(a+1 == THRNUM)
        return 0;
    return a+1;
}

static void *thr_func(void *p)
{
    int n = (int)p;
    int ch = n + 'a';
    while (1) {
        pthread_mutex_lock(mut+n);
        write(1,&ch,1);
        pthread_mutex_unlock(mut+next(n));
    }
    pthread_exit(NULL);
}

int main()
{
    int i,err;
    pthread_t tid[THRNUM];
    for (i = 0 ; i < THRNUM ; i++) {
        pthread_mutex_init(mut+i,NULL);
        pthread_mutex_lock(mut+i);
        err = pthread_create(tid+i,NULL,thr_func,(void *)i);
        if (err) {
            fprintf(stderr,"pthread_create():%s\n",strerror(err));
            exit(1);
        }
    }
    pthread_mutex_unlock(mut+0);
    alarm(5);
    for(i = 0 ; i < THRNUM ; i++)
        pthread_join(tid[i],NULL);
    exit(0);
}

上面这段代码通过多个互斥量实现了一个锁链的结构巧妙的实现了需求。
首先定义 4 个互斥量,然后创建 4 个线程,每个互斥量对应一个线程,每个线程负责打印一个字母。4 个线程刚刚被创建好时,4 把锁都处于锁定状态,4 个线程全部都阻塞在临界区之外,等 4 个线程全部都创建好之后解锁其中一把锁。被解锁的线程首先将自己的互斥量上锁,然后打印字符再解锁下一个线程对应的互斥量,然后再次等待自己被解锁。如此往复,使 4 个线程有条不紊的循环执行 锁定自己 -> 打印字符 -> 解锁下一个线程 的步骤,这样打印到控制台上的 abcd 就是有序的了。
从上面的例子可以看出来:互斥量限制的是一段代码能否执行,而不是一个变量或一个资源。
上面的代码虽然使用锁链巧妙的完成了任务,但并不是最优方案,后面会介绍介绍条件变量(pthread_cond_t)。

多线程并发版的令牌桶

大家还记得我们在上一篇博文中提到过令牌桶吗?当时只是实现了一个简单的令牌桶,这次写一个通用的多线程并发版的令牌桶。

/* mytbf.h */
#ifndef MYTBF_H__
#define MYTBF_H__
#define MYTBF_MAX        1024
typedef void mytbf_t;
mytbf_t *mytbf_init(int cps,int burst);
int mytbf_fetchtoken(mytbf_t *,int);
int mytbf_returntoken(mytbf_t *,int );
void mytbf_destroy(mytbf_t *);
#endif
/* mytbf.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <string.h>
#include "mytbf.h"
/* 每一个令牌桶 */
struct mytbf_st
{
    int cps; // 速率
    int burst; // 令牌上限
    int token; // 可用令牌数量
    int pos; // 当前令牌桶在 job 数组中的下标
    pthread_mutex_t mut; // 用来保护令牌竞争的互斥量
};

/* 所有的令牌桶 */
static struct mytbf_st *job[MYTBF_MAX];
/* 用来保护令牌桶数组竞争的互斥量 */
static pthread_mutex_t mut_job = PTHREAD_MUTEX_INITIALIZER;
/* 添加令牌的线程 ID */
static pthread_t tid;
/* 初始化添加令牌的线程 */
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
/* 线程处理函数:负责定时向令牌桶中添加令牌 */
static void *thr_alrm(void *p)
{
    int i;
    while (1) {
        pthread_mutex_lock(&mut_job);
        // 遍历所有的桶
        for (i = 0 ; i < MYTBF_MAX; i++) {
            // 为可用的桶添加令牌
            if (job[i] != NULL) {
                pthread_mutex_lock(&job[i]->mut);
                job[i]->token += job[i]->cps;
                // 桶中可用的令牌不能超过上限
                if (job[i]->token > job[i]->burst)
                    job[i]->token = job[i]->burst;
                pthread_mutex_unlock(&job[i]->mut);
            }
        }
        pthread_mutex_unlock(&mut_job);
        // 等待一秒钟后继续添加令牌
        sleep(1);
    }
    pthread_exit(NULL);
}

static void module_unload(void)
{
    int i;
    pthread_cancel(tid);
    pthread_join(tid,NULL);
    pthread_mutex_lock(&mut_job);
    for (i = 0 ; i < MYTBF_MAX ; i++) {
        if (job[i] != NULL) {
            // 互斥量使用完毕不要忘记释放资源
            pthread_mutex_destroy(&job[i]->mut);
            free(job[i]);
        }
    }
    pthread_mutex_unlock(&mut_job);
    pthread_mutex_destroy(&mut_job);
}

static void module_load(void)
{
    int err;
    err = pthread_create(&tid,NULL,thr_alrm,NULL);
    if (err) {
        fprintf(stderr,"pthread_create():%s\n",strerror(err));
        exit(1);
    }
    atexit(module_unload);
}
/*
 * 为了不破坏调用者对令牌桶操作的原子性,
 * 在该函数内加锁可能会导致死锁,
 * 所以该函数内部无法加锁,
 * 必须在调用该函数之前先加锁。
 */
static int get_free_pos_unlocked(void)
{
    int i;
    for (i = 0 ; i < MYTBF_MAX; i++)
        if(job[i] == NULL)
            return i;
    return -1;
}

mytbf_t *mytbf_init(int cps,int burst)
{
    struct mytbf_st *me;
    int pos;
    pthread_once(&init_once,module_load);
    me = malloc(sizeof(*me));
    if(me == NULL)
        return NULL;
    me->cps = cps;
    me->burst = burst;
    me->token = 0;
    pthread_mutex_init(&me->mut,NULL);
    pthread_mutex_lock(&mut_job);
    pos = get_free_pos_unlocked();
    if (pos < 0) {
        // 带锁跳转不要忘记先解锁再跳转
        pthread_mutex_unlock(&mut_job);
        free(me);
        return NULL;
    }
    me->pos = pos;
    job[pos] = me;
    pthread_mutex_unlock(&mut_job);
    return me;
}

static inline int min(int a,int b)
{
    return (a < b) ? a : b;
}

int mytbf_fetchtoken(mytbf_t *ptr,int size)
{
    int n;
    struct mytbf_st *me = ptr;
    if(size < 0)
        return -EINVAL;
    pthread_mutex_lock(&me->mut);
    // 令牌数量不足,等待令牌被添加进来
    while (me->token <= 0) {
        // 先解锁,出让调度器让别人先跑起来,然后再抢锁检查令牌是否够用
        pthread_mutex_unlock(&me->mut);
        sched_yield();
        pthread_mutex_lock(&me->mut);
    }
    n = min(me->token,size);
    me->token -= n;
    pthread_mutex_unlock(&me->mut);
    return n;
}

/* 令牌用不完要归还哟,可不能浪费了 */
int mytbf_returntoken(mytbf_t *ptr,int size)
{
    struct mytbf_st *me = ptr;
    // 逗我玩呢?
    if(size < 0)
            return -EINVAL;
    pthread_mutex_lock(&me->mut);
    me->token += size;
    if(me->token > me->burst)
        me->token = me->burst;
    pthread_mutex_unlock(&me->mut);
    return size;
}

void mytbf_destroy(mytbf_t *ptr)
{
    struct mytbf_st *me = ptr;
    pthread_mutex_lock(&mut_job);
    job[me->pos] = NULL;
    pthread_mutex_unlock(&mut_job);
    pthread_mutex_destroy(&me->mut);
    free(ptr);
}

上面这个令牌桶库可以支持最多 1024 个桶,也就是可以使用多线程同时操作这 1024 个桶来获得不同的速率,每个桶的速率是固定的。

这 1024 个桶保存在一个数组中,所以每次访问桶的时候都需要对它进行加锁,避免多个线程同时访问发生竞争。
同样每个桶也允许使用多个线程同时访问,所以每个桶中也需要一个互斥量来保障处理令牌的时候不会发生竞争。
写互斥量的代码一定要注意临界区内的所有的跳转,通常在跳转之前需要解锁,避免产生死锁。常见的跳转包括 continue; break; return; goto; longjmp(3); 等等,甚至函数调用也是一种跳转。
当某个函数内包含临界区,也就是需要加锁再进入临界区,但是从程序的布局来看该函数无法加锁,那么根据 POSIX 标准的约定,这种函数的命名规则是必须以 _unlocked 作为后缀,所以大家在看到这样的函数时在调用之前一定要先加锁。总结起来说就是以这个后缀命名的函数表示函数内需要加锁但是没有加锁,所以调用者需要先加锁再调用,例如上面代码中的 get_free_pos_unlocked() 函数。
解释一下上面这个令牌桶中用过的几个没见过的函数。

sched_yield — yield the processor
#include <sched.h>
int sched_yield(void);

sched_yield(2) 这个函数的作用是出让调度器。在用户态无法模拟它的实现,它会让出当前线程所占用的调度器给其它线程使用,而不必等待时间片耗尽才切换调度器,大家暂时可以把它理解成一个很短暂的 sleep(3) 。一般用于在使用一个资源时需要同时获得多把锁但是却没法一次性获得全部的锁的场景下,只要有任何一把锁没有抢到,那么就立即释放已抢到的锁,并让出自己的调度器让其它线程有机会获得被自己释放的锁。当再次调度到自己时再重新抢锁,直到能一次性抢到所有的锁时再进入临界区,这样就避免了出现死锁的情况。

pthread_once - dynamic package initialization
#include <pthread.h>
int pthread_once(pthread_once_t *once_control,
       void (*init_routine)(void));
pthread_once_t once_control = PTHREAD_ONCE_INIT;

pthread_once(3) 函数一般用于动态单次初始化,它能保证 init_routine 函数仅被调用一次。
pthread_once_t 只能使用 PTHREAD_ONCE_INIT 宏初始化,没有提供其它初始化方式。这个与我们前面见到的初始化 pthread_t 和 pthread_nutex_t 不一样。

上面的代码中,向令牌桶添加令牌的线程只需要启动一次,而初始化令牌桶的函数却在开启每个令牌桶的时候都需要调用。为了在初始化令牌桶的函数中仅启动一次添加令牌的线程,采用 pthread_once(3) 函数来创建线程就可以了。这样之后在第一次调用 mytbf_init() 函数的时候会启动新线程添加令牌,而后续再调用 mytbf_init() 的时候就不会启动添加令牌的线程了。
上面代码中调用 pthread_once(3) 相当于下面的伪代码:

lock();
if (init_flag)
{
    init_flag = 0;
    // do sth
}
unlock();

条件变量(pthread_cond_t)
上面的程序经过测试,发现 CPU 正在满负荷工作,说明程序中出现了忙等, 是哪里出现了忙等呢?其实就是 mytbf_fetchtoken() 函数获得锁的时候采用了忙等的方式。前面我们提到过,异步程序有两种处理方式,一种是通知法,一种是查询法,我们这里用的就是查询法,下面我们把它修改成通知法来实现。

/* mytbf.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <string.h>
#include "mytbf.h"

/* 每一个令牌桶 */
struct mytbf_st
{
    int cps; // 速率
    int burst; // 令牌上限
    int token; // 可用令牌数量
    int pos; // 当前令牌桶在 job 数组中的下标
    pthread_mutex_t mut; // 用来保护令牌竞争的互斥量
    pthread_cond_t cond; // 用于在令牌互斥量状态改变时发送通知
};

/* 所有的令牌桶 */
static struct mytbf_st *job[MYTBF_MAX];
/* 用来保护令牌桶数组竞争的互斥量 */
static pthread_mutex_t mut_job = PTHREAD_MUTEX_INITIALIZER;
/* 添加令牌的线程 ID */
static pthread_t tid;
/* 初始化添加令牌的线程 */
static pthread_once_t init_once = PTHREAD_ONCE_INIT;

/* 线程处理函数:负责定时向令牌桶中添加令牌 */
static void *thr_alrm(void *p)
{
    int i;
    while (1) {
        pthread_mutex_lock(&mut_job);
        // 遍历所有的桶
        for (i = 0 ; i < MYTBF_MAX; i++) {
            // 为可用的桶添加令牌
            if (job[i] != NULL) {
                pthread_mutex_lock(&job[i]->mut);
                job[i]->token += job[i]->cps;
                // 桶中可用的令牌不能超过上限
                if(job[i]->token > job[i]->burst)
                    job[i]->token = job[i]->burst;
                // 令牌添加完毕之后,通知所有等待使用令牌的线程准备抢锁
                pthread_cond_broadcast(&job[i]->cond);
                pthread_mutex_unlock(&job[i]->mut);
            }
        }
        pthread_mutex_unlock(&mut_job);
        // 等待一秒钟后继续添加令牌
        sleep(1);
    }
    pthread_exit(NULL);
}

static void module_unload(void)
{
    int i;
    pthread_cancel(tid);
    pthread_join(tid,NULL);
    pthread_mutex_lock(&mut_job);
    for (i = 0 ; i < MYTBF_MAX ; i++) {
        if (job[i] != NULL) {
            // 互斥量和条件变量使用完之后不要忘记释放资源
            pthread_mutex_destroy(&job[i]->mut);
            pthread_cond_destroy(&job[i]->cond);
            free(job[i]);
        }
    }
    pthread_mutex_unlock(&mut_job);
    pthread_mutex_destroy(&mut_job);
}

static void module_load(void)
{
    int err;
    err = pthread_create(&tid,NULL,thr_alrm,NULL);
    if (err) {
        fprintf(stderr,"pthread_create():%s\n",strerror(err));
        exit(1);
    }
    atexit(module_unload);
}

/*
 * 为了不破坏调用者对令牌桶操作的原子性,
 * 在该函数内加锁可能会导致死锁,
 * 所以该函数内部无法加锁,
 * 必须在调用该函数之前先加锁。
 */
static int get_free_pos_unlocked(void)
{
    int i;
    for(i = 0 ; i < MYTBF_MAX; i++)
        if(job[i] == NULL)
            return i;
    return -1;
}

mytbf_t *mytbf_init(int cps,int burst)
{
    struct mytbf_st *me;
    int pos;
    pthread_once(&init_once,module_load);
    me = malloc(sizeof(*me));
    if(me == NULL)
        return NULL;
    me->cps = cps;
    me->burst = burst;
    me->token = 0;
    pthread_mutex_init(&me->mut,NULL);
    pthread_cond_init(&me->cond,NULL);
    pthread_mutex_lock(&mut_job);
    pos = get_free_pos_unlocked();
    if (pos < 0) {
        pthread_mutex_unlock(&mut_job);
        free(me);
        return NULL;
    }
    me->pos = pos;
    job[pos] = me;
    pthread_mutex_unlock(&mut_job);
    return me;
}

static inline int min(int a,int b)
{
    return (a < b) ? a : b;
}

int mytbf_fetchtoken(mytbf_t *ptr,int size)
{
    int n;
    struct mytbf_st *me = ptr;
    if(size < 0)
        return -EINVAL;
    pthread_mutex_lock(&me->mut);
    // 令牌数量不足,等待令牌被添加进来
    while (me->token <= 0) {
        /*
         * 原子化的解锁、出让调度器再抢锁以便工作或等待
         * 它会等待其它线程发送通知再唤醒
         * 放在循环中是因为可能同时有多个线程再使用同一个桶,
         * 被唤醒时未必就能拿得到令牌,所以要直到能拿到令牌再出去工作
         */
        pthread_cond_wait(&me->cond,&me->mut);
//        pthread_mutex_unlock(&me->mut);
//        sched_yield();
//        pthread_mutex_lock(&me->mut);
    }
    n = min(me->token,size);
    me->token -= n;
    pthread_mutex_unlock(&me->mut);
    return n;
}

/* 令牌用不完要归还哟,可不能浪费了 */
int mytbf_returntoken(mytbf_t *ptr,int size)
{
    struct mytbf_st *me = ptr;
    // 逗我玩呢?
    if(size < 0)
            return -EINVAL;
    pthread_mutex_lock(&me->mut);
    me->token += size;
    if(me->token > me->burst)
        me->token = me->burst;
    /*
     * 令牌归还完毕,通知其它正在等待令牌的线程赶紧起床,准备抢锁
     * 这两行谁在上面谁在后面都无所谓
     * 如果先发通知再解锁,收到通知的线程发现锁没有释放会等待锁释放再抢;
     * 如果先解锁再发通知,反正已经出了临界区了,
     * 就算有线程在通知发出之前抢到了锁也不会发生竞争,
     * 大不了其它被唤醒的线程起床之后发现没有锁可以抢,那就继续睡呗。
     */
    pthread_cond_broadcast(&me->cond);
    pthread_mutex_unlock(&me->mut);
    return size;
}

void mytbf_destroy(mytbf_t *ptr)
{
    struct mytbf_st *me = ptr;
    pthread_mutex_lock(&mut_job);
    job[me->pos] = NULL;
    pthread_mutex_unlock(&mut_job);
    pthread_mutex_destroy(&me->mut);
    pthread_cond_destroy(&me->cond);
    free(ptr);
}

不难看出这两段代码的差别,把查询法(忙等)修改为通知法(非忙等)仅仅加一个条件变量(pthread_cond_t) 就行了。
条件变量的作用是什么?其实就是让线程以无竞争的形式等待某个条件的发生,当条件发生时通知等待的线程醒来去做某件事。
通知进程醒来有两种方式,一种是仅通知一个线程醒来,如果有多个线程都在等待,那么不一定是哪个线程被唤醒;另一种方式是把所有等待同一个条件的线程都唤醒。
在下面我们会介绍这两种方式,先从条件变量的初始化和销毁开始讨论。

pthread_cond_destroy, pthread_cond_init - destroy and initialize condition variables
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
       const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

与互斥量一样,条件变量也有两种初始化方式,一种是使用 pthread_cond_init(3) 函数,另一种是使用 PTHREAD_COND_INITIALIZER 宏。这两种方式的使用场景也与互斥量相同,这里就不再赘述了。
条件变量在使用完之后要用 pthread_cond_destroy(3) 函数释放资源,否则会导致内存泄漏!

pthread_cond_broadcast, pthread_cond_signal -  broadcast  or  signal  a condition
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

这两个函数就是条件变量的关键操作了,大家注意看。
pthread_cond_signal(3) 函数用于唤醒当前多个等待的线程中的任何一个。虽然名字上有 signal,但是跟系统中的信号没有任何关系。
pthread_cond_broadcast(3) 惊群,将现在正在等待的线程全部唤醒。

pthread_cond_timedwait, pthread_cond_wait - wait on a condition
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex,
       const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex);

这几个函数与上面的两个函数的作用是成对的,上面的两个函数用于唤醒线程,唤醒什么线程呢?当然是唤醒 _wait() 等待条件满足的线程。
当一个线程做某件事之前发现条件不满足,那就使用这几个 _wait() 函数进入等待状态,当某个线程使条件满足的时候,就要用上面的两个函数唤醒等待的线程继续工作了。
pthread_cond_wait(3) 在临界区外阻塞等待某一个条件发生变化,直到有一个通知到来打断它的等待。这种方式是死等。
pthread_cond_timedwait(3) 增加了超时功能的等待,超时之后无论能否拿到锁都返回。这种方式是尝试等。
pthread_cond_wait(3) 相当于下面三行代码的原子操作:

pthread_mutex_unlock(mutex);
sched_yield();
pthread_mutex_lock(mutex);

通常等待会放在一个循环中,就像上面的令牌桶一样,因为可能有多个线程都在等待条件满足,当前的线程被唤醒时不代表执行条件一定满足,可能先被唤醒的线程发现条件满足已经去工作了,等轮到当前线程调度的时候条件可能又不满足了,所以如果条件不满足需要继续进入等待。

重构锁链

回到上面提到的面试题,用锁链实现的疯狂打印有序的 abcd 5 秒钟。
锁链的办法并不是这道题的考点,这道题真正的考点其实是使用互斥量 + 条件变量的方式来实现,下面重构一遍。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#define THRNUM        4

static pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond_num = PTHREAD_COND_INITIALIZER;
static int num = 0;

static int next(int a)
{
    if(a+1 == THRNUM)
        return 0;
    return a+1;
}

static void *thr_func(void *p)
{
    int n = (int)p;
    int ch = n + 'a';
    while (1) {
        // 先抢锁,能抢到锁就可以获得打印的机会
        pthread_mutex_lock(&mut_num);
        while (num != n) {
            // 抢到锁但是发现不应该自己打印,那就释放锁再出让调度器,让别人尝试抢锁
            pthread_cond_wait(&cond_num,&mut_num);
        }
        write(1,&ch,1);
        num = next(num);
        /*
         * 自己打印完了,通知别人你们抢锁吧
         * 因为不知道下一个应该运行的线程是谁,
         * 所以采用惊群的方式把它们全都唤醒,
         * 让它们自己检查是不是该自己运行了。
         */
        pthread_cond_broadcast(&cond_num);
        pthread_mutex_unlock(&mut_num);
    }
    pthread_exit(NULL);
}

int main()
{
    int i,err;
    pthread_t tid[THRNUM];
    for (i = 0 ; i < THRNUM ; i++) {
        // 直接启动 4 个线程,让它们自己判断自己是否应该运行,而不用提前锁住它们
        err = pthread_create(tid+i,NULL,thr_func,(void *)i);
        if (err) {
            fprintf(stderr,"pthread_create():%s\n",strerror(err));
            exit(1);
        }
    }
    alarm(5);
    for(i = 0 ; i < THRNUM ; i++)
        pthread_join(tid[i],NULL);
    exit(0);
}

在实际场景中,如何使用 pthread_cond_signal(3) 和 pthread_cond_broadcast(3) 呢?
这个其实没有固定套路,要根据具体的场景来选择。一般只有一个线程在等待或者明确知道哪个线程应该被唤醒的时候使用 _signal() 函数,如果有多个线程在等待并且不确定应该由谁起来工作的时候使用惊群。
这里说的不确定是指业务上不能确定哪个线程应该工作,而不是你作为程序猿稀里糊涂的不知道哪个线程该工作。程序猿应该保证了解你的每一行代码在做什么,而不要写出一坨自己都不知道它在做什么的代码。
至于应该先发通知再解锁还是先解锁再发通知,效果上没有太大的区别,这一点在上面令牌桶的栗子中已经阐述了。

一个进程最多能创建多少个线程
一个进程能够创建多少个线程呢?主要受两个因素影响,一个是 PID 耗尽,一个是在之前的博文中画 C 程序地址空间布局时的阴影区域被栈空间占满了 。
PID 看上去是进程 ID,但是在之前讨论进程的博文中说过,内核的最小执行单元其实是线程,实际上是线程在消耗 PID。一个系统中的线程可以有很多,所以 PID 被耗尽也是有可能的。
使用 ulimit(1) 命令可以查看栈空间的大小,阴影区剩余空间的大小 / 栈空间的大小 == 就是我们能创建的线程数量。
大家可以自己写个程序测试一下一个进程最多能够创建多少个线程,然后使用 ulimit(1) 命令修改栈的大小再测试几次,看看能有什么发现。

管道的特点

1)管道的同义词是队列;
2)管道是单工的;
3)管道必须凑齐读写双方,如果只有一方,则阻塞等待。

关于管道的详细内容,我们在后面讨论进程间通信(IPC)的时候还会再详细讨论。

线程控制

之前我们在创建线程的时候都是使用的默认属性,下面介绍自定义线程的属性。
《APUE》第三版 P341 表中的属性可以用来限定一个进程能创建线程的最大数量,但是限定线程数量的宏不必太当真,因为一个进程能创建的线程的数量是受很多因素影响的,并非一定是以这几个宏值为准的。

线程属性使用 pthread_attr_t 类型表示。

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

static void *func(void *p)
{
    puts("Thread is working.");
    pthread_exit(NULL);
}

int main()
{
    pthread_t tid;
    int err, i;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    // 修改每个线程的栈大小
    pthread_attr_setstacksize(&attr,1024*1024);
    for (i = 0 ; ; i++) {
        // 测试当前进程能创建多少个线程
        err = pthread_create(&tid,&attr,func,NULL);
        if (err) {
            fprintf(stderr,"pthread_create():%s\n",strerror(err));
            break;
        }
    }
    printf("i = %d\n",i);
    pthread_attr_destroy(&attr);
    exit(0);
}

上面的例子就是通过线程的属性修改了为每个线程分配的栈空间大小,这样创建出来的线程数量与默认的就不同了。
线程属性使用 pthread_attr_init(3) 函数初始化,用完之后使用 pthread_attr_destroy(3) 函数销毁。
线程属性不仅可以设定线程的栈空间大小,还可以创建分离的线程等等。

互斥量属性
互斥量属性使用 pthread_mutexattr_t 类型表示,与线程属性一样,使用之前要初始化,使用完毕要销毁。
pthread_mutexattr_init(3) 函数用于初始化互斥量的属性,用法跟线程的属性很相似。

pthread_mutexattr_getpshared, pthread_mutexattr_setpshared  -  get  and
       set the process-shared attribute
#include <pthread.h>
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *
       restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
       int pshared);

函数名称里面的 p 是指 process,这两个函数的作用是设定线程的属性是否可以跨进程使用,线程的属性怎么能跨进程使用呢?别急,我们先看看 clone(2) 函数。

clone, __clone2 - create a child process
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
          int flags, void *arg, ...
          /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

clone(2) 进程的 flags 如果设置了 CLONE_FILES 则父子进程共享文件描述符表,正常情况文件描述符表是线程之间共享的,因为多线程是运行在同一个进程的地址空间之内的。
虽然 clone(2) 函数的描述是创建子进程,但实际上如果将 flags 属性设置得极端分离(各种资源都独享),相当于创建了一个子进程;
而如果 flags 属性设置得极端近似(各种资源都共享),则相当于创建了兄弟线程。所以对于内核来讲并没有进程这个概念,只有线程的概念。你创建出来的到底是进程还是线程,并不影响内核进行调度。
如果需要创建一个“东西”与当前的线程既共享一部分资源,又独占一部分资源,就可以使用 clone(2) 函数创建一个既不是线程也不是进程的“东西”,因为对内核来说进程和线程本来就是模糊的概念。
现在能理解为什么上面说 pthread_mutexattr_setpshared(3) 函数的作用是设定线程的属性是否可以跨进程使用了吧?
互斥量分为四种,不同的互斥量在遇到不同情况时效果是不同的,《APUE》第三版 P347 有图12-5 说明了这个现象,如下所示:

互斥量类型没有解锁时重新加锁不占用时解锁在已解锁时解锁
PTHREAD_MUTEX_NORMAL(常规)死锁未定义未定义
PTHREAD_MUTEX_ERRORCHECK(检错)返回错误返回错误返回错误
PTHREAD_MUTEX_RECURSIVE(递归)允许返回错误返回错误
PTHREAD_MUTEX_DEFAULT(默认,我们平时使用的就是这个)未定义未定义未定义

解释一下表头上的描述是什么意思:

1)没有解锁时重新加锁:当前 mutex 已 lock,再次 lock 的情况;
2)不占用时解锁:他人锁定由你解锁的情况;
3)在已解锁时解锁:当前 mutex 已 unlock,再次 unlock 的情况;

重入
在信号阶段提过重入,如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的。
POSIX 标准要求,在线程标准制定之后,所有的库必须支持线程安全,如果不支持线程安全需要在函数名添加 _unlocked 后缀,或发布一个支持线程安全的函数,函数名要添加 _r 后缀,在 man 手册中有很多带 _r 后缀的函数。

线程特定数据
就是为了某些数据支持多线程并发而做的改进,最典型的就是 errno,errno 最初是全局变量,现在早已变成宏定义了。
把 errno 预编译一下,看看它的庐山真面目。

#include <errno.h>
errno;

>$ gcc -E errno.c
# 2 "errno.c" 2

(*__errno_location ());
>$

线程的取消
pthread_cancel(3) 函数只是提出取消请求,并不能强制取消线程。
线程的取消分为两种情况:允许取消 或 不允许取消。
pthread_cancel(3) 提出取消请求后,是否允许取消由被请求取消的线程自己决定。
不允许取消没什么好说的,说一下允许取消。
允许取消分为两种情况:异步 cancel 和 推迟 cancel(默认)

1)异步 cancel:是内核的操作方式,这里不做解释。
2)推迟 cancel:推迟到取消点再响应取消操作。
            取消点其实就是一个函数,收到取消请求时取消点的代码不会执行。

《APUE》第三版 P362 图12-14 都是可能导致阻塞的系统调用,它们都是 POSIX 定义的一定存在的取消点。P363 图12-15 是 POSIX 定义的可选取消点,这些函数实际是否为取消点要看平台具体的实现。
为什么要采用推迟取消的策略,而不是收到请求在任何地方都立即取消?看伪代码:

thr_func()
{
   p = malloc();
  -------------------------->收到了一个取消请求
  -------------------------->pthread_cleanup_push();->free(p); // 不是取消点,继续执行
   fd1 = open(); // 是取消点,在取消点执行之前响应取消动作
  -------------------------->pthread_cleanup_push();->close(fd1);
   fd2 = open();
  -------------------------->pthread_cleanup_push();->close(fd2);
   pthread_exit();
}

在线程执行函数运行的任何时候都可能收到取消请求,假设上面的函数刚刚使用 malloc(3) 函数动态分配了一段内存,还没来得及挂钩子函数的时候就收到了一个取消请求,如果立即响应这个取消请求就会导致内存泄漏。而挂载钩子函数的宏 pthread_cleanup_push 不是取消点,所以会推迟这个取消请求继续工作。等它把钩子函数挂载完毕之后继续运行来到 open(2) 函数,由于 open(2) 函数是有效的取消点,所以响应了这个取消请求,线程被取消并且通过钩子函数释放了上面 malloc(3) 所申请的空间,这就是推迟取消最明显的作用。
pthread_setcancelstate(3) 函数的作用就是修改线程的可取消状态,可以将线程设置为可取消的或不可取消的。
pthread_setcanceltype(3) 函数用来修改取消类型,也就是可以选择 异步 cancel 和 推迟 cancel。
pthread_testcancel(3) 函数的作用是人为放置取消点。假如某个线程一启动就疯狂的做数学运算10分钟,没有调用任何函数,则这个线程无法响应取消,为了使这个线程可以响应取消就可以通过这个函数人为放置取消点。

线程和信号
在这里插入图片描述
在前面讨论信号的博文中,画过一张信号处理过程的草图,简单地把一个线程的标准信号画成两个位图。而实际上每个线程级别都持有一个 mask 位图和一个 padding 位图,每个进程级别持有一个 padding 位图而没有 mask 位图。从内核态回到用户态之前,当前线程先用自己的 mask 位图与进程级别的 padding 做按位与(&)运算,如果有信号就要去处理;然后再用自己的 mask 位图与自己的 padding 位图做按位与运算,再处理相应的信号。
所以其实是哪个线程被调度,就由哪个线程响应进程级别的信号。
由此可见,线程之间也是可以互相发信号的。

pthread_kill - send a signal to a thread
#include <signal.h>
int pthread_kill(pthread_t thread, int sig);
Compile and link with -pthread.

pthread_kill(3) 函数的作用就是在线程阶段发信号,thread 表示给哪个线程发送信号,sig 是发送哪个信号。
pthread_sigmask(3) 人为干预线程级别的 mask 位图,类似于 sigsetmask(3) 函数。

线程和 fork
这一小节主要说的是 fork(2) 在不同平台上实现有歧义。
在fork的发展过程中主要有两大阵营,一大阵营使用写时拷贝技术,另一大阵营使用类似 vfork(2) 的策略。

线程和 I/O
这一小节主要介绍 pread(2) 和 pwrite(2) 函数,这两个函数实际当中用得并不多,可以看看书上的介绍或者 man 手册里的说明,到这里 POSIX 标准的线程就介绍完了。*nix 平台线程的标准不只有 POSIX 一家,还有像 OpenMP 等标准也定义了不同的线程实现方式。

OpenMP 标准
使用 OpenMP 标准写一个 Hello World 程序。

#include <stdio.h>
#include <stdlib.h>
#include <omp.h>

int main()
{
#pragma omp parallel sections
{
#pragma omp section
        printf("[%d]:Hello\n",omp_get_thread_num());
#pragma omp section
        printf("[%d]:World\n",omp_get_thread_num());
}
        exit(0);
}

OpenMP 标准的多线程就是使用 # 这种预处理标签实现的,使用 GCC 编译的时候需要加 -fopenmp 参数。

>$ make hello
cc -fopenmp -Wall    hello.c   -o hello
>$ ./hello
[0]:Hello
[1]:World
>$ ./hello
[1]:World
[0]:Hello
>$

从上面的运行结果可以看出来,线程已经创建,并且已经发生了竞争。
GCC 从 4.0 以上的版本开始支持 OpenMP 标准。
由于 OpenMP 标准不是 《APUE》里面介绍的,所以我们这里就不做过多的探讨了,感兴趣的小伙伴们可以去 http://www.openmp.org 了解更多内容。

线程安全

在多线程环境中,多个线程在同一时刻对同一份资源进行写操作时,不会出现数据不一致。
线程安全是程序设计中的术语,指某个函数、函数库在多线程环境中被调用时,能正确处理多个线程之间的公用变量,使程序正常运行,可通过以下方式实现线程安全。

  1. 使用互斥锁
    一个线程,如果需要访问公共资源,需要获得互斥锁并对其加锁,资源在锁定过程中,如果其它线程对其进行访问,也需要获得互斥锁,如果获取不到,线程只能进行阻塞,直到获得该锁的线程解锁。
#include <pthread.h>

int increment_counter(void)
{
    static int counter = 0;
    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    // only allow one thread to increment at a time
    ++counter;
    // store value before any other threads increment it further
    int result = counter;   
    pthread_mutex_unlock(&mutex);
    return result;
}
  1. 使用原子操作
    使用 互斥锁 来保护一次简单的增量操作过于繁琐,可以使用一些专门的原子操作 API 函数来替代,如:
#include <atomic>

int increment_counter(void)
{
    static std::atomic<int> counter(0);
    
    // increment is guaranteed to be done atomically
    int result = ++counter;
    return result;
}

Linux内核中原子整形操作:

#include <linux/types.h>

int increment_counter(void)
{
    atomic_t counter = ATOMIC_INIT(0);
    
    // increment is guaranteed to be done atomically
    atomic_inc(&counter);
    int result = counter;
    return result;
}
  1. 防止过度优化
    线程安全的函数应该为每个调用它的线程分配专门的空间,把多个线程共享的变量正确对待,如,通知编译器该变量为“易失(volatile)”型,阻止其进行一些不恰当的优化。

线程安全函数与可重入函数

先明确概念:
线程安全函数:能够正确地处理多个线程之间的公用变量的函数。
可重入函数:在任意时刻被中断,然后操作系统调度执行另一段代码,中断返回后,该程序不会出错。

可重入函数应当满足条件:
不能含有静态(全局)非常量数据。
不能返回静态(全局)非常量数据的地址。
只能处理由调用者提供的数据。
不能依赖于单例模式资源的锁。
调用(call)的函数也必需是可重入的。

可重入函数未必是线程安全的;线程安全函数未必是可重入的。

例1:上述例子中的increment_counter函数是线程安全的,但不是可重入的。因为使用了互斥锁,如果这个函数用在可重入的中断处理程序中,在pthread_mutex_lock(&mutex)和pthread_mutex_unlock(&mutex)之间,另一个函数调用increment_counter,则会第二次执行此函数,此时由于mutex已被lock,函数会在pthread_mutex_lock(&mutex)处阻塞,并且由于mutex没有机会被unlock,阻塞会永远持续下去。

例2:一个函数打开某个文件并读入数据。这个函数是可重入的,因为它的多个实例同时执行不会造成冲突;但它不是线程安全的,因为在它读入文件时可能有别的线程正在修改该文件,为了线程安全必须对文件加“同步锁”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值