Linux下的多线程教程

目录

1.线程技术起源

(1)线程和进程

(2)获取线程ID实例

2.为什么要引入多线程

3.线程的实现

(1)注意事项

(2)创建线程

(3)拓展(线程结束的方式)

(4)初始线程和主线程

(5)线程的基本状态

(6)线程取消

(7)向线程发送信号

(8)进程信号处理和线程信号屏蔽处理

(9)注册和销毁线程

(10)线程私有数据

4.修改线程的属性

5.绑定属性

6.分离属性

7.线程栈的属性

8.优先级设置

9.多线程访问控制

(1)pthread_cond_init函数

(2)pthread_cond_wait()函数

(3)pthread_cond_timewait()函数

(4)pthread_cond_signal()函数

(5)拓展知识点

(6)互斥量属性

(7)读写锁 

(8)条件变量

10.综合例子

11.一次性初始化


Linux中的Make工程管理

Linux中关于使用make管理工具的实例

Linux下的Makefile规则(隐式规则和模式规则)

1.线程技术起源

(1)线程和进程

  • 线程技术早在20世纪60年代就被提出来了,但是真正的倍应用到操作系统中是在80年代中期。
线程和进程
线程进程
标识符类型pthread_tpid_t
获取ID号pthread_self()getpid()
创建pthread_create()fork()

(2)获取线程ID实例

makefile文件书写:

提示:可以看到主函数中的进程ID号和父进程的ID号是一样的,主函数下线程的ID号和打印函数下的线程ID号是不一样的,说明两个不同的线程共享了同样的进程资源。 

2.为什么要引入多线程

  • 由于在Linux下启动一个新的进程必须分配给它独立的地址空间,建立很多的数据表为维护它的代码段,堆栈段和数据段,对于多任务工作来说是非常的“耗资源”的。
  • 运行一个进程中的多个线程,彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的时间,并且线程之间彼此切换所需要的时间也远远小于进程之间切换的时间。
  • 如下图所示,可以看到进程之间都有独立的自己地址空间,并且相隔比较疏远;而对于同一个进程中的两个线程来说,虽然这两个线程之间保持独立的寄存器和堆栈,但是它们之间关系紧密的多。也就是说进程为线程提供运行的资源并构成静态环境,线程是处理机调度的基本单位。

  •  使用多线程方便线程之间进行通信
  • 提高应用程序响应
  • 使多CPU系统更加有效
  • 改善程序结构。

3.线程的实现

(1)注意事项

  • 包含头文件pthread.h:Linux下的多线程遵循POSIX线程接口,称为PTHREAD。
  • 链接的时候需要使用库文件:libpthread.a

(2)创建线程

#include<pthread.h>
int pthread_create((pthread*thread,pthread_attr_t*attr,
                    void*(*start_routine)(void*),void arg))
  • 第一个参数表示为指向线程标识符的指针;
  • 第二个参数表示用来设置线程属性;
  • 第三个参数表示线程运行函数的起始地址;
  • 最后一个参数表示就是运行函数的参数;
  • 返回值:线程创建成功之后,函数返回0,否则返回错误的代码:EAGAIN和EINVAL

(看了上面参数的解释感觉还是不知道在讲什么,到底是什么意思,继续看面,后面通过实例来理解)。

例子:

C文件代码:

  • 参数解释:
    • 第一个参数为线程标识符指针;
    • 第二个参数为NULL,表示生成默认属性的线程;
    • 第三个参数为运行函数thread的起始地址;
    • 第四个参数由于函数thread没有传递参数,所以为NULL;
  • pthread_join:由于一个进程中的多个线程是共享数据段的,所以在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放,因此需要使用pthread_join将当前线程挂起,等待线程的结束。这个是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,等待线程的资源就被释放。
    • 原函数:int pthread_join(pthread_t th,void **thread_return);
      • 第一个参数表示线程标识符指针;
      • 第二个参数表示用户定义的指针,可以用来存储被等待线程的返回值。 

Makefile文件书写: 

提示:如果不使用Makefile文件来管理,也可以直接输入:gcc -lpthread pthread_Create.c -o pthread生成可执行文件pthread. 

Linux中的Make工程管理

Linux中关于使用make管理工具的实例

Linux下的Makefile规则(隐式规则和模式规则)

(3)拓展(线程结束的方式)

  • 方法一:线程创建之后,就开始运行相关的线程函数,函数结束,调用它的线程也就结束;
  • 方法二:通过函数pthread_exit实现结束
    • void pthread_exit(void**retval)
      • 该唯一的参数表示函数的返回代码
        • 当pthread_join中的第二个参数不是NULL的时候,这个值就会被传递给thread_return。
    • int pthread_join(pthread_t tid,void**retval)(可以查看手册:man pthred_join)
      • 作用:调用该函数线程会一直阻塞,直到指定的线程tid调用pthread_exit或者从自动例程返回取消(exit(0))或者使用return返回。
      • 参数解释
        • pthread_t:表示线程的标识符指针id;
        • retval:表示码,如果retval设置为PTHREAD_CANCELED,并且调用成功的话,那么返回0,否则返回错误码。
      • pthread_join会使得执行的线程处于分离的状态。如果指定的线程已经是处于分离状态了,那么调用该函数会失效,其中可以使用pthread_detach分离一个线程:int pthread_detach(pthread_t tid);

例子1:使用三种不同的结束线程的方式,查看输出的结果并总结其中的区别。

makefile文件书写:

 提示:可以看到各个输出的结果,使用pthread_exit和return返回语句的时候会等待主线程的结束,而使用exit的话不会等待主线程的结束直接终止。

例子2:使用pthread_join和pthread_detach实战

首先知道怎么查看错误码:sudo gedit /usr/inlcude/asm-generic/errno-bash.h

makefile文件书写:

 

提示:可以看到thread2的retval2值为22,通过查看上面给出的错误表,可以看到22对应的为 :EINVAL

再查看pthread_join手册:man pthread_join

 

从pthread_join手册可以看到当调用了pthread_detach函数分离线程之后,再调用pthread_join(tid2,&retval2)连接的时候会出错,并且退出码返回的不是2,而是一个计算机中随机的值。

(4)初始线程和主线程

1.初始线程:当程序执行时,首先运行在主函数中,而在线程代码中,这个特殊的执行流程也称为初始线程。

2.当主函数结束时,会导致进程结束,进程内所有的线程也都会结束。但是可以调用pthread_exit函数(后面详解),等待所有的线程结束时进程才终止。

3.大多数情况下,主线程默认是在堆栈上运行的,该堆栈可以增长到足够的长度,但是普通的堆栈是受限制的,一旦溢出将会产生错误。

例子1:定义一个结构体,并且创建一个线程,将结构体传入,最后打印输出主函数参数。

 

Makefile文件书写:

拓展:可以上面这个例子在主函数的最后使用sleep进行了睡眠,但是如果将该睡眠放置在thread函数中会是什么结果呢?如下:

提示:从输出的结果来判断,由于在thread设置了睡眠,可以发现主函数在thread函数打印出之前就已经终止线程了,所以 thread函数还没有来得及打印结果,线程就已经结束了。

那么解决这个问题了,这个时候就可以使用上面提到的pthread_exit函数让等待所有的线程结束之后再终止。如下:

  

例子2:主函数打印奇数,创建的线程打印偶数。

  

(5)线程的基本状态

  • 就绪状态:线程能够运行,但是在等待可用的处理器(CPU);
  • 运行状态:线程正在运行,在多核系统中,可能同时有多个线程在运行;
  • 阻塞状态:线程在等待处理器以外的其他条件;
  • 终止状态:线程从启动函数中返回,或者调用pthread_exit函数,或者被取消。

提示:如果读者学习过操作系统的话,上面的知识点将非常容易理解。

(6)线程取消

  • int pthread_cancel(pthread_t tid)
    • 取消线程tid,取消成功则返回0;但是取消只是发送一个请求,并不意味着等待线程就会终止,而且发送成功也不是一定意味着线程tid一定会终止。
  • 取消状态
    • int pthread_setcancelstate(int state,int*oldstate);
      • 设置本线程对Cancel信号的反应,state有两种值:
        • PTHREAD_CANCEL_ENABLE(默认值):表示接收到信号后设置为CANCELED状态。
        • PTHREAD_CANCEL_DISABLE:表示忽略CANCEL信号继续运行;
      • old_state:如果不加NULL,则加入原来的Cancel状态以便恢复;
      • 返回值:如果取消成功,则返回0,否则返回错误码!
  • 取消类型
    • int pthread_setcanceltype(int type,int *oldtype)
      • 设置本线程取消动作的执行时机。
      • type两种值
        • PTHREAD_CANCEL_DEFFERED:表示接收到信号后继续运行下去直到下一个取消点再退出。
        • PTHREAD_CANCEL_ASYCHRONOUS:表示立即执行取消动作,后面的程序代码将不会继续执行;
      • old_state:如果不加NULL,则加入原来的Cancel状态类型;
      • 返回值:如果设置取消类型成功,则返回0,否则返回错误码!

Makefile文件书写: 

 提示:从退出码的结果20来看,退出是正常的。

但是如果进行下面的改动之后:将取消线程的后面设置取消类型,这个取消类型设置为立即执行(也就是后面的操作不再执行)。

 

 提示:从输出的结果来看,printf("finally!\n")没有执行,并且退出码也不是20,说明不是正常的退出,说明设置取消和设置取消类型成功。

(7)向线程发送信号

  • int pthread_kill(phtread_t tid,int sig);
    • phtread_kill不是终止线程的意思,表示向线程发送信号。
    • 方式:向线程tid发送信号,如果线程代码内不做处理,则按照信号默认的行为影响整个进程。也就是说如果给线程发送了一个信号,但是该信号没有被处理,那么整个进程将推出。
    • 参数sig:
      • 如果sig为0的话,表示是一个保留的信号,用来判断进行是否终止。
      • 查看手册:man pthread_kill。
    • 返回值:如果是0,表示发送成功;否则返回错误码。

例子:测试新建的线程结束之后,再向新建的线程发送一个0的信号,如果出错的话,那么将打印出错的信息。

提示:为什么是这样的结果?主要是因为在主线程中睡眠了1秒,所以这个时候新线程已经执行完毕,那么再向新线程发送信息,将会出错! 

(8)进程信号处理和线程信号屏蔽处理

  • int sigaction(int signum,const struct sigaction*act,struct sigaction*oldact);
    • 参数解释:
      • signum:给信号signum设置一个处理函数,而处理函数在sigaction中指定。
      • sigaction:一个结构体,其中包含的内容如下:可以查看手册man sigaction

        • 其中sa_mask表示信号屏蔽字;sa_handle表示信号处理函数。
  • ing pthread_sigmask(int how,const sigset_t*set,sigset_t*oldset)
    • 多线程信号屏蔽处理;
    • 参数解释:
      • how=SIG_BLOCK:向当前的信号掩码中添加set,其中set表示要阻塞的信号组;
      • SIG_UNBLOCK:向当前的信号掩码中删除set,其中set表示要取消阻塞的信号组;
      • SIG_SETMASK:将当前的信号掩码替换为set,其中set表示新的信号掩码。

例子:

Makefile文件书写:

(9)注册和销毁线程

  • 注册处理程序:pthread_cleanup_push(void(*routine)(void),void*args);
  • 清除处理程序:pthread_cleanup_pop(int execute);
    • routine是处于栈顶的清除处理函数;
    • args表示处理函数的参数;
    • execute:表示当为非0的时候执行。
    • 注意:清除的顺序和入栈的顺序是相反的(栈——后进先出)。
  • 注意:这两个函数必须同时使用。

例子:使用注册处理函数和清除处理函数实现一个清除的过程。

提示:如果将pthread_cleanup_pop(0)参数修改为0的时候,是不执行清除操作的,但是如果修改成1的话,那么将执行清除操作:

提示:如果调用pthread_exit的话,也会响应清除请求,比将代码修改为如下面:

  

(10)线程私有数据

在线程的私有数据之间,尽管名字是相同的,但是线程访问的都是数据的副本。

 

首先需要创建一个与私有数据相关的键,获取对私有数据的访问权限。

  • int pthread_key_create(pthread_key_t*key,void(*destructor)(void*));
    • 其中创建的key放在指定的内存单元, destructor是与键相关的析构函数(当调用pthread_exit或者return的时候,会调用析构函数)。当析构函数被调用的时候,只有一个参数,就是与key关联的数据地址。
    • 使用键可以和私有数据进行关联,之后就可以通过键来查找数据,所有的线程都可以访问创建的键。
  • 将键与私有数据进行关联
    • void*pthread_setspecific(pthread_key_t key,const void*value);
  • 获取私有数据,如果没有关联,则返回空
    • void*pthread_getspecific(pthread_key_t key);

 

 

 

提示:可以设置不同的键值,之间是不相互影响的。 

4.修改线程的属性

提示:对于大部分创建线程的情况,使用默认的属性即可。

  • 属性结构为pthread_attr_t
  • 初始化函数为pthread_attr_init:该函数必须在创建线程之前调用;
  • 属性对象
    • 是否绑定
    • 是否分离
    • 堆栈大小
    • 堆栈地址
    • 优先级
  • 默认属性为
    • 非绑定
    • 非分离
    • 缺省1MB的堆栈和父进程同样的优先级别

5.绑定属性

线程的绑定需要:轻进程LWP(Light Weight Process)

  • 位于用户层和系统层之间
  • 系统对于线程资源的分配和对线程的控制是通过轻进程实现的
  • 一个轻进程可以控制一个或者多个线程
  • 非绑定:默认属性情况下,启动多少轻进程,哪些轻进程来空间哪些线程是由系统来控制的(非绑定)
  • 绑定:某个线程固定的“绑”在一个轻进程之上,并且被绑定的线程具有较高的响应速度。
pthread_attr_t attr;
pthread_t tid;
//初始化属性,均为默认属性
pthread_attr_init(&attr);
pthread_attr_setscope(&attr,PTHREAD_SCOPE_SYSTEM);
//创建一个线程,thread是定义的一个函数
pthread_create(&id,&attr,(void*)thread,NULL);
  • pthread_attr_setscope参数解释
    • 第一个参数为属性结构的指针;
    • 第二个参数为绑定类型,其中的PTHREAD_SCOPE_SYSTEM(绑定),PTHREAD_SCOPE_PROCESS(非绑定)。

6.分离属性

分离属性决定一个线程 以什么样的方式终止:

  • 默认属性情况下为非分离属性,也就是原有的线程等待创建的线程结束;
  • 默认情况下只有当pthread_join函数返回时,创建的线程才算终止,才能释放自己的资源。
  • 如果设置为分离的属性的话,不会被其他的线程所等待,自己运行结束了,线程也就结束了,马上释放资源
  • void pthread_attr_setdetachstate(pthread_attr_t*attr,int detachstate)
    • 参数解释:
      • 第一个参数为属性结构的指针;
      • 第二个参数可为:
        • PTHREAD_CREATE_DETACHED(分离属性)
        • PTHREAD_CREATE_JOINABLE(非分离属性)
  • 设置了分离属性之后,该线程会运行很快。
  • 注意:
    • 由于设置了分离属性之后线程运行的很快,所以很有可能在pthread_create之前就可以终止并释放资源,那么会导致pthread_create线程得到错误的代码,所以解决方法是采取同步的措施。
      • 在被创建的线程里调用pthread_cond_timewait函数,让这个线程等一会,使得pthread_create有足够的时间返回。
      • 不要使用wait()方法,因为这样会使得整个线程处于睡眠,不能解决线程同步的问题。
  • 获得分离属性:int pthread_attr_getdetachstate(pthread_attr_t*attr,int detachstate);
  • 初始化:int pthread_attr_init(pthread_attr_t*attr);
  • 销毁:int pthread_attr_destroy(pthread_attr_t*attr);

  • 设置分离属性的步骤:
    • 定义线程属性变量:pthread_attr_t attr;
    • 初始化attr:pthread_attr_init(&attr);
    • 设置线程为分离属性或者非分离属性:pthread_attr_setdetachstate(&attr,detachstate);
    • 最后创建线程:pthread_create(&tid,&attr,thread,NULL);

例子:通过设置分离属性和一个非设置分离属性(默认)线程,输出查看设置分离属性和非分离属性的结果。

  

提示:可以看到线程1在设置了分离的属性之后,连接是失败的。 

7.线程栈的属性

对于线程,虚拟地址空间大小是固定的,进程只有一个栈;但是对于线程,同样的虚拟地址被所有的线程共享,如果应用程序使用了太多的线程,那么导致线程累计和超过可用的虚拟地址空间,那么这个时候需要减少线程默认的栈大小;但是如果线程分配了大量的自动变量或者线程的栈太深,那么这个时候需要使得默认的栈更大。

  • 引入头文件<limits.h>
  • 查看栈的大小:ulimit -s
    • 修改栈属性
      • 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);
      • 其中stackaddr表示栈的内存单元最低地址;
      • stacksize表示栈的大小;
    • 设置栈的大小
      • int pthread_attr_setstacksize(pthread_attr_t*attr,size_t stacksize);
    • 获取栈的大小
      • int pthread_attr_getstacksize(pthread_attr_t*attr,size_t stacksize);
    • 检查是否可以修改栈的属性
      • _POSIX_THREAD_ATTR_STACKADDR和_POSIX_THREAD_ATTR_STACKSIZE用来检查系统是否支持线程属性(宏定义的位置:/usr/include/bits/posix_opt.h)
  • 设置栈溢出警戒区
    • 设置线程栈末尾
      • int pthread_attr_setguardsize(pthread_attr_t*attr,size_t guardsize);
    • 获取线程栈末尾
      • int pthread_attr_getguardsize(pthread_attr_t*attr,size_t guardsize);
    • 设置线程栈末尾guardsize主要是避免栈溢出的扩展内存大小,默认是PAGESIZE个字节,如果设置为0,就不提供警戒缓冲区。

 

8.优先级设置

比较常用的属性是线程的优先级

  • 存放的结构:sched_param
  • 调用函数:pthread_attr_getschedparam和pthread_attr_setschedparam.
  • 总是首先获取优先级,获取优先级之后再是设置优先级。
#include<pthread.h>
#include<sched.h>

pthread_t tid;
pthread_attr_t attr;
sched_param param;

int newprio=20;

pthread_attr_init(&attr);
//首先获取优先级
pthread_attr_getschedparam(&attr,&param);

//获取优先级之后再设置优先级
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr,&param);

//创建线程,其中thread是定义的一个函数,thparam是thread函数的参数
pthread_create(&tid,&attr,(void*)thread,thparam);

9.多线程访问控制

由于多线程共享同一个进程的资源和地址,因此对这些资源的操作,必须要考虑线程之间资源访问的唯一性问题。

  • 线程同步可以使用互斥锁和信号量机制(操作系统之间有详细讲解)来解决线程之间数据的共享和通信问题。
  • 互斥锁
    • 锁定和非锁定
  • 条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。
    • 当条件变量被用来阻塞一个线程,当条件不满足时,线程解开相应的互斥锁并等待条件发生变化。
    • 一旦其他的某个线程改变了条件变量,将通知相应的条件变量唤醒一个或者多个正在被此条件变量阻塞的线程。
    • 这些线程将重新锁定互斥锁并重新测试条件是否满足。

例子:假设两个线程没有同步,输出结果:

  

(1)pthread_cond_init函数

  • 函数条件变量的结构为pthread_cond_t,函数pthread_cond_init()用来初始化条件变量
    • int pthread_cond_init(pthread_cond_t*cond,__const pthread_condattr_t*cond_attr);
    • 参数解释
      • 第一个参数为指向结构pthread_cond_t的指针;
      • 第二参数cond_attr指向一个结构pthread_condattr。
        • pthread_condattr是条件变量的属性结构,与互斥锁一样可以用来设置条件变量是进程内还是进程间可用,默认属性为PTHREAD_PROCESS_PRIVATE,表示被同一进程内的各个线程使用。
    • 注意:当初始化条件变量只有未被使用时才能重新初始化或者被释放。

(2)pthread_cond_wait()函数

  • 使得线程阻塞在一个条件变量上面
  • extern int pthread_cond_wait(pthread_cond_t*__restrict__cond,pthread_mutex_t*__restrict_mutex)
  • 线程解开mutex指向的锁并被条件变量cond阻塞。
  • 线程可以被函数pthread_cond_signal和函数pthread_cond_broadcast唤醒,其中条件变量只是起唤醒和阻塞的作用,具体的判断条件需要自己给出。

(3)pthread_cond_timewait()函数

  • 用来阻塞线程
  • extern int pthread_cond_timewait__P((pthread_cond_t*__cond,pthread_mutex_t*__mutex,__const struct timespec*__abstime))
  • 相比于pthread_cond_wait函数而言,多了一个时间参数,经历了abstime时间之后,即使条件变量不满足,阻塞也将被解除。

(4)pthread_cond_signal()函数

  • 用来阻塞在条件变量cond上的一个线程。
  • extern int pthread_cond_signal(pthread_cond_t*__cond)
  • 决定哪个线程被唤醒,是有线程的调度策略决定的。
  • 必须使用保护条件变量的互斥锁来保护这个函数,否则条件满足信号可能在测试条件和调用pthread_cond_wait函数之间被发出,造成无限制的等待。

(5)拓展知识点

  • 获得父进程的ID:
    • pthread_t pthread_self(void);
  • 测试两个线程号是否相同:
    • int pthread_equal(pthread_t __thread1,pthread_t __thread2);
  • 互斥量初始化:
    • pthread_mutex_init(pthread_mutex_t*,__const pthread_mutexattr_t*);
    • 如果是动态分配的互斥量使用此方式进行初始化。
    • 如果是静态分配的互斥量,将它设置为常量:PTHREAD_MUTEX_INITIALIZER
    • 第一个参数为初始化的互斥量,第二个参数为属性,通常为NULL(默认)。
  • 销毁互斥量:
    • int pthread_mutex_destry(pthread_mutex_t*__mutex);
    • 如果是动态分配的互斥量使用此方式进行销毁。
  • 获得互斥量的锁定(非阻塞):
    • int pthread_mutex_trylock(pthread_mutex_t*__mutex);
    • 返回值:如果为0,表示成功,否则返回错误码。
    • 如果互斥量已经被锁住,那么不会导致该线程被阻塞。
  • 锁定互斥量(阻塞):
    • int pthread_mutex_lock(pthread_mutex_t*__mutex);
    • 返回值:如果为0,表示成功,否则返回错误码。
    • 如果互斥量已经被锁住,那么会导致该线程被阻塞。
  • 解锁互斥量:
    • int pthread_mutex_unlock(pthread_mutex_t*__mutex);

例子1:依然是上面的一个例子进行修改。

(6)互斥量属性

 线程互斥量属性pthread_mutexattr_t类型的数据表示。

  • 互斥量初始化
    • int pthread_mutexattr_init(pthread_mutexattr_t*attr);
  • 互斥量销毁
    •  int pthread_mutexattr_destroy(pthread_mutexattr_t*attr);

进程共享属性两种值:

  • PTHREAD_PROCESS_PRIVATE:默认值,同一个进程中的多个线程访问同一个同步对象。
  •  PTHREAD_PROCESS_SHARED:可以使得互斥量在多个进程中进行同步,如果互斥量在多进程的共享内存区域,那么这个属性的互斥量可以同步多进程。
  • 设置互斥量进程共享属性
    • int pthread_mutexattr_getshared(const pthread_mutexattr_t*restrict attr,int *restrict pshared);
    •  int pthread_mutexattr_setshared(const pthread_mutexattr_t*attr,int * pshared);
  • 检查共享属性否支持:_POSIX_THREAD_PROCESS_SHARED
互斥量类型属性
互斥量类型没有解锁时再次加锁不占用解锁已解锁时再次解锁
PTHREAD_MUTEX_NORMLA死锁未定义未定义
PTHREAD_MUTEX_ERRORCHEK返回错误返回错误返回错误
PTHREAD_MUTEX_RECURSIVE允许返回错误返回错误
PTHREAD_MUTEX_DEFAULT未定义未定义未定义
  • 设置互斥量的类型属性
    • int pthread_mutexattr_gettype(const pthread_mutexattr_t*restrict attr,int *restrict type);
    • int pthread_mutexattr_settype(const pthread_mutexattr_t* attr,int  type);

注意下面的程序编译方式:sudo gcc -o pDemo01 pDemo01.c -lrt

  

 

使用Makefile文件:

 

 

(7)读写锁 

  • 初始化读写锁: int pthread_rwlock_init(pthread_rwlock_t*restrict rwlock,const pthread_rwlockattr_t*restrict attr);
    • 返回值:如果为0,表示成功,否则返回错误码。
    • 读模式加锁:
      • int pthread_rwlock_rdlock(pthread_rwlock_t*rwlock);
      • int pthread_rwlock_tryrdlock(pthread_rwlock_t*rwlock);
    • 写模式加锁:
      • int pthread_rwlock_wrlock(pthread_rwlock_t*rwlock);
      • int pthread_rwlock_trywrlock(pthread_rwlock_t*rwlock);
    • 解锁:
      • int pthread_rwlock_unlock(pthread_rwlock_t*rwlock);
  • 销毁读写锁:int pthread_rwlock_destroy(pthread_rwlock_t*rwlock);
    • 返回值:如果为0,表示成功,否则返回错误码。

例子:验证两个线程可以同时读锁。

 提示:

1.可以看到当线程1拥有了读锁之后,线程2也可以拥有读锁。

2.但是对于写加锁状态的话,当两个线程都是写状态的时候,只能有一个线程在执行,只有当该线程执行完毕之后,那么另一个线程才能开始执行。

3.如果是一个写加锁,另一个是读加锁的话,也是只能有一个线程在执行,只有当该线程执行完毕之后,那么另一个线程才能开始执行。

(8)条件变量

  • 条件变量初始化:
    • int pthread_cond_init(pthread_cond_t*__restrict __cond,__const pthread_condattr_t*__restrict_cond_attr);
    • pthread_cond_t cond=PTHREAD_COND_INITLALIZER;
  • 销毁条件变量COND:
    • int pthread_cond_destroy(pthread_cond_t*__cond);
  • 唤醒线程等待条件变量:
    • 至少唤醒等待条件的某一个线程:int pthread_cond_signal(pthread_cond_t*__cond);
    • 唤醒等待条件的所有线程:int pthread_cond_broadcast(pthread_cond_t*__cond);
  • 等待条件变量(阻塞):
    • int pthread_cond_wait(pthread_cond_t*__restrict __cond,pthread_mutex_t* __restrict_mutex);
    • 使用等待条件变量为真,传递给pthread_cond_wait的互斥量对条件进行保护,调用为者把锁住的互斥量传递给函数;
    • 将线程放到等待条件的线程列表上,然后对互斥量进行解锁,当条件满足这个函数返回,返回后进行对互斥量进行加锁。
  • 在指定时间到达前等待条件变量:
    • int pthread_cond_timewait(pthread_cond_t* __restrict_cond,pthread_mutex*__restrict__mutex,__const struct timepec*__restrict__abstime);
    • timewait指定时间,如果到了指定的时间条件还是不满足的话,那么就返回。
    • timewait的结构体内容如下:
      • time_t tv_sec;
      • long_t tv_nsec;

例子1:交替打印奇数和偶数

 

  

例子2:下面的综合例子消费者和生产者就是最好的应用。

10.综合例子

著名的生产者-消费者问题模型的实现,主程序中分别启动生产者线程和消费者 线程。生产者线程不断顺序地将 0 到 50 的数字写入共享的循环缓冲区,同时消费者线程 不断地从共享的循环缓冲区读取数据。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
#define BUFFER_SIZE 16
/*  设置一个整数的圆形缓冲区 */
struct prodcons {
	int buffer[BUFFER_SIZE];	/* 缓冲区数组 */
	pthread_mutex_t lock;	/* 互斥锁 */
	int readpos, writepos;	/*  读写的位置*/
	pthread_cond_t notempty;	/*  缓冲区非空信号 */
	pthread_cond_t notfull;	/*缓冲区非满信号 */
};
/*--------------------------------------------------------*/
/*初始化缓冲区*/
void init(struct prodcons * b)
{
	pthread_mutex_init(&b->lock, NULL); 
	pthread_cond_init(&b->notempty, NULL);  
	pthread_cond_init(&b->notfull, NULL);
	b->readpos = 0;
	b->writepos = 0;
}
/*--------------------------------------------------------*/
/*  向缓冲区中写入一个整数*/
void put(struct prodcons * b, int data)
{
	pthread_mutex_lock(&b->lock);
	/*等待缓冲区非满 when the buffer is not full and you can write a number*/
	while ((b->writepos + 1) % BUFFER_SIZE == b->readpos) {
		printf("wait for not full\n");
		pthread_cond_wait(&b->notfull, &b->lock);
	}
	/*写数据并且指针前移*/
	b->buffer[b->writepos] = data;
	b->writepos++;
	if (b->writepos >= BUFFER_SIZE) b->writepos = 0;
	/*设置缓冲区非空信号*/
	pthread_cond_signal(&b->notempty);
	pthread_mutex_unlock(&b->lock);
}
/*--------------------------------------------------------*/
/*从缓冲区中读出一个整数 */
int get(struct prodcons * b)
{
	int data;
	pthread_mutex_lock(&b->lock);
	/*等待缓冲区非空when the buffer is not empty and you can read a number*/
	while (b->writepos == b->readpos) { 
		printf("wait for not empty\n"); 
		pthread_cond_wait(&b->notempty, &b->lock);
	}

	/*  读数据并且指针前移 */ 
	data = b->buffer[b->readpos]; 
	b->readpos++;
	if (b->readpos >= BUFFER_SIZE) b->readpos = 0;
	/*  设置缓冲区非满信号*/
	pthread_cond_signal(&b->notfull);
	pthread_mutex_unlock(&b->lock);
	return data;
}
/*--------------------------------------------------------*/
#define OVER (-1)
struct prodcons buffer;
/*--------------------------------------------------------*/
void * producer(void * data)
{
	int n;
	for (n = 0; n < 50; n++) { 
		printf(" put-->%d\n", n); 
		put(&buffer, n);
	}
	put(&buffer, OVER); 
	printf("producer stopped!\n"); 
	return NULL;
}
/*--------------------------------------------------------*/
void * consumer(void * data)
{
	int d;
	while (1) {
		d = get(&buffer);
		if (d == OVER ) break;
		printf("	%d-->get\n", d);
	}
	printf("consumer stopped!\n");
	return NULL;
}
/*--------------------------------------------------------*/
int main(void)
{
	pthread_t th_a, th_b; 
	void * retval; 
	init(&buffer);
	pthread_create(&th_a, NULL, producer, 0);
	pthread_create(&th_b, NULL, consumer, 0);
	/*  等待生产者和消费者结束 */ 
	pthread_join(th_a, &retval); 
	pthread_join(th_b, &retval); 
	return 0;
}

11.一次性初始化

像互斥量只能初始化一次,一般的初始化方法都是放在主函数main中,但是对于库函数不能这样做,如果要实现一次性初始化,可以使用下面的方法:

  • 首先定义一个pthread_once_t类型的变量,使用PTHREAD_ONCE_INIT进行初始化
    • pthread_once_t once=PTHREAD_ONCE_INIT;
  • 初始化函数书写:

void init_routine(){

        //初始化互斥量

        //初始化读写锁

}

  • 调用函数
    • int pthread_once(pthread_once_t*once_control,void(*init_routine)(void))
    • 函数功能:该函数使用PTHREAD_ONCE_INIT初始化once_control变量,保证init_routine函数在本进程中执行时,仅仅执行一次。在多个线程的情况下,即使pthread_once函数被调用了多次,init_routine函数也只会执行一次。
    • PTHREAD_ONCE_INIT如果为0,表示pthread_once从没有执行过,init_routine函数就会执行;
    • PTHREAD_ONCE_INIT如果为1,表示pthread_once都必须等待其中一个激发“已经执行一次”信号,因此所有的pthread_once都会处于阻塞当中,init_routine函数就不会执行;
    • PTHREAD_ONCE_INIT如果为2,表示pthread_once已经执行过一次了,init_routine函数以后都不会执行了;

 提示:如果将once的值设置为1,那么将处于阻塞状态。

教程主要来源

《嵌入式Linux操作系统应用于开发》

【linux多线程编程】

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值