Linux下线程编程

举报
DS小龙哥 发表于 2022/10/08 13:26:00 2022/10/08
【摘要】 线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

第一章 实现Linux下线程基本运用

1.1线程简介

线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

1.2​ 线程与进程的区别

1.2.1 前言

进程与线程的区别,早已经成为了经典问题。自线程概念诞生起,关于这个问题的讨论就没有停止过。无论是初级程序员,还是资深专家,都应该考虑过这个问题,只是层次角度不同。一般程序员而言,搞清楚二者的概念,在工作实际中去运用成为了焦点。而资深工程师则在考虑系统层面如何实现两种技术及其各自的性能和实现代价。以至于到今天,Linux内核还在持续更新完善(关于进程和线程的实现模块也是内核完善的任务之一)。

1.2.2 二者的相同点

无论是进程还是线程,对于程序员而言,都是用来实现多任务并发的技术手段。二者都可以独立调度,因此在多任务环境下,功能上并无差异。并且二者都具有各自的实体,是系统独立管理的对象个体。所以在系统层面,都可以通过技术手段实现二者的控制。而且二者所具有的状态都非常相似。而且,在多任务程序中,子进程(子线程)的调度一般与父进程(父线程)平等竞争。

其实在Linux内核2.4版以前,线程的实现和管理方式就是完全按照进程方式实现的。在2.6版内核以后才有了单独的线程实现。

1.2.3 实现方式的差异

进程是资源分配的基本单位,线程是调度的基本单位。

进程的个体间是完全独立的,而线程间是彼此依存的。多进程环境中,任何一个进程的终止,不会影响到其他进程。而多线程环境中,父线程终止,全部子线程被迫终止(没有了资源)。而任何一个子线程终止一般不会影响其他线程,除非子线程执行了exit()系统调用。任何一个子线程执行exit(),全部线程同时灭亡。


(1)​ 多任务程序设计模式的区别

由于进程间是独立的,所以在设计多进程程序时,需要做到资源独立管理时就有了天然优势,而线程就显得麻烦多了。

比如:多任务的TCP程序的服务端,父进程执行accept()一个客户端连接请求之后会返回一个新建立的连接的描述符DES,此时如果fork()一个子进程,将DES带入到子进程空间去处理该连接的请求,父进程继续accept等待别的客户端连接请求,这样设计非常简练,而且父进程可以用同一变量(val)保存accept()的返回值,因为子进程会复制val到自己空间,父进程再覆盖此前的值不影响子进程工作。但是如果换成多线程,父线程就不能复用一个变量val多次执行accept()了。因为子线程没有复制val的存储空间,而是使用父线程的,如果子线程在读取val时父线程接受了另一个客户端请求覆盖了该值,则子线程无法继续处理上一次的连接任务了。改进的办法是子线程立马复制val的值在自己的栈区,但父线程必须保证子线程复制动作完成之后再执行新的accept()。但这执行起来并不简单,因为子线程与父线程的调度是独立的,父线程无法知道子线程何时复制完毕。这又得发生线程间通信,子线程复制完成后主动通知父线程。这样一来父线程的处理动作必然不能连贯,比起多进程环境,父线程显得效率有所下降。

关于资源不独立,看似是个缺点,但在有的情况下就成了优点。多进程环境间完全独立,要实现通信的话就得采用进程间的通信方式,它们通常都是耗时间的。而线程则不用任何手段数据就是共享的。当然多个子线程在同时执行写入操作时需要实现互斥。


1.2.4 实体间(进程间,线程间,进线程间)通信方式的不同

进程间的通信方式有这样几种:

A.共享内存 B.消息队列 C.信号量 D.有名管道 E.无名管道 F.信号

G.文件 H.socket

线程间的通信方式上述进程间的方式都可沿用,且还有自己独特的几种:

A.互斥量 B.自旋锁 C.条件变量 D.读写锁 E.线程信号 G.全局变量

进程间采用的通信方式要么需要切换内核上下文,要么要与外设访问(有名管道,文件)。所以速度会比较慢。而线程采用自己特有的通信方式的话,基本都在自己的进程空间内完成,不存在切换,所以通信速度会较快。也就是说,进程间与线程间分别采用的通信方式,除了种类的区别外,还有速度上的区别。

说明: 当运行多线程的进程捕获到信号时,只会阻塞主线程,其他子线程不会影响会继续执行。

1.2.5 控制方式的异同

进程与线程的身份标示ID管理方式不一样,进程的ID为pid_t类型,实际为一个int型的变量(也就是说是有限的)。

在全系统中,进程ID是唯一标识,对于进程的管理都是通过PID来实现的。每创建一个进程,内核去中就会创建一个结构体来存储该进程的全部信息:

每一个存储进程信息的节点也都保存着自己的PID。需要管理该进程时就通过这个ID来实现(比如发送信号)。当子进程结束要回收时(子进程调用exit()退出或代码执行完),需要通过wait()系统调用来进行,未回收的消亡进程会成为僵尸进程,其进程实体已经不复存在,但会虚占PID资源,因此回收是有必要的。

线程的ID是一个long型变量:

它的范围大得多,管理方式也不一样。线程ID一般在本进程空间内作用就可以了,当然系统在管理线程时也需要记录其信息。

对于线程而言,若要主动终止需要调用pthread_exit() ,主线程需要调用pthread_join()来回收(前提是该线程没有设置 “分离属性”)。像线发送线程信号也是通过线程ID实现的。

1.2.6 资源管理方式的异同

进程本身是资源分配的基本单位,因而它的资源都是独立的,如果有多进程间的共享资源,就要用到进程间的通信方式了,比如共享内存。共享数据就放在共享内存去,大家都可以访问,为保证数据写入的安全,加上信号量一同使用。一般而言,共享内存都是和信号量一起使用。消息队列则不同,由于消息的收发是原子操作,因而自动实现了互斥,单独使用就是安全的。

线程间要使用共享资源不需要用共享内存,直接使用全局变量即可,或者malloc()动态申请内存。显得方便直接。而且互斥使用的是同一进程空间内的互斥量,所以效率上也有优势。


1.3 线程接口函数

1.3.1 创建线程

pthread_create是Unix操作系统(Unix、Linux等)的创建线程的函数。

​ 编译时需要指定链接库:-lpthread

​ 函数原型

#include <pthread.h>

int pthread_create

(

pthread_t *thread,

const pthread_attr_t *attr,

void *(*start_routine) (void *),

void *arg

);

​ 参数说明

第一个参数为指向线程标识符的指针。

第二个参数用来设置线程属性。默认可填NULL。

第三个参数是线程运行函数的起始地址。

最后一个参数是运行函数的参数。不需要参数可填NULL。

​ Linux下查看函数帮助:# man pthread_create

​ 返回值:

若线程创建成功,则返回0。若线程创建失败,则返回出错编号。

线程创建成功后, attr参数用于指定各种不同的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个万能指针参数arg,如果需要向线程工作函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。

示例:

#include <stdio.h>

#include <pthread.h>


//线程函数1

void *pthread_func1(void *arg)

{

while(1)

{

printf("线程函数1正在运行.....\n");

sleep(2);

}

}


//线程函数2

void *pthread_func2(void *arg)

{

while(1)

{

printf("线程函数2正在运行.....\n");

sleep(2);

}

}


int main(int argc,char **argv)

{


pthread_t thread_id1;

pthread_t thread_id2;

/*1. 创建线程1*/

if(pthread_create(&thread_id1,NULL,pthread_func1,NULL))

{

printf("线程1创建失败!\n");

return -1;

}

/*2. 创建线程2*/

if(pthread_create(&thread_id2,NULL,pthread_func2,NULL))

{

printf("线程2创建失败!\n");

return -1;

}


/*3. 等待线程结束,释放线程的资源*/

pthread_join(thread_id1,NULL);

pthread_join(thread_id2,NULL);

return 0;

}


//gcc pthread_demo_code.c -lpthread


1.3.3 退出线程

线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。

这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针,该返回值可以通过pthread_join函数的第二个参数得到。

​ 函数原型

#include <pthread.h>

void pthread_exit(void *retval);

​ 参数解析

线程的需要返回的地址。

注意:线程结束必须释放线程堆栈,就是说线程函数必须调用pthread_exit()结束,否则直到主进程函数退出才释放

1.3.3 等待线程结束

pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable(结合属性)属性。

​ 函数原型

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

​ 参数

第一个参数: 线程标识符,即线程ID,标识唯一线程。

最后一个参数: 用户定义的指针,用来存储被等待线程返回的地址。

​ 返回值

0代表成功。 失败,返回的则是错误号。

​ 接收线程返回值示例:

//退出线程

pthread_exit ("线程已正常退出");


//接收线程的返回值

void *pth_join_ret1;

pthread_join( thread1, &pth_join_ret1);

1.3.4 线程分离属性

创建一个线程默认的状态是joinable(结合属性),如果一个线程结束运行但没有调用pthread_join,则它的状态类似于进程中的Zombie Process(僵死进程),即还有一部分资源没有被回收(退出状态码),所以创建线程者应该pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源(类似于进程的wait,waitpid)。但是调用pthread_join(pthread_id)函数后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此。

pthread_detach函数可以将该线程的状态设置为detached(分离状态),则该线程运行结束后会自动释放所有资源。

​ 函数原型

#include <pthread.h>

int pthread_detach(pthread_t thread);

​ 参数

线程标识符

​ 返回值

0表示成功。错误返回错误码。

EINVAL线程并不是一个可接合线程。

ESRCH没有线程ID可以被发现。

1.3.5 获取当前线程的标识符

pthread_self函数功能是获得线程自身的ID。

​ 函数原型

#include <pthread.h>

pthread_t pthread_self(void);

​ 返回值

当前线程的标识符。

pthread_t的类型为unsigned long int,所以在打印的时候要使用%lu方式,否则显示结果出问题。

1.3.6 自动清理线程资源

线程可以安排它退出时需要调用的函数,这样的函数称为线程清理处理程序。用于程序异常退出的时候做一些善后的资源清理。

在POSIX线程API中提供了一个pthread_cleanup_push()/pthread_cleanup_pop()函数用于自动释放资源。从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序段中的终止动作(包括调用 pthread_exit()和异常终止)都将执行pthread_cleanup_push()所指定的清理函数。

注意:pthread_cleanup_push函数与pthread_cleanup_pop函数需要成对调用。

​ 函数原型

void pthread_cleanup_push(void (*routine)(void *),void *arg); //注册清理函数

void pthread_cleanup_pop(int execute); //释放清理函数

​ 参数

void (*routine)(void *) :处理程序的函数入口。

void *arg :传递给处理函数的形参。

int execute:执行的状态值。 0表示不调用清理函数。1表示调用清理函数。

​ 导致清理函数调用的条件:

1.​ 调用pthread_exit()函数

2.​ pthread_cleanup_pop的形参为1。

注意:return不会导致清理函数调用。

1.3.7 自动清理线程示例代码

#include <stdio.h>

#include <pthread.h>

#include <stdlib.h>


//线程清理函数

void routine_func(void *arg)

{

printf("线程资源清理成功\n");

}


//线程工作函数

void *start_routine(void *dev)

{

pthread_cleanup_push(routine_func,NULL);


//终止线程

// pthread_exit(NULL);


pthread_cleanup_pop(1); //1会导致清理函数被调用。0不会调用。


}


int main(int argc,char *argv[])

{

pthread_t thread_id; //存放线程的标识符


/*1. 创建线程*/

if(pthread_create(&thread_id,NULL,start_routine,NULL)!=0)

{

printf("线程创建失败!\n");

}

/*2.设置线程的分离属性*/

if(pthread_detach(thread_id)!=0)

{

printf("分离属性设置失败!\n");

}

while(1){}

return 0;

}

1.3.8 线程取消函数

pthread_cancel函数为线程取消函数,用来取消同一进程中的其他线程。

头文件: #include <pthread.h>

函数原型:pthread_cancel(pthread_t tid);

1.4 线程栈空间设置

1.4.1 通过ulimit命令设置栈空间大小

pthread_create 创建线程时,若不指定分配堆栈大小,系统会分配默认值,查看默认值方法如下:

[root@tiny4412 ]#ulimit -s

10240

上面的10240单位是KB,也就是默认的线程栈空间大小为10M

也可以通过ulimit -a命令查看,其中的stack size也表示栈空间大小。

[root@tiny4412 ]#ulimit -a

-f: file size (blocks) unlimited

-t: cpu time (seconds) unlimited

-d: data seg size (kb) unlimited

-s: stack size (kb) 10240

-c: core file size (blocks) 0

-m: resident set size (kb) unlimited

-l: locked memory (kb) 64

-p: processes 7512

-n: file descriptors 1024

-v: address space (kb) unlimited

-w: locks unlimited

-e: scheduling priority 0

-r: real-time priority 0

​ 设置栈空间大小: ulimit -s <栈空间大小>

[root@tiny4412 ]#ulimit -s 8192 //设置栈空间大小

[root@tiny4412 ]#ulimit -s //查看栈空间大小

8192 //大小为8M

注意: 栈空间设置只能在超级管理员用户权限下设置

每个线程的栈空间都是独立的,如果栈空间溢出程序会出现段错误。如果一个进程有10个线程,那么分配的栈空间大小就是10*<每个线程栈大小>

例如:

int main(int argc,char **argv)

{

char buff[1024*1024*10]; //在栈空间定义数组,如果超出了栈空间总大小程序会奔溃。

printf("hello world!\n");

return 0;

}


1.4.2 通过线程函数设置栈空间大小

堆栈最小值定义为 PTHREAD_STACK_MIN(单位字节),包含#include <limits.h>后可以通过打印其值查看。

示例:

#include <pthread.h>

#include <limits.h>

int main(void)

{

printf("STACK_MIN:%d\n",PTHREAD_STACK_MIN); //16384->16KB

return 0;

}

对于默认值可以通过pthread_attr_getstacksize (&attr, &stack_size); 打印stack_size来查看。

示例:

#include <pthread.h>

#include <limits.h>


int main(void)

{

pthread_attr_t attr;

int ret,stack_size;

ret=pthread_attr_init(&attr); /*初始化线程属性*/

if(ret!=0)return -1;


ret=pthread_attr_getstacksize(&attr,&stack_size);

if(ret!=0)return -1;

printf("stacksize=%d\n",stack_size/1024/1024);//stack_size是字节单位 10485760 –10M

return 0;

}


尤其在嵌入式中内存不是很大,若采用默认值的话,会导致出现问题,若内存不足,则 pthread_create 会返回12,定义如下:

#define EAGAIN 11

#define ENOMEM 12 /* Out of memory */


上面了解了堆栈大小,下面就来了解如何使用 pthread_attr_setstacksize 重新设置堆栈大小。先看下它的原型:

#include <pthread.h>

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

attr 是线程属性变量;stacksize 则是设置的堆栈大小。 返回值0,-1分别表示成功与失败。

示例:

#include <pthread.h>

#include <limits.h>

#include <stdio.h>

int main(void)

{

pthread_attr_t attr;

int ret,stack_size=1024*1024*20;//20M


/*1.初始化线程属性*/

ret=pthread_attr_init(&attr);

if(ret!=0)return -1;


/*2.设置栈空间大小*/

ret=pthread_attr_setstacksize(&attr,stack_size);

if(ret!=0)return -1;


/*3. 查看设置之后的栈空间大小*/

ret=pthread_attr_getstacksize(&attr,&stack_size);

if(ret!=0)return -1;


printf("stacksize=%dM\n",stack_size/1024/1024);//stack_size是字节单位

return 0;

}


1.5 可重入函数

1.5.1概念

(1)重入:即重复调用,函数被不同的线程调用,有可能会出现第一次调用还没返回时就再次进入该函数开始下一次调用。

(2)可重入:当程序被多个线程反复执行,产生的结果正确。

如果一个函数只访问自己的局部变量或参数,称为可重入函数。

(3)不可重入:当程序被多个线程反复调用,产生的结果出错。

当函数访问一个全局的变量或者参数时,有可能因为重入而造成混乱,像这样的函数称为不可重入函数。

​ 线性安全:一般来说,一个函数被称为线程安全的,当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。

1.5.2 可重入特点

由于可重入函数多次调用不会出错,因此可重入函数不用担心数据会被破坏。可重入函数任何时候都可以被中断,一段时间后又可以运行,而相应的数据不会丢失。可重入函数只使用局部变量,即保存在CPU寄存器或者堆栈中;或者如果使用全局变量时,则要对全局变量予以保护(信号量或者互斥锁)。

void strcpy(*dest,*src)

while(* dest++ = *src ++){;}

*dest = NUL;

分析:上面的函数用于字符串复制,而参数是存放在堆栈中的,故而改函数可以被多任务调用,而不必担心各个任务调用期间会互相破坏对方的指针。



第二章 线程间通信机制(同步与互斥)

2.1 互斥锁

在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

Linux系统下定义了一套专门用于线程互斥的mutex函数。

mutex 是一种简单的加锁的方法来控制对共享资源的存取,这个互斥锁只有两种状态(上锁和解锁),可以把互斥锁看作某种意义上的全局变量。

为什么需要加锁?

就是因为多个线程共用进程的资源,要访问的是公共区间时(全局变量),当一个线程访问的时候,需要加上锁以防止另外的线程对它进行访问,以实现资源的独占。在一个时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程才能够对共享资源进行操作。若其他线程希望上锁一个已经上锁了的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。

2.1.1 互斥锁接口函数

1. 初始化互斥锁

头文件

#include <pthread.h>

定义函数

int pthread_mutex_init( pthread_mutex_t *mutex, const pthread_mutex_attr_t* attr );

函数说明

该函数初始化一个互斥体变量,如果参数attr 为NULL,则互斥体变量mutex 使用默认的属性。

2. 销毁互斥锁

头文件

#include <pthread.h>

定义函数

int pthread_mutex_destroy ( pthread_mutex_t *mutex );

函数说明

该函数用来释放分配给参数mutex 的资源。

返回值

调用成功时返回值为 0, 否则返回一个非0 的错误代码。

3. 上锁

头文件

#include <pthread.h>

定义函数

int pthread_mutex_lock( pthread_mutex_t *mutex );

函数说明

该函数用来锁住互斥体变量。如果参数mutex 所指的互斥体已经被锁住了,那么发出调用的线程将被阻塞直到其他线程对mutex 解锁。

如果上锁成功,将返回0。

4. 尝试上锁

表头文件

#include <pthread.h>

定义函数

int pthread_mutex_trylock( pthread_t *mutex );

函数说明

该函数用来锁住mutex 所指定的互斥体,但不阻塞。

返回值

如果该互斥体已经被上锁,该调用不会阻塞等待,而会返回一个错误代码。

如果上锁成功,将返回0.

5. 解锁

头文件

#include <pthread.h>

定义函数

int pthread_mutex_unlock( pthread_mutex_t *mutex );

函数说明

该函数用来对一个互斥体解锁。如果当前线程拥有参数mutex 所指定的互斥体,该调用将该互斥体解锁。

如果解锁成功,将返回0.

说明: 对同一个锁多次解锁没有叠加效果,如果锁是上锁状态,那么多次解锁也只有一次有效


2.1.2 互斥锁框架运用模型

pthread_mutex_t mutex;

void 线程1(void)

{

while(1)

{

//上锁

pthread_mutex_lock(&mutex);

.....主体代码......

//解锁

pthread_mutex_unlock(&mutex);

}

}

void 线程2(void)

{

while(1)

{

//上锁

pthread_mutex_lock(&mutex);

.....主体代码......

//解锁

pthread_mutex_unlock(&mutex);

}

}


int main(void)

{

//初始化互斥锁

pthread_mutex_init(&mutex,NULL);

.....主体代码......

//销毁互斥锁

pthread_mutex_destroy(&mutex);

}


2.1.3 互斥锁属性

互斥锁的属性在创建锁的时候指定,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同也就是是否阻塞等待。

说明: 对于Linux下的信号量/读写锁文件进行编译,需要在编译选项中指明-D_GNU_SOURCE

否则用gcc编译就会出现

PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP未声明(在此函数内第一次使用) 这样的提示。

例如: $ gcc app.c -lpthread -D_GNU_SOURCE


互斥锁有三个类型可供选择:

1、PTHREAD_MUTEX_TIMED_NP:普通锁(默认锁)。当一个线程加锁以后,其余请求锁的线程将形成一个阻塞等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性

示例:初始化一个快速锁。

pthread_mutex_t lock;

pthread_mutex_init(&lock, NULL);

2、PTHREAD_MUTEX_RECURSIVE_NP: 嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争

示例:初始化一个嵌套锁。

pthread_mutex_t lock;

pthread_mutexattr_t mutexattr;

mutexattr.__mutexkind = PTHREAD_MUTEX_RECURSIVE_NP;

pthread_mutex_init(&lock, &mutexattr);

嵌套锁的使用场景: 假设你在处理一个公共函数的时候,因为中间涉及公共数据,所以你加了一个锁。但是,有一种情况,这个公共函数自身也加了一个锁,而且和你加的锁是一样的。所以,除非你的使用的是信号量,要不然你的程序永远也获取不了这个锁。

示例:

//子函数

void func(void)

{

pthread_mutex_lock(&mutex);

printf("hello world!\n");

pthread_mutex_unlock(&mutex);

}


//线程函数

void *thread1_func(void *arg)

{

while(1)

{

pthread_mutex_lock(&mutex);

func(); //调用函数

pthread_mutex_unlock(&mutex);

sleep(3);

}

}


3、PTHREAD_MUTEX_ERRORCHECK_NP: 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,

否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁

检错锁的主要特点就是: 同一个线程无法多次重复进行加锁,第一次获取锁成功后,没有解锁的情况下,如果继续获取锁将不会阻塞,会返回一个错误值(35)。

示例:初始化一个检错锁。

pthread_mutex_t lock;

pthread_mutexattr_t mutexattr;

mutexattr.__mutexkind = PTHREAD_MUTEX_ERRORCHECK_NP;

pthread_mutex_init(&lock, &mutexattr);

2.1.4 互斥锁3种静态初始化方式

1.​ 普通锁: pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;

2.​ 嵌套锁: pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;

3.​ 检错锁: pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;

如果使用了静态方式初始化互斥锁结构,就不需要调用pthread_mutex_init函数。

示例:

//静态初始化普通锁

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void 线程1(void)

{

while(1)

{

//上锁

pthread_mutex_lock(&mutex);

.....主体代码......

//解锁

pthread_mutex_unlock(&mutex);

}

}

void 线程2(void)

{

while(1)

{

//上锁

pthread_mutex_lock(&mutex);

.....主体代码......

//解锁

pthread_mutex_unlock(&mutex);

}

}


int main(void)

{

.....主体代码......

//销毁互斥锁

pthread_mutex_destroy(&mutex);

}


2.1.5 互斥锁实例1:对公共函数保护

下面代码是两个线程同时调用了一个打印函数,分别打印: “123” “456”

void print(char *p)

{

while(*p!='\0')

{

printf("%c",*p++);

sleep(1);

}

}


void *thread1_func(void *arg)

{

print("123\n");

}


void *thread2_func(void *arg)

{

print("456\n");

}

打印结果:

[wbyq@wbyq linux-share-dir]$ ./a.out

412536

预期的结果应该是打印123\456连续在一起的,对于这种情况,就可以加锁进行保护。

示例代码:

void print(char *p)

{

pthread_mutex_lock(&mutex);

while(*p!='\0')

{

printf("%c",*p++);

sleep(1);

}

pthread_mutex_unlock(&mutex);

}


void *thread1_func(void *arg)

{

print("123\n");

}


void *thread2_func(void *arg)

{

print("456\n");

}

打印结果:

[wbyq@wbyq linux-share-dir]$ ./a.out

123

456

2.1.6 互斥锁实例2: 售卖火车票

​ 示例代码: 下面代码模拟一个火车票售卖系统,此处不加锁,则会出现卖出负数票的情况。

#include <stdio.h>

#include <pthread.h>

int ticketcount = 11; //火车票,公共资源(全局)

void* salewinds1(void* args)

{

while(ticketcount > 0)

{

printf("窗口1开始售票!门票是:%d\n",ticketcount);

sleep(2);

ticketcount --;

printf("窗口1售票结束,最后一张票是:%d\n",ticketcount);

}

}


void* salewinds2(void* args)

{

while(ticketcount > 0)

{

printf("窗口2开始售票!门票是:%d\n",ticketcount);

sleep(2);

ticketcount --;

printf("窗口2售票结束,最后一张票是:%d\n",ticketcount);

}

}

int main()

{

pthread_t pthid1 = 0;

pthread_t pthid2 = 0;

pthread_create(&pthid1,NULL,salewinds1,NULL);

pthread_create(&pthid2,NULL,salewinds2,NULL);

pthread_join(pthid1,NULL);

pthread_join(pthid2,NULL);

return 0;

}

​ 示例代码: 加锁之后的火车售票

#include <stdio.h>

#include <pthread.h>

int ticketcount = 11;

pthread_mutex_t lock;

void* salewinds1(void* args)

{

while(1)

{

pthread_mutex_lock(&lock); //因为要访问全局的共享变量,所以就要加锁

if(ticketcount > 0) //如果有票

{

printf("窗口1开始售票!门票是:%d\n",ticketcount);

sleep(2);

ticketcount --;

printf("窗口1售票结束,最后一张票是:%d\n",ticketcount);

}

else

{

pthread_mutex_unlock(&lock);

pthread_exit(NULL);

}

pthread_mutex_unlock(&lock);

sleep(1); //要放到锁的外面,让另一个有时间锁

}

}

void* salewinds2(void* args)

{

while(1)

{

pthread_mutex_lock(&lock);

if(ticketcount>0)

{

printf("窗口2开始售票!门票是:%d\n",ticketcount);

sleep(2);

ticketcount --;

printf("窗口2售票结束,最后一张票是:%d\n",ticketcount);

}

else

{

pthread_mutex_unlock(&lock);

pthread_exit(NULL);

}

pthread_mutex_unlock(&lock);

sleep(1);

}

}

int main()

{

pthread_t pthid1 = 0;

pthread_t pthid2 = 0;

pthread_mutex_init(&lock,NULL); //初始化锁

pthread_create(&pthid1,NULL,salewinds1,NULL);

pthread_create(&pthid2,NULL,salewinds2,NULL);

pthread_join(pthid1,NULL);

pthread_join(pthid2,NULL);

pthread_mutex_destroy(&lock); //销毁锁

return 0;

}


2.2 读写锁

读写锁与互斥锁类似,但读写锁有更高的并行性:

         1. 读写锁有三种状态,读模式下加锁(共享)、写模式下加锁(独占)以及不加锁。

         2. 一次只有一个线程可以占有写模式下的读写锁;但是多个线程可以同时占有读模式下的读写锁

         3. 读写锁在写加锁状态时,其他试图以写状态加锁的线程都会被阻塞。读写锁在读加锁状态时,如果有线程希望以写模式加锁时,必须阻塞,直到所有线程释放锁

          4. 当读写锁以读模式加锁时,如果有线程试图以写模式对其加锁,那么读写锁会阻塞随后的读模式锁请求,以避免读锁长期占用,而写锁得不到请求。

2.2.1 读写锁接口函数

1. 读写锁初始化

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

2. 读模式加锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

3. 写模式加锁

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

4. 解锁

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

5. 销毁读写锁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

2.2.2 读写锁框架运用模型

#include <stdio.h>

#include <pthread.h>


pthread_rwlock_t rwlock;

void *salewinds1(void* args)

{

while(1)

{

pthread_rwlock_rdlock(&rwlock);//读模式加锁

printf("线程1正在读:\n");

pthread_rwlock_unlock(&rwlock);

sleep(1);

}

}


void *salewinds2(void* args)

{

while(1)

{

pthread_rwlock_rdlock(&rwlock);//读模式加锁

printf("线程2正在读:\n");

pthread_rwlock_unlock(&rwlock);

sleep(1);

}

}


void *salewinds3(void* args)

{

while(1)

{

pthread_rwlock_wrlock(&rwlock);//写模式加锁

printf("线程3正在写:\n");

pthread_rwlock_unlock(&rwlock);

sleep(1);

}

}


int main()

{

pthread_t pthid1 = 0;

pthread_t pthid2 = 0;

pthread_t pthid3 = 0;

pthread_rwlock_init(&rwlock,NULL);

pthread_create(&pthid1,NULL,salewinds1,NULL);

pthread_create(&pthid2,NULL,salewinds2,NULL);

pthread_create(&pthid3,NULL,salewinds3,NULL);

pthread_join(pthid1,NULL);

pthread_join(pthid2,NULL);

pthread_join(pthid3,NULL);

pthread_rwlock_destroy(&rwlock); //销毁锁

return 0;

}

2.3 条件变量

条件变量是线程可用的一种同步机制,条件变量给多个线程提供了一个回合的场所,条件变量和互斥量一起使用,允许线程以无竞争的方式等待特定的条件发生。条件变量本事是由互斥体保护的,线程在改变条件状态之前必须首先锁住互斥量,其他线程在获取互斥量之前就不会觉察到这种变化,因为互斥量必须锁定之后才改变条件。

2.3.1 条件变量接口函数

1. 条件变量: 初始化与销毁接口函数

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

int pthread_cond_destroy(pthread_cond_t *cond);

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

使用条件变量前调用pthread_cond_init初始化,使用完毕后调用pthread_cond_destroy做清理工作。除非需要创建一个具有非默认属性的条件变量,否则pthread_cond_init函数的attr参数可以设置为NULL。

2. 条件变量: 等待与唤醒接口函数

#include<pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);

int pthread_cond_signal (pthread_cond_t *cond);

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

 传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥量解锁。这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait函数返回时,互斥量再次被锁住。

pthread_cond_broadcast用广播的形式唤醒所有等待条件变量的线程。pthread_cond_signal用于唤醒一个等待条件变量的线程,至于哪个线程被唤醒,取决于线程的优先级和调度机制。有时候需要唤醒一个指定的线程,但pthread没有对该需要提供解决方法。可以间接实现该需求:定义一个能够唯一表示目标线程的全局变量,在唤醒等待条件变量的线程前先设置该变量为目标线程,然后以广播形式唤醒所有等待条件变量的线程,这些线程被唤醒后都检查改变量是否是自己,如果是就开始执行后续代码,否则继续等待。

​ 总结:

pthread_cond_signal函数一次性可以唤醒阻塞队列中的一个线程,pthread_cond_broadcast函数一次性可以唤醒阻塞队列中的所有线程

2.3.2 条件变量框架运用模型

//条件变量与互斥锁代码

#include <stdio.h>

#include <pthread.h>

#define MAX_CNT 50

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_cond_t cond;


void *start_func(void *arg)

{

int a=*(int*)arg;

printf("进入线程:%d,%lu\n",a,pthread_self());

pthread_mutex_lock(&mutex);

pthread_cond_wait(&cond,&mutex);

/*mutex参数用来保护条件变量的互斥锁,调用pthread_cond_wait前mutex必须加锁 */

pthread_mutex_unlock(&mutex);

printf("id=%lu,cnt=%d\n",pthread_self(),a);

}


void sighandler(int sig)

{

printf("解锁一次.\n");

pthread_cond_broadcast(&cond); //唤醒所有阻塞的线程

//pthread_cond_signal(&cond); //唤醒一个阻塞的线程

}


int main()

{

int i;

pthread_t thread[MAX_CNT];

signal(2,sighandler);

pthread_cond_init(&cond, NULL);

for(i=0;i<MAX_CNT;i++)

{

pthread_create(&thread[i],NULL,start_func,&i);

sleep(1);

}

for(i=0;i<MAX_CNT;i++)

{

pthread_join(thread[i],NULL);

}

return 0;

}


2.4 自旋锁

自旋锁和互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)状态,自旋锁可用于下面的情况:锁被持有的时间短,并且线程不希望再重新调度上花费太多的成本。自旋锁通常作为底层原语用于实现其他类型的锁。根据他们所基于的系统架构,可以通过使用测试并设置指令有效地实现。当然这里说的有效也还是会导致CPU资源的浪费:当线程自旋锁变为可用时,CPU不能做其他任何事情,这也是自旋锁只能够被只有一小段时间的原因。

2.4.1 自旋锁接口函数

int pthread_spin_destroy(pthread_spinlock_t *);

int pthread_spin_init(pthread_spinlock_t *, int);

int pthread_spin_lock(pthread_spinlock_t *);

int pthread_spin_trylock(pthread_spinlock_t *);

int pthread_spin_unlock(pthread_spinlock_t *);

pthread_spin_destroy: 销毁自旋锁

pthread_spin_init: 初始化自旋锁

pthread_spin_lock: 自旋锁上锁(阻塞)

pthread_spin_trylock: 自旋锁上锁(非阻塞)

pthread_spin_unlock : 自旋锁解锁

以上函数成功都返回0.

pthread_spin_init 函数的pshared参数表示进程共享属性,表明自旋锁是如何获取的,如果它设为PTHREAD_PROCESS_SHARED,则自旋锁能被,可以访问锁底层内存的线程所获取,即使那些线程属于不同的进程。否则pshared参数设为PTHREAD_PROCESS_PRIVATE,自旋锁就只能被初始化该锁的进程内部的线程访问到。

如果自旋锁当前在解锁状态,pthread_spin_lock函数不要自旋就可以对它加锁,试图对没有加锁的自旋锁进行解锁,结果是未定义的。需要注意,不要在持有自旋锁情况下可能会进入休眠状态的函数,如果调用了这些函数,会浪费CPU资源,其他线程需要获取自旋锁需要等待的时间更长了。

2.4.2 自旋锁框架运用模型

//条件变量与互斥锁代码

#include <stdio.h>

#include <pthread.h>

#define MAX_CNT 50

pthread_spinlock_t lock;


void *start_func(void *arg)

{

int a=*(int*)arg;

printf("进入线程:%d,%lu\n",a,pthread_self());

pthread_spin_lock(&lock);

printf("id=%lu,cnt=%d\n",pthread_self(),a);

}


void sighandler(int sig)

{

printf("解锁一次.\n");

pthread_spin_unlock(&lock);

}


int main()

{

int i;

pthread_t thread[MAX_CNT];

signal(2,sighandler);

pthread_spin_init(&lock,PTHREAD_PROCESS_PRIVATE);

for(i=0;i<MAX_CNT;i++)

{

pthread_create(&thread[i],NULL,start_func,&i);

sleep(1);

}

for(i=0;i<MAX_CNT;i++)

{

pthread_join(thread[i],NULL);

}

pthread_spin_destroy(&lock);

return 0;

}

2.5 屏障(围栏机制)

2.5.1 屏障实现接口函数

在Linux线程里的屏障功能由pthread_barrier 系列函数实现,在<pthread.h>中定义,用于多线程的同步。

它包含三个函数:

#include <pthread.h>

int pthread_barrier_destroy(pthread_barrier_t *barrier);

int pthread_barrier_init(pthread_barrier_t *restrict barrier,const pthread_barrierattr_t *restrict attr, unsigned count);

int pthread_barrier_wait(pthread_barrier_t *barrier);

那么pthread_barrier_*是用来做什么的?这三个函数又怎么配合使用呢?

pthread_barrier_*其实只做且只能做一件事,就是充当栏杆(barrier意为栏杆)。

形象的说就是把先后到达的多个线程挡在同一栏杆前,直到所有线程到齐,然后撤下栏杆同时放行。

1)init函数负责指定要等待的线程个数;

2) wait()函数由每个线程主动调用,它告诉栏杆“我到起跑线前了”。wait()执行末尾栏杆会检查是否所有人都到栏杆前了,如果是,栏杆就消失所有线程继续执行下一句代码;如果不是,则所有已到wait()的线程停在该函数不动,剩下没执行到wait()的线程继续执行;

3)destroy函数释放init申请的资源。

参数解释:

pthread_barrier_t,是一个计数锁,对该锁的操作都包含在三个函数内部,我们不用关心也无法直接操作。只需要实例化一个对象丢给它就好。

pthread_barrierattr_t,锁的属性设置,设为NULL让函数使用默认属性即可。

count,你要指定的等待个数。

2.5.2 使用场景举例

这种“栏杆”机制最大的特点就是最后一个执行wait的动作最为重要,就像赛跑时的起跑枪一样,它来之前所有人都必须等着。所以实际使用中,pthread_barrier_*常常用来让所有线程等待“起跑枪”响起后再一起行动。比如我们可以用pthread_create()生成100 个线程,每个子线程在被create出的瞬间就会自顾自的立刻进入回调函数运行。但我们可能不希望它们这样做,因为这时主进程还没准备好,和它们一起配合的其它线程还没准备好,我们希望它们在回调函数中申请完线程空间、初始化后停下来,一起等待主进程释放一个“开始”信号,然后所有线程再开始执行业务逻辑代码。

解决方案:

为了解决上述场景问题,我们可以在init时指定n+1个等待,其中n是线程数。而在每个线程执行函数的首部调用wait()。这样100个pthread_create()结束后所有线程都停下来等待最后一个wait()函数被调用。这个wait()由主进程在它觉得合适的时候调用就好。最后这个wait()就是鸣响的起跑枪。

2.5.3 屏障框架运用模型

//条件变量与互斥锁代码

#include <stdio.h>

#include <pthread.h>

#include <stdlib.h>

#define MAX_CNT 10

pthread_barrier_t barrier;


void *start_func(void *arg)

{

int *p=(int*)arg;

printf("进入线程:%d,%lu\n",*p,pthread_self());

pthread_barrier_wait(&barrier);

printf("id=%lu,cnt=%d\n",pthread_self(),*p);

free(p);

}


int main()

{

int i;

char *p=NULL;

pthread_t thread[MAX_CNT];

pthread_barrier_init(&barrier,NULL,MAX_CNT);

for(i=0;i<MAX_CNT;i++)

{

p=malloc(4);

*p=i;

pthread_create(&thread[i],NULL,start_func,p);

}

for(i=0;i<MAX_CNT;i++)

{

pthread_join(thread[i],NULL);

}

pthread_barrier_destroy(&barrier);

return 0;

}

2.6 线程信号量

信号量本质上是一个计数器,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个线程独享。

信号量是一种特殊的变量,访问具有原子性, 用于解决进程或线程间共享资源引发的同步问题。

信号量和互斥锁(mutex)的区别:互斥锁只允许一个线程进入临界区,而信号量允许多个线程同时进入临界区,要使用信号量同步,需要包含头文件semaphore.h。

2.6.1​ 信号量实现接口函数

1. 初始化信号量

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value); //通常 pshared 为 0.表示线程间

这是创建信号量的 API,其中 value 为信号量的初值,pshared 表示是否为多进程共享而不仅仅是用于一个进程之间的多线程共享。

如果pshared的值为0,那么信号量在进程的线程之间共享,并且应位于所有线程可见的某个地址(例如,全局变量)能够,或在堆上动态分配的变量),如果pshared不为零,那么信号量在进程之间共享,信号量的值就位于共享内存区域。

2. 注销信号量

int sem_destroy(sem_t * sem);

被注销的信号量 sem 要求已没有线程在等待该信号量,否则返回-1,且置 errno 为 EBUSY。正常返回0

3. 释放信号量

int sem_post(sem_t * sem); //相当于解锁

释放信号量操作将信号量值原子地加 1,表示增加一个可访问的资源。只有信号量值大于 0,才能访问公共资源。主要用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞。

4. 等待信号量

int sem_wait(sem_t * sem); //相当于加锁

int sem_trywait(sem_t * sem); //不阻塞

sem_wait()用于阻塞等待信号量(获取信号量),主要被用来阻塞当前线程直到信号量 sem 的值大于 0,得到信号量之后,信号量的值会减一。

5. 获取信号量值

int sem_getvalue(sem_t * sem, int * sval);

读取sem中的信号量计数,存于*sval 中,并返回 0。

2.6.2 信号量框架运用模型

//条件变量与互斥锁代码

#include <stdio.h>

#include <pthread.h>

#include <stdlib.h>

#include <semaphore.h>

#define MAX_CNT 10

sem_t sem;

pthread_t thread[MAX_CNT];

void *start_func(void *arg)

{

int *p=(int*)arg;

printf("进入线程:%d,%lu\n",*p,pthread_self());

sem_wait(&sem);

printf("id=%lu,cnt=%d\n",pthread_self(),*p);

free(p);

}


void sighandler(int sig)

{

sem_post(&sem);

sem_post(&sem);

}


int main()

{

int i;

char *p=NULL;

sem_init(&sem,0,1);

signal(2,sighandler);

for(i=0;i<MAX_CNT;i++)

{

p=malloc(4);

*p=i;

pthread_create(&thread[i],NULL,start_func,p);

sleep(1);

}

for(i=0;i<MAX_CNT;i++)

{

pthread_join(thread[i],NULL);

}

sem_destroy(&sem);

return 0;

}


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。