linux应用编程:一篇搞定多线程编程

一、概述

1.1 简介
  • 进程是系统中程序执行和资源分配的基本单位。每个进程有自己的数据段、代码段和堆栈段。这就造成进程在进行切换等操作时都需要有比较负责的上下文切换等动作。为了进一步减少处理器的空转时间支持多处理器和减少上下文切换开销,也就出现了线程。
  • 线程通常叫做轻量级进程。线程是在共享内存空间中并发执行的多道执行路径,是一个更加接近于执行体的概念,拥有独立的执行序列,是进程的基本调度单元,每个进程至少都有一个 main 线程。它与同进程中的其他线程共享进程空间{堆 代码 数据 文件描述符 信号等},只拥有自己的栈空间,大大减少了上下文切换的开销。
  • 线程和进程在使用上各有优缺点:
    • 线程执行开销小,占用的 CPU 资源少,线程之间的切换快,但不利于资源的管理和保护;而进程正相反。
    • 从可移植性来讲,多进程的可移植性要好些。同进程一样,线程也将相关的变量值放在线程控制表内。一个进程可以有多个线程,也就是有多个线程控制表及堆栈寄存器,但却共享一个用户地址空间。要注意的是,由于线程共享了进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他线程带来影响,所以多线程同步相当重要

在这里插入图片描述
在这里插入图片描述

1.2 分类
  • 用户级线程:主要解决上下文切换问题,调度算法和调度过程全部由用户决定,在运行时不需要特定的内核支持。缺点是无法发挥多处理器的优势
  • 核心级线程:允许不同进程中的线程按照同一相对优先调度方法调度,发挥多处理器的并发优势现在大多数系统都采用用户级线程和核心级线程并存的方法。一个用户级线程可以对应一个或多个核心级线程,也就是“一对一”或“一对多”模型。
  • !!! Compile and link with -pthread
  • !!! Compile and link with -pthread
  • !!! Compile and link with -pthread

二、线程标识

  • 每个进程都一个自己的ID(pid_t类型),每个线程也有一个自己的ID(pthread_t类型)
#include <pthread.h>
   
/* @function:得到线程自己的标识
 * @return: 成功:线程标识(总是成功,不会出现失败的情况)
 */
pthread_t pthread_self(void);


/* @function:比较两个线程的标识
 * @param[in]:t1 线程标识1
 * @param[in]:t2 线程标识2
 * @return: 若相等,非0;否则为0
 */
int pthread_equal(pthread_t t1, pthread_t t2);

三、线程创建

#include <pthread.h>

 /* @function:创建一个新的线程
  * @param[out]:thread  线程标识
  * @param[in]:attr 是一个结构体指针,结构中的元素分别指定新线程的运行属性,
  * 			attr可以用 pthread_attr_init 等函数设置各成员的值,但通常
  * 			传入为 NULL 即可
  * @param[in]:start_routine 是一个函数指针,指向新线程的入口点函数,线程
  * 			入口点函数带有一个 void *的参数由pthread_create 的arg参数传入
  * @param[in]:arg 用于传递给第 3 个参数指向的入口点函数的参数,
  * 			可以为 NULL,表示不传递。
  * @return: 成功 0, 失败:错误编号(如:EAGAIN、EINVAL、EPERM),具体含义查看man文档
  */
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

四、线程终止

在这里插入图片描述

  • 对应的三种退出操作:return、 pthread_cancel、pthread_exit

  • 随着线程的终止退出,对应而来的就是一个资源的释放问题:

    线程的资源可分为两种:一种是创建时由主线程copy而来的栈内存;另一种是由线程自己内部创建的堆内存(malloc、realloc、calloc)、锁资源。
    (1) 针对于第一种资源, 释放的方式有两种:第一种是分离状态(detached)线程的释放方式;第二种是非分离状态(joinable,也叫可结合)线程的释放方式。可结合的线程在线程退出后不会立即释放资源,必须要调用pthread_join来显式的结束线程。分离的线程在线程退出时系统会自动回收资源。默认线程属性是可结合线程,配置请参考下面线程属性。
    (2)针对于第二种资源,如果线程还没进行显示释放(如free())或刚好执行到加锁后,解锁前的这段程序,就被其他线程调用pthread_cancel杀死掉了,这类的线程资源是无法通过系统自动释放的,最终结果就是会导致内存泄漏,或者死锁的情况,然后就是程序崩溃了。所以第一慎用pthread_cancel;第二使用清理函数pthread_cleanup_push()和pthread_cleanup_pop()来处理。

  • 分离状态与非分离状态(可结合)的解析:配置方式见线程属性章节

    一个可结合的线程能够被其他线程收回其资源,在被其他线程回收之前,它的存储器资源(如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收,它的存储器资源在它终止时由系统自动释放。
    所以,如果不需要线程退出码,最好设置成分离线程,系统资源能自动释放

  • 分离线程和可结合线程释放资源测试例程

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


int count = 0;

void *pthread1(void *arg)
{
    count++;
}

int main()
{
    pthread_t tid1;
    pthread_attr_t attr;
    int err = pthread_attr_init(&attr);
    if (err != 0)
        return (err);
    err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (err != 0)
        return (err);
    while (1)
    {
        int err=pthread_create(&tid1, &attr, pthread1, (void *)1);
        //int err = pthread_create(&tid1, NULL, pthread1, (void *)1);
        if (err != 0)
        {
            printf("\nfailed:%s\n", strerror(errno));
            break;
        }
        else printf("\rcount = %d", count);
        usleep(2); /*切忌要加2us延时,具体原因是主线程创建速度快于子线程退出速度,会报错*/
        //pthread_join(tid1, NULL);
    }
    return 0;
}
  • 网络上好多博客都说分离线程无法被其他线程杀死,但是在下面测试例程中,得出的结果表示很纳闷,结果很明显,分离线程被pthread_cancel了
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

void *pthread1(void *arg)
{
    int count = 0;
    for (;;) {
        count++;
        printf("pthread1 count=%d\n", count);
        sleep(1);
    }
    
}

int main(int argc, char **argv)
{
    int i=0;
    pthread_t tid1;
    pthread_attr_t attr;
    int ret;

    int err = pthread_attr_init(&attr);
    if (err != 0)
        return (err);
    err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (err != 0)
        return (err);
    err=pthread_create(&tid1, &attr, pthread1, NULL);
    pthread_attr_getdetachstate(&attr, &ret);
    if (ret == PTHREAD_CREATE_DETACHED)
        printf("ret=%d\n", ret);
    //int err = pthread_create(&tid1, NULL, pthread1, (void *)1);
    if (err != 0)
        printf("failed:%s\n", strerror(errno));
    sleep(1);
    pthread_cancel(tid1);
    for (;;) {
        printf("main pthread count=%d\n", i++);
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

  • 调用pthread_join后自动把线程置于分离状态,然后这样资源才可以恢复。如果线程已经处于分离状态, pthread_join调用就会失败,返回 EINVAL,尽管这种行为是与具体实现相关的。
  • 切忌在进程中的任意线程调用了exit、Exit或者_exit,那么整个进程就会终止。用pthread_exit代替
#include <pthread.h>

/* @function:线程退出,并返回一个退出码
 * @param[in]:retval 线程退出的返回值,可以被pthread_join中第二个参数捕获
 */
void pthread_exit(void *retval);


/* @function:等待线程退出
 * @param[in]:thread 线程标识
 * @param[out]:retval  捕获的线程退出码,可以为NULL
 * @return: 成功 0;失败:错误编码
 * @note1:调用的线程将一直阻塞,直到指定的线程用pthread_exit、或被取消、或正常返回.
 * @note2:如果指定的线程正常的返回(包括调用pthread_exit),retval就包含返回码;
 * 		如果调用pthread_cancel取消线程,则retval = PTHREAD_CANCELED。
 */
int pthread_join(pthread_t thread, void **retval);


/* @function:的另一个线程中取消其他线程
 * @param[in]:thread 要取消的线程标识
 * @param[out]:retval  捕获的线程退出码
 * @return: 成功 0;失败:错误编码
 * @note: pthread_cancel只是提出一个取消请求,该请求也可以被要取消的线程忽略,具体看该线程的设置
 */
int pthread_cancel(pthread_t thread);

资源清理

  • 不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,在不考虑因运行出错而退出的前提下,如何保证种植时能顺利的释放掉自己所占用的资源,包括单独申请的对内存,特别是锁资源,就是一个必需考虑的问题
  • 最经常出现的情形是资源独占锁的使用:线程为了访问临界共享资源而为其加上锁,但在访问过程中该线程被外界取消,或者发生了中断,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程。
  • pthread_cleanup_push()/pthread_cleanup_pop()函数用于自动释放资源
  • 一个线程可以建立多个清理函数
/* @function: 线程清理入栈
 * @param[in]:void (*routine) (void *) 清理调用函数
 * @param[in]:arg 传入清理函数中的参数
 * @note: 
 */
void pthread_cleanup_push(void (*routine) (void *), void *arg)


/* @function: 线程清理出栈
 * @param[in]:execute  1 执行pthread_cleanup_push()调用的routine()清理函数; 0 不执行清理函数
 * @note: pthread_cleanup_pop 的参数 execute 如果为非 0 值,则按栈的顺序注销掉一个原来注册的清理函数的时候,会执行该函数;
 * 			当 pthread_cleanup_pop()函数的参数为 0 时,仅仅在线程调用 pthread_exit 函数或者其它线程对本线程调用
 *  		pthread_cancel 函数时,才在弹出“清理函数”的同时执行该“清理函数”。
 */
void pthread_cleanup_pop(int execute)

/* 可见,pthread_cleanup_push()带有一个"{",而 pthread_cleanup_pop()带有一个"}",
因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。*/
#define pthread_cleanup_push(routine,arg) \
{ struct _pthread_cleanup_buffer _buffer; \
_pthread_cleanup_push (&_buffer, (routine), (arg));
#define pthread_cleanup_pop(execute) \
_pthread_cleanup_pop (&_buffer, (execute)); }
/* 在有的系统平台,函数如果在调用 pthread_cleanup_ push和 pthread_cleanup_pop之间返回(调用return),会产生未定义行为。
唯一的可移植方法是调用 pthread_ exit */

/* 释放内存常见用法 */
void clean_func(void *p_arg)
{
	printf("clean_func run\n");
    free(p_arg);
}
void *pthread1(void *arg)
{
    char *p=(char *)malloc(SIZE);
    if (p == NULL) {
         printf("malloc failed\n");
    }
    pthread_cleanup_push(clean_func, p);
    free(p);
    pthread_cleanup_pop(0); //为0时,线程正常退出,不调用清理函数,
    pthread_exit(0);
}

/* 解锁常见用法 */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void clean_func(void *p_arg)
{
	printf("clean_func run\n");
    pthread_mutex_unlock(&mutex);
}
void *pthread1(void *arg)
{
	pthread_mutex_lock(&mutex);
    pthread_cleanup_push(clean_func, NULL);
	/* 代码区 */
	pthread_mutex_unlock(&mutex);
    pthread_cleanup_pop(0); //为0时,线程正常退出,不调用清理函数,
    pthread_exit(0);
}
  • 通过测试可知,当pthread_exit(0);置于pthread_cleanup_pop(0);前面,不论pthread_cleanup_pop()参数为0还是1,都会调用清理函数。置于之后,为0时则不会调用,结合测试结果及上面概述可以得到:

    pthread_cleanup_push 和 pthread_cleanup_pop 成对出现形成一个{ }区,凡是线程执行这个区内出现终止情况,都会触发清理机制,并调用清理函数(return除外)。
    所以上面 pthread_cleanup_pop的note中提到:当 pthread_cleanup_pop()函数的参数为 0 时,仅仅在线程调用 pthread_exit 函数或者其它线程对本线程调用pthread_cancel 函数时,才在弹出“清理函数”的同时执行该“清理函数”。这个pthread_exit 仅在pthread_cleanup_pop之前才会触发清理函数。
    又引发一个猜想:当其他线程调用pthread_cancel 杀死本线程时,本线程刚好执行到pthread_cleanup_pop之后,还会触发清理函数吗?理论应该是不触发的,但是同样不管触不触发,对资源的释放并没有影响。

五、线程访问控制(同步)

  • 当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取和修改的,那么就不存在一致性问题。同样,如果变量是只读的,多个线程同时读取变量也不会有一致性问题。但是,当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。
  • 线程同步的方式有:互斥锁、条件变量、读写锁、信号量等等

5.1 互斥锁

在这里插入图片描述

5.1.1 互斥锁操作
#include <pthread.h>

/*静态初始化*/
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //快速锁

/* 嵌套锁(递归锁),允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
如果是不同线程请求,则在加锁线程解锁时重新竞争。*/
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;//递归锁

/*检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与 PTHREAD_MUTEX_TIMED_NP 
类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。*/
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;

/* @function:动态初始化锁
 * @param[in]:mutex 互斥锁
 * @param[in]:mutexattr  锁的属性设置,为NULL,表示设置为默认属性
 * 		@arg mutexattr.__mutexkind=PTHREAD_MUTEX_RECURSIVE_NP; 初始化为一个嵌套锁(递归锁)
 * 		@arg mutexattr.__mutexkind=PTHREAD_MUTEX_ERRORCHECK_NP; 初始化为一个检错锁
 * 		@arg mutexattr.__mutexkind=PTHREAD_MUTEX_TIMED_NP; NULL的默认属性,快速锁
 * @return: 成功 0;失败:错误编码
 */
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)


/* @function: 阻塞式的上锁
 * @param[in]:mutex 互斥锁
 * @return: 成功 0;失败:错误编码
 * @note:如果互斥锁已经上了锁,则阻塞线程,不占用CPU,直到互斥量被解锁,也就是这一特性保护了临界区资源
 */
int pthread_mutex_lock(pthread_mutex_t *mutex)


/* @function:解锁
 * @param[in]:mutex 互斥锁
 * @return: 成功 0;失败:错误编码
 */
int pthread_mutex_unlock(pthread_mutex_t *mutex)


/* @function: 销毁锁,释放资源
 * @param[in]:mutex 互斥锁
 * @return: 成功 0;失败:错误编码
 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);

/* 例程 */
struct msg {
    int count;
}msg;

pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;

void *pthread1(void *arg)
{
    for (;;) {
        pthread_mutex_lock(&mutex);
        msg.count--;
        printf("pthread1 count=%d\n", msg.count);
        if (msg.count == 0) 
            printf("pthread1  xxxxxxxxxx\n");
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}

void *pthread2(void *arg)
{
    for (;;) {
        pthread_mutex_lock(&mutex);
        msg.count--;
        printf("pthread2 count=%d\n", msg.count);
        if (msg.count == 0) 
            printf("pthread2  xxxxxxxxxx\n");
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}

int main(int argc, char **argv)
{
    pthread_t  ptd_id1;
    pthread_t  ptd_id2;
    msg.count = 30;
    pthread_create(&ptd_id1, NULL, pthread1, NULL);
    pthread_create(&ptd_id2, NULL, pthread2, NULL);
    pthread_join(ptd_id1, NULL);
    pthread_join(ptd_id2, NULL);
    exit(0);
}
  • 看到 count在两个线程间有序的自减,如果没有互斥锁呢?会是什么现象
    在这里插入图片描述
  • 没加互斥锁
    在这里插入图片描述
5.1.2 死锁
  • 第一种情况:如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁(Deadlock)。
  • 第二种情况:在信号的处理函数中加锁,这种情况类似于第一种情况。
  • 第三种情况:若线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了,这也叫死锁。
  • 第四种情况:在获得锁后,解锁前线程被异常终止,且没有设置清理机制。
#include <pthread.h>
#include <time.h>

/* @function: 带有阻塞时间的上锁(阻塞根据时间而定)
 * @param[in]:mutex 互斥锁
 * @param[in]:tsptr 时间  eg:tsptr.tv_sec=10;
 * @return: 成功 0;失败:错误编码
 * @note:pthread_mutex_timedlock 与 pthread_mutex_lock是等价的,只是在pthread_mutex_lock增加了一个时间参数,
 * 		当时间到达后,pthread_mutex_timedlock不再进行阻塞,并返回ETIMEDOUT,从而避免了死锁
 */
int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timesec *tsptr);

/* function: 非阻塞式上锁
 * param[in]:mutex 互斥锁
 * return: 成功 0;失败:错误编码
 * note: 若互斥锁未加锁,则上锁,返回 0
 *		 若互斥锁已加锁,则函数直接返回失败,即 EBUSY。 不会访问临界区资源
 */
int pthread_mutex_trylock(pthread_mutex_t *mutex)
  • 死锁实例1 ----调用线程两次加同一把锁。
#include <stdio.h>
#include <pthread.h>

pthread_mutex_t lock;

void* pthfunc(void *args)
{
	pthread_mutex_lock(&lock); //先加一次锁
	pthread_mutex_lock(&lock); //再用 lock 加锁,会挂起阻塞
	//pthread_mutex_trylock(&lock); //用 trylock 加锁,则不会挂起阻塞
	printf("hello\n");
	sleep(1);
	pthread_exit(NULL);
}
int main(int argc, char **argv)
{
	pthread_t pthid = 0;
	pthread_mutex_init(&lock,NULL);
	pthread_create(&pthid,NULL,pthfunc,NULL);
	pthread_join(pthid,NULL);
	pthread_mutex_destroy(&lock);
	
	return 0;
}
  • 死锁实例2 ---- 不应该在信号处理函数中使用互斥锁,否则容易造成死锁。
#include <stdio.h>
#include <pthread.h>
#include <signal.h>

pthread_mutex_t lock;

void fun(int n)
{
	pthread_mutex_lock(&lock); //此处加锁会成死锁,导致信号处理函数无法返回,进而导致主线程也不能再执行了
	int i = 0;
	while (i < 5) {
		printf("fun i = %d\n", i);
		i++;
		sleep(1);
		pthread_mutex_unlock(&lock); //解锁
	}
}
void* pthfunc(void *args)
{
	int n;
	signal(2, fun);
	pthread_mutex_lock(&lock); //先加一次锁
	while (1) {
		printf("子线程正在运行\n");
		sleep(1);
	}
	pthread_mutex_unlock(&lock); //解锁
	pthread_exit(NULL);
}
int main(int argc, char **argv)
{
	pthread_t pthid = 0;
	pthread_mutex_init(&lock,NULL);
	pthread_create(&pthid,NULL,pthfunc,NULL);
	while (1) {
		printf("主线程正在运行\n");
		sleep(1);
	}
	pthread_join(pthid,NULL);
	pthread_mutex_destroy(&lock);

	return 0;
}
5.1.3 互斥锁属性
#include <pthread.h>

/* @function: 
 * @param[in]:attr 互斥锁属性,与pthread_mutex_init 第二个参数一样
 * @return: 成功 0;失败:错误编码
 */
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

/* @function: 
 * @param[in]:attr 互斥锁属性,与pthread_mutex_init 第二个参数一样
 * @return: 成功 0;失败:错误编码
 */
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

(1) 获取和修改互斥锁的共享属性

#include <pthread.h>

/* @function: 设置互斥锁的共享属性
 * @param[in]:attr 互斥锁属性,与pthread_mutex_init 第二个参数一样
 * @param[in]:pshared 
 * 		@arg PTHREAD_PROCESS_PRIVATE //互斥锁只能在同一进程的多线程间使用(不跨进程)
 * 		@arg PTHREAD_PROCESS_SHARED	//互斥锁可以在多个进程的多线程间使用(跨进程)
 * @return: 成功 0;失败:错误编码
 */
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

/* @function: 获取互斥锁的共享属性
 * @param[in]:attr 互斥锁属性,与pthread_mutex_init 第二个参数一样
 * @param[out]:pshared 
 * @return: 成功 0;失败:错误编码
 */
int pthread_mutexattr_getpshared(pthread_mutexattr_t *attr, int *pshared);

  • 设置代码编写流程,后面其他属性都大同小异(包括条件变量、读写锁、线程属性)
pthread_mutexattr_t attr; //互斥锁属性
pthread_mutexattr_init(&attr);  //初始化互斥锁属性
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE ); //设置互斥锁属性

(2) 获取和修改互斥锁的健壮属性

#include <pthread.h>

/* @function: 
 * @param[in]:attr 互斥锁属性,与pthread_mutex_init 第二个参数一样
 * @param[in]:robust 
 * 		@arg PTHREAD_MUTEX_STALLED 默认
 * 		@arg PTHREAD_MUTEX_ROBUST 
 * @return: 成功 0;失败:错误编码
 */
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);

/* @function: 
 * @param[in]:attr 互斥锁属性,与pthread_mutex_init 第二个参数一样
 * @param[out]:robust 
 * @return: 成功 0;失败:错误编码
 */
int pthread_mutexattr_getrobust(pthread_mutexattr_t *attr, int *robust);

(3) 获取和修改互斥锁类型属性

  • 详细参考 http://man7.org/linux/man-pages/man3/pthread_mutexattr_gettype.3p.html

    PTHREAD_MUTEX_TIMED_NP, 普通锁,NULL的缺省值。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
    PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁(递归锁),允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
    PTHREAD_MUTEX_ERRORCHECK_NP, 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP 类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁
    PTHREAD_MUTEX_ADAPTIVE_NP 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
    在这里插入图片描述

#include <pthread.h>

/* @function: 设置互斥锁类型
 * @param[in]:attr 互斥锁属性,与pthread_mutex_init 第二个参数一样
 * @param[in]:type 
 * 		@arg PTHREAD_MUTEX_NORMAL
 * 		@arg PTHREAD_MUTEX_ERRORCHECK
 * 		@arg PTHREAD_MUTEX_RECURSIVE
 * 		@arg PTHREAD_MUTEX_DEFAULT
 * @return: 成功 0;失败:错误编码
 */
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

/* @function: 获取互斥锁类型
 * @param[in]:attr 互斥锁属性,与pthread_mutex_init 第二个参数一样
 * @param[out]:type 
 * @return: 成功 0;失败:错误编码
 */
int pthread_mutexattr_gettype(pthread_mutexattr_t *attr, int *type);

5.2 条件变量

  • 条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待条件变量的条件成立而挂起;另一个线程使条件成立(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起,从而使线程以无竞争的方式等待特定的条件发生。
  • 条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。
  • 条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。
  • 互斥锁用于上锁,条件变量则用于等待。这两种不同类型的同步都是需要的。
5.2.1 条件变量操作
#include <pthread.h>

/* 静态创建一个条件变量 */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

/* @function: 动态创建一个条件变量
 * @param[in]:cond 条件变量标识
 * @param[in]:attr  条件变量属性,默认属性为NULL
 * @return: 成功 0;失败:错误编码
 */
int pthread_cond_init(pthread_cond_t * cond, const pthread_condattr_t *attr);


/* @function: 销毁一个条件变量
 * @param[in]:cond 条件变量标识
 * @return: 成功 0;失败:错误编码
 * @note:只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回 EBUSY。
 * 			因为 Linux 实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。
 */
int pthreead_cond_destroy(pthread_cond_t *cond);


/* @function: 线程阻塞式等待一个条件变量
 * @param[in]:cond 条件变量标识
 * @param[in]:mutex 互斥锁标识
 * @return: 成功 0;失败:错误编码
 */
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);


/* @function: 线程半阻塞式等待一个条件变量
 * @param[in]:cond 条件变量标识
 * @param[in]:mutex 互斥锁标识
 * @param[in]:tsptr 阻塞时间 
 * @return: 成功 0;失败:错误编码
 */
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timesec *tsptr);


/* @function: 唤醒一个等待该条件变量的线程
 * @param[in]:cond 条件变量标识
 * @return: 成功 0;失败:错误编码
 */
int pthread_cond_signal(pthread_cond_t *cond);


/* @function: 唤醒所有等待该条件变量的线程
 * @param[in]:cond 条件变量标识
 * @return: 成功 0;失败:错误编码
 */
int pthread_cond_broadcast(pthread_cond_t *cond);


/*例程*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

struct msg {
    int count;
};
struct msg msg;

pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t   cond = PTHREAD_COND_INITIALIZER;
void *pthread1(void *arg)
{
    msg.count = 0;
    for (;;) {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);
            msg.count++;
            printf("count=%d\n", msg.count);
        pthread_mutex_unlock(&mutex);
    }
    pthread_exit((void *)-1);
}

void *pthread2(void *arg)
{
    for (;;) {
        pthread_mutex_lock(&mutex);
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    pthread_exit((void *)-1);
}


int main(int argc, char **argv)
{
    pthread_t  ptd_id1;
    pthread_t  ptd_id2;
    int  pthread_exit_code;
    
    pthread_create(&ptd_id1, NULL, pthread1, NULL);
    pthread_create(&ptd_id2, NULL, pthread2, NULL);
    pthread_join(ptd_id1, (void *)&pthread_exit_code);
        printf("ptd_id1 exit code=%d\n", (int)pthread_exit_code);
    pthread_join(ptd_id2, (void *)&pthread_exit_code);
        printf("ptd_id2 exit code=%d\n", (int)pthread_exit_code);

    exit(0);
}

在这里插入图片描述

  • 从结果中可以看到每隔1s打印一次,互斥锁与条件变量的结合充分体现出了线程间的同步与互斥。
5.2.2 条件变量属性

详细参考 http://man7.org/linux/man-pages/man3/pthread_condattr_destroy.3p.html

#include <pthread.h>
/* @function: 
 * @param[in]:attr 属性
 * @return: 成功 0;失败:错误编码
 */
int pthread_condattr_destroy(pthread_condattr_t *attr);

/* @function: 
 * @param[in]:attr 属性
 * @return: 成功 0;失败:错误编码
 */
int pthread_condattr_init(pthread_condattr_t *attr);

(1) 获取和修改条件变量的共享属性

#include <pthread.h>

int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared);

(2) 获取和修改条件变量的时钟属性

#include <pthread.h>

int pthread_condattr_getclock(const pthread_condattr_t *restrict attr, clockid_t *restrict clock_id);
int pthread_condattr_setclock(pthread_condattr_t *attr, clockid_t clock_id);

5.3 读写锁

  • 某些应用中读数据比修改数据频繁,这些应用可从改用读写锁代替互斥锁中获益。任意给定时刻允许多个读出者存在提供了更高的并发度,同时在某个写入者修改数据期间保护该数据,以免任何其他读出者或写入者的干扰。
  • 仅当没有其他线程在读或修改某个给定的数据时,当前线程才可以修改它。
  • 只要没有线程在修改某个给定的数据,那么任意数目的线程都可以拥有该数据的读访问权。
5.3.1 读写锁操作
#include <pthread.h>

/* @function: 创建一个读写锁
 * @param[in]:rwlock 读写锁标识
 * @param[in]:attr 读写锁属性
 * @return: 成功 0;失败:错误编码
 */
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);


/* @function: 销毁一个读写锁
 * @param[in]:rwlock 读写锁标识
 * @return: 成功 0;失败:错误编码
 */
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

/* @function: 加读写锁
 */
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //加读取锁,可以写入,不能读取,较少用
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //加写入锁,可以读取,不能写入,常用

/* @function: 解读写锁
 */
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

/* @function: 尝试性加一个读写锁(不阻塞)
 * @param[in]:rwlock 读写锁标识
 * @return: 成功 0;失败:错误编码
 */
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);


#include <time.h>

/* @function: 带有阻塞时间的加锁
 * @param[in]:rwlock 读写锁
 * @param[in]:tsptr 时间  eg:tsptr.tv_sec=10;
 * @return: 成功 0;失败:错误编码
 * @note:效果同互斥锁pthread_mutex_timedlock一样
 */
int pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock, const struct timesec *tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *rwlock, const struct timesec *tsptr);

5.3.2 读写锁属性

详细参考 http://man7.org/linux/man-pages/man3/pthread_rwlockattr_destroy.3p.html

#include <pthread.h>
/* @function: 
 * @param[in]:attr 属性
 * @return: 成功 0;失败:错误编码
 */
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);

/* @function: 
 * @param[in]:attr 属性
 * @return: 成功 0;失败:错误编码
 */
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

(1) 获取和修改读写锁的共享属性

#include <pthread.h>

int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);

5.4 信号量

六、线程属性

#include <pthread.h>

typedef struct __pthread_attr_s
{
    int __detachstate; 

    int __schedpolicy;

    struct __sched_param __schedparam;

    int __inheritsched;

    int __scope;

    size_t __guardsize;
    int __stackaddr_set;

    void *__stackaddr;
    size_t __stacksize;表示堆栈的大小。

}pthread_attr_t;



/* @function: 初始化或销毁一个pthread_attr_t 结构体变量
 * @param[out]:attr 要被初始化或销毁的pthread_attr_t 结构体变量
 * @return: 成功 0;失败:错误编码
 */
int pthread_attr_init (pthread_attr_t *attr);
int pthread_attr_destroy (pthread_attr_t *attr);

  • 属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。
  • 默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。

6.1 线程的分离状态(__detachstate )

  • __detachstate 表示新线程是否与进程中其他线程脱离同步,如果置位则新线程不能用 pthread_join()来同步,且在退出时自行释放所占用的资源。通俗的来讲就是线程的分离状态决定一个线程以什么样的方式来终止自己。

  • 非分离状态 PTHREAD_CREATE_JOINABLE
    线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。

  • 分离状态 PTHREAD_CREATE_DETACHED
    分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的

  • 缺省为 PTHREAD_CREATE_JOINABLE 状态。这个属性也可以在线程创建并运行以后用 pthread_attr_setdetachstate()来设置,而一旦设置为 PTHREAD_CREATE_DETACHED 状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE 状态。

  • 这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

  • int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

  • int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

6.2 线程的调度策略(__schedpolicy)

  • 包括:

① SCHED_OTHER(正常、非实时)缺省 线程一旦开始运行,直到被抢占或者直到线程阻塞或停止为止。
② SCHED_RR(实时、轮转法) 如果调用进程具有有效的用户 ID 0,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM)) 的循环线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,并且这些线程没有放弃或阻塞,则在系统确定的时间段内将一直执行这些线程。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS) 的线程,请使用 SCHED_RR(基于 TS 调度类)。此外,这些线程的调用进程没有有效的用户 ID 0。
③ SCHED_FIFO(实时、先入先出) 如果调用进程具有有效的用户 ID 0,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM) 的先入先出线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,则会继续处理该线程,直到该线程放弃或阻塞为止。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS)) 的线程或其调用进程没有有效用户 ID 0 的线程,请使用 SCHED_FIFO,SCHED_FIFO 基于 TS 调度类。

  • int pthread_attr_getschedparam(pthread_attr_attr *attr, struct sched_param *param);
  • int pthread_attr_setschedparam(pthread_attr_attr *attr, struct sched_param *param);

6.3 线程的优先级(__schedparam)

__schedparam,一个 sched_param 结构,目前仅有一个 sched_priority 整型变量表示线程的运行优先级。这个
参 数 仅 当 调 度 策 略 为 实 时 ( 即 SCHED_RR 或 SCHED_FIFO ) 时 才 有 效 , 并 可 以 在 运 行 时 通 过
pthread_setschedparam()函数来改变,缺省为 0。

  • int pthread_attr_getschedparam(pthread_attr_attr *attr, struct sched_param *param);
  • int pthread_attr_setschedparam(pthread_attr_attr *attr, struct sched_param *param);

6.4 线程的继承性(__inheritsched)

  • 有两种值可供选择:PTHREAD_EXPLICIT_SCHED 和 PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和调度参数(即 attr 中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED

  • int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched)

  • int pthread_attr_setinheritsched(const pthread_attr_t *attr, int inheritsched)

6.5 线程的作用域(__scope)

  • 表示线程间竞争 CPU 的范围,也就是说线程优先级的有效范围。
  • 进程域(process scope):仅与同进程中的线程竞争 CPU PTHREAD_SCOPE_PROCESS
  • 系统域(system scope):与系统中的所有线程。一个具有系统域的线程将与整个系统中所有具有系统域的线程按照优先级竞争处理器资源,进行调度。PTHREAD_SCOPE_SYSTEM
  • 目前 Linux 仅实现了 PTHREAD_SCOPE_SYSTEM 一值。
  • int pthread_attr_setscope(pthread_attr_t *attr, int scope);
  • int pthread_attr_getscope(pthread_attr_t *attr, int *scope);

6.7 线程的栈保护区大小(__guardsize)

  • 在线程栈顶留出一段空间,防止栈溢出。
  • 当栈指针进入这段保护区时,系统会发出错误,通常是发送信号给线程。
  • 该属性默认值是PAGESIZE大小,该属性被设置时,系统会自动将该属性大小补齐为页大小的整数倍。当改变栈地址属性时,栈保护区大小通常清零。
  • int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
  • int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize);

6.8 线程的栈地址(__stackaddr)

  • POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE检测系统是否支持栈属性。
  • 也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE来进行检测。
    当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstackaddr和pthread_attr_getstackaddr两个函数分别设置和获取线程的栈地址。传给pthread_attr_setstackaddr函数的地址是缓冲区的低地址(不一定是栈的开始地址,栈可能从高地址往低地址增长)。
  • int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
  • int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);

6.9 线程的栈大小(__stacksize)

  • 当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用
  • 当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
    函数pthread_attr_getstacksize和 pthread_attr_setstacksize提供设置。
  • int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
  • int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

七、其他

7.1 取消选项

详细参考:http://man7.org/linux/man-pages/man3/pthread_setcancelstate.3.html

#include <pthread.h>

/* @function: 设置线程取消状态
 * @param[in]:state 新状态
 * 		@arg PTHREAD_CANCEL_ENABLE 可被pthread_cancel()取消
 * 		@arg PTHREAD_CANCEL_DISABLE 线程不可被pthread_cancel()取消
 * @param[out]:oldstate 要保存的旧状态
 * @return: 成功 0;失败:错误编码
 */
int pthread_setcancelstate(int state, int *oldstate);

/* @function: 设置线程取消类型
 * @param[in]:type 新类型
 * @param[out]:oldtype 要保存的旧类型
 * 		@arg PTHREAD_CANCEL_DEFERRED 推迟取消
 * 		@arg PTHREAD_CANCEL_ASYNCHRONOUS 异步取消
 * @return: 成功 0;失败:错误编码
 */
int pthread_setcanceltype(int type, int *oldtype);

八、总结

参考:《UNIX环境高级编程》《UNIX网络编程 卷2 进程间通信》

博客:https://blog.csdn.net/zsf8701/article/details/7842392?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
https://www.cnblogs.com/cthon/p/9078042.html

更多API参考:http://man7.org/linux/man-pages/man0/pthread.h.0p.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值