Linux高并发服务器开发 :linux系统编程 1-8 线程和线程同步

01. 线程简介

1.1 线程概念

在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。

所以,线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。

为了让进程完成一定的工作,进程必须至少包含一个线程。

在这里插入图片描述
进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是CPU分配资源的最小单位。

线程存在与进程当中(进程可以认为是线程的容器),是操作系统调度执行的最小单位。说通俗点,线程就是干活的。

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

如果说进程是一个资源管家,负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。

进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆啊,栈啊,静态存储区什么的。

进程是操作系统分配资源的最小单位

线程是操作系统调度的最小单位

1.2 线程函数列表安装

命令:

sudo apt-get install manpages-posix-dev

【说明】manpages-posix-dev 包含 POSIX 的 header files 和 library calls 的用法

查看:

man -k pthread

1.3 NPTL

当 Linux 最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone() 系统调用将进程作为可调度的实体。这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享相同的地址空间。LinuxThreads 项目使用这个调用来完全在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程间同步原语方面都存在问题。另外,这个线程模型也不符合 POSIX 的要求。

要改进 LinuxThreads,非常明显我们需要内核的支持,并且需要重写线程库。有两个相互竞争的项目开始来满足这些要求。一个包括 IBM 的开发人员的团队开展了 NGPT(Next-Generation POSIX Threads)项目。同时,Red Hat 的一些开发人员开展了 NPTL 项目。NGPT 在 2003 年中期被放弃了,把这个领域完全留给了 NPTL。

NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。

查看当前pthread库版本:getconf GNU_LIBPTHREAD_VERSION

在这里插入图片描述

1.4 线程的特点

类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。

因此在这类系统中,进程和线程关系密切:

  1. 线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone

  2. 从内核里看进程和线程是一样的,都有各自不同的PCB.

  3. 进程可以蜕变成线程

  4. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位

在这里插入图片描述
查看指定进程的LWP号:

ps  -Lf  pid

实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone 。

Ø 如果复制对方的地址空间,那么就产出一个“进程”

Ø 如果共享对方的地址空间,就产生一个“线程”。

Linux内核是不区分进程和线程的, 只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

1.5 线程共享资源

  1. 文件描述符表

  2. 每种信号的处理方式

  3. 当前工作目录

  4. 用户ID和组ID

内存地址空间 (.text/.data/.bss/heap/共享库)

1.6 线程非共享资源

  1. 线程id

  2. 处理器现场和栈指针(内核栈)

  3. 独立的栈空间(用户空间栈)

  4. errno变量

  5. 信号屏蔽字

  6. 调度优先级

1.7 线程的优缺点

优点:

Ø 提高程序并发性

Ø 开销小

Ø 数据通信、共享数据方便

缺点:

Ø 库函数,不稳定

Ø 调试、编写困难、gdb不支持

Ø 对信号支持不好

优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

02. 线程常用操作

2.1 线程号

就像每个进程都有一个进程号一样,每个线程也有一个线程号。进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。

进程号用 pid_t 数据类型表示,是一个非负整数。线程号则用 pthread_t 数据类型来表示,Linux 使用无符号长整数表示。

有的系统在实现pthread_t 的时候,用一个结构体来表示,所以在可移植的操作系统实现不能把它做为整数处理。

pthread_self函数:

#include <pthread.h>pthread_t pthread_self(void);
功能:
    获取线程号。
参数:
    无
返回值:
    调用线程的线程 ID 。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#include<pthread.h>

//线程常用函数

int main(){
    pthread_t tid = 0;
    //获取当前线程的线程号
    tid = pthread_self();
    //pthread_t为long无符号数
    printf("tid:%lu\n",tid);

    return 0;
}

注意
通过 zbc@zbc-virtual-machine:~/1-8$ gcc 3pthread_self.c -pthread 来链接线程库 否则结果不对

在这里插入图片描述

pthread_equal函数:

int pthread_equal(pthread_t t1, pthread_t t2);
功能:
    判断线程号 t1 和 t2 是否相等。为了方便移植,尽量使用函数来比较线程 ID。
参数:
    t1,t2:待判断的线程号。
返回值:
    相等:  非 0
    不相等:0

参考程序:


int main()
{
    pthread_t thread_id;
​
    thread_id = pthread_self(); // 返回调用线程的线程ID
    printf("Thread ID = %lu \n", thread_id);if (0 != pthread_equal(thread_id, pthread_self()))
    {
        printf("Equal!\n");
    }
    else
    {
        printf("Not equal!\n");
    }return 0;
}

22 线程的创建

pthread_create函数:

#include <pthread.h>int pthread_create(pthread_t *thread,
            const pthread_attr_t *attr,
            void *(*start_routine)(void *),
            void *arg );
功能:
    创建一个线程。
参数:
    thread:线程标识符地址。
    attr:线程属性结构体地址,通常设置为 NULL。
    start_routine:线程函数的入口地址。
    arg:传给线程函数的参数。
返回值:
    成功:0
    失败:非 0

在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。

由于pthread_create的错误码不保存在errno中,因此不能直接用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印。


// 回调函数
void *thread_fun(void * arg)
{
    sleep(1);
    int num = *((int *)arg);
    printf("int the new thread: num = %d\n", num);return NULL;
}int main()
{
    pthread_t tid;
    int test = 100;// 返回错误号
    int ret = pthread_create(&tid, NULL, thread_fun, (void *)&test);
    if (ret != 0)
    {
        printf("error number: %d\n", ret);
        // 根据错误号打印错误信息
        printf("error information: %s\n", strerror(ret));
    }printf("main thread: %s\n", pthread_self());
    printf("按下任意键继续\n");getchar();
	
    return 0;
}

线程和进程共享数据段、代码段、堆段、BBS区

2.3 线程资源回收

pthread_join函数:

#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
功能:
    等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。如果线程已经结束,那么该函数会立即返回。
参数:
    thread:被等待的线程号。
    retval:用来存储线程退出状态的指针的地址。
返回值:
    成功:0
    失败:非 0
void *thead(void *arg)
{
    static int num = 123; //静态变量printf("after 2 seceonds, thread will return\n");
    sleep(2);return &num;
}int main()
{
    pthread_t tid;
    int ret = 0;
    void *value = NULL;// 创建线程
    pthread_create(&tid, NULL, thead, NULL);
​
​
    // 等待线程号为 tid 的线程,如果此线程结束就回收其资源
    // &value保存线程退出的返回值
    pthread_join(tid, &value);printf("value = %d\n", *((int *)value));return 0;
}
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>

#include<pthread.h>

//线程处理函数
void *fun(void *arg){
    int i=0;
    for(int i=0;i<5;i++){
        printf("fun thread do working %d\n",i);
        sleep(1);
    }
    return (void*)0x03;
}

int main(){
    int ret = -1;
    void *retp = NULL;
    pthread_t tid = -1;
    
    //创建一个线程
    ret = pthread_create(&tid,NULL,fun,NULL);
    if(0!=ret){
        printf("pthread_creat filed...\n");
        return 1;
    }
    printf("main thread runing....\n");

    //等待线程结束 会阻塞
    ret = pthread_join(tid,&retp);
    if(0 != ret){
        printf("pthread_join failed....");
        return 1;
    }
    printf("retp:%p\n",retp);
    printf("main thread exit.....\n");

    return 0;

}

由于pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以在使用pthread_create()创建线程,以及调用 pthread_atfork()函数建立fork处理程序时,在编译中要加 -lpthread参数。

在这里插入图片描述
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值

  2. 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED

  3. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数

2.4 线程分离

一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。

不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

pthread_detach函数:

#include <pthread.h>int pthread_detach(pthread_t thread);
功能:
    使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞。
参数:
    thread:线程号。
返回值:
    成功:0
    失败:非0
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>

#include<pthread.h>

//线程处理函数
void *fun(void *arg){
    int i=0;
    for(int i=0;i<5;i++){
        printf("fun thread do working %d\n",i);
        sleep(1);
    }
    return (void*)0x03;
}

int main(){
    int ret = -1;
    void *retp = NULL;
    pthread_t tid = -1;
    
    //创建一个线程
    ret = pthread_create(&tid,NULL,fun,NULL);
    if(0!=ret){
        printf("pthread_creat filed...\n");
        return 1;
    }
    printf("main thread runing....\n");

    //设置线程分离
    ret = pthread_detach(tid);
    if(0!=ret){
        printf("pthread_detach filed...\n");
        return 1;
    }

    //测试分离窗台状态的线程是否可以join
    ret = pthread_join(tid,NULL);
    if(0!=ret){
        printf("pthread_join filed...\n");
    }else{
        printf("pthread_join ok...\n");
    }

    
    printf("按下任意键退出...\n");
    getchar();

    return 0;
}

在这里插入图片描述

2.5 线程退出

在进程中我们可以调用exit函数或**_exit**函数来结束进程,在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。

  • 线程从执行函数中返回。
  • 线程调用pthread_exit退出线程。
  • 线程可以被同一进程中的其它线程取消。
pthread_exit函数:

#include <pthread.h>void pthread_exit(void *retval);
功能:
    退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放。
参数:
    retval:存储线程退出状态的指针。
返回值:无  


参考程序:

void *thread(void *arg)
{
    static int num = 123; //静态变量
    int i = 0;
    while (1)
    {
        printf("I am runing\n");
        sleep(1);
        i++;
        if (i == 3)
        {
        	//相当于return (void *)&num
            pthread_exit((void *)&num);
            // return &num;
        }
    }return NULL;
}int main(int argc, char *argv[])
{
    int ret = 0;
    pthread_t tid;
    void *value = NULL;pthread_create(&tid, NULL, thread, NULL);
​
​
    pthread_join(tid, &value);
    printf("value = %d\n", *(int *)value);return 0;
}

2.6 线程取消

#include <pthread.h>int pthread_cancel(pthread_t thread);
功能:
    杀死(取消)线程
参数:
    thread : 目标线程ID。
返回值:
    成功:0
    失败:出错编号


注意:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点(检查点)。

类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。

杀死线程也不是立刻就能完成,必须要到达取消点。

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。

可粗略认为一个系统调用(进入内核)即为一个取消点。

参考程序:

void *thread_cancel(void *arg)
{
    while (1)
    {
        pthread_testcancel(); //设置取消点
    }
    return NULL;
}int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, thread_cancel, NULL); //创建线程sleep(3);                   //3秒后
    pthread_cancel(tid); //取消tid线程pthread_join(tid, NULL);return 0;
}

03. 线程属性(了解)

3.1 概述

Linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。

如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。

typedef struct
{
    int             etachstate;     //线程的分离状态
    int             schedpolicy;    //线程调度策略
    struct sched_param  schedparam; //线程的调度参数
    int             inheritsched;   //线程的继承性
    int             scope;      //线程的作用域
    size_t          guardsize;  //线程栈末尾的警戒缓冲区大小
    int             stackaddr_set; //线程的栈设置
    void*           stackaddr;  //线程栈的位置
    size_t          stacksize;  //线程栈的大小
} pthread_attr_t;

主要结构体成员:

  1. 线程分离状态

  2. 线程栈大小(默认平均分配)

  3. 线程栈警戒缓冲区大小(位于栈末尾)

  4. 线程栈最低地址

属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。

线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。

3.2 线程属性初始化和销毁

#include <pthread.h>int pthread_attr_init(pthread_attr_t *attr);
功能:
    初始化线程属性函数,注意:应先初始化线程属性,再pthread_create创建线程
参数:
    attr:线程属性结构体
返回值:
    成功:0
    失败:错误号
​
int pthread_attr_destroy(pthread_attr_t *attr);
功能:
    销毁线程属性所占用的资源函数
参数:
    attr:线程属性结构体
返回值:
    成功:0
    失败:错误号

3.3 线程分离状态

线程的分离状态决定一个线程以什么样的方式来终止自己。

非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
相关函数:

#include <pthread.h>int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
功能:设置线程分离状态
参数:
    attr:已初始化的线程属性
    detachstate:    分离状态
        PTHREAD_CREATE_DETACHED(分离线程)
        PTHREAD_CREATE_JOINABLE(非分离线程)
返回值:
    成功:0
    失败:非0
    
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
功能:获取线程分离状态
参数:
    attr:已初始化的线程属性
    detachstate:    分离状态
        PTHREAD_CREATE_DETACHED(分离线程)
        PTHREAD _CREATE_JOINABLE(非分离线程)
返回值:
    成功:0
    失败:非0


这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。

要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。

设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

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

#include<pthread.h>

//线程处理函数
void *fun(void *arg){
    int i=0;
    for(int i=0;i<5;i++){
        printf("fun thread do working %d\n",i);
        sleep(1);
    }
    pthread_exit(NULL);
}

int main(){
    int ret = -1;
    pthread_t tid = -1;
    pthread_attr_t attr;

    //初始化线程属性
    ret = pthread_attr_init(&attr);
    if(0!=ret){
        printf("pthread_attr_init filed...\n");
        return 1;
    }
    printf("线程属性初始化函数ok....\n");

    //设置线程为分离状态
    ret = pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    if(0!=ret){
        printf("pthread_attr_setdetachstate filed...\n");
        return 1;
    }
    
    //创建一个线程 使用初始化好的属性
    ret = pthread_create(&tid,&attr,fun,NULL);
    if(0!=ret){
        printf("pthread_creat filed...\n");
        return 1;
    }
    printf("main thread runing....\n");


    //测试线程是否分离状态
    ret = pthread_join(tid,NULL);
    if(0!=ret){
        printf("pthread_join filed——分离状态...\n");
    }else{
        printf("pthread_join ok——非分离状态...\n");
    }


    //销毁线程属性
    ret = pthread_attr_destroy(&attr);
    if(0!=ret){
        printf("pthread_attr_destroy filed...\n");
        return 1;
    }
    printf("销毁线程属性 ok....\n");

    printf("按下任意键退出...\n");
    getchar();
    return 0;
}

3.4 线程栈地址

POSIX.1定义了两个常量来检测系统是否支持栈属性:

  • _POSIX_THREAD_ATTR_STACKADDR
  • _POSIX_THREAD_ATTR_STACKSIZE

也可以给sysconf函数传递来进行检测:

  • _SC_THREAD_ATTR_STACKADDR
  • _SC_THREAD_ATTR_STACKSIZE

当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack和pthread_attr_getstack两个函数分别设置和获取线程的栈地址。

#include <pthread.h>int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr,  size_t stacksize);
功能:设置线程的栈地址
参数:
    attr:指向一个线程属性的指针
    stackaddr:内存首地址
    stacksize:返回线程的堆栈大小
返回值:
    成功:0
    失败:错误号
​
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr,  size_t *stacksize);
功能:获取线程的栈地址
参数:
    attr:指向一个线程属性的指针
    stackaddr:返回获取的栈地址
    stacksize:返回获取的栈大小
返回值:
    成功:0
    失败:错误号

3.5 线程栈大小

当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。

#include <pthread.h>int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
功能:设置线程的栈大小
参数:
    attr:指向一个线程属性的指针
    stacksize:线程的堆栈大小
返回值:
    成功:0
    失败:错误号
​
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
功能:获取线程的栈大小
参数: 
    attr:指向一个线程属性的指针
    stacksize:返回线程的堆栈大小
返回值:
    成功:0
    失败:错误号

3.6 综合参考程序

#define SIZE 0x100000void *th_fun(void *arg)
{
    while (1)
    {
        sleep(1);
    }
}int main()
{
    pthread_t tid;
    int err, detachstate, i = 1;pthread_attr_t attr;
    size_t stacksize;
    void *stackaddr;pthread_attr_init(&attr);  //线程属性初始化
    pthread_attr_getstack(&attr, &stackaddr, &stacksize); //获取线程的栈地址
    pthread_attr_getdetachstate(&attr, &detachstate);           //获取线程分离状态if (detachstate == PTHREAD_CREATE_DETACHED)
    {
        printf("thread detached\n");
    }
    else if (detachstate == PTHREAD_CREATE_JOINABLE)
    {
        printf("thread join\n");
    }
    else
    {
        printf("thread unknown\n");
    }
        
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置分离状态while (1) 
    {
        stackaddr = malloc(SIZE);
        if (stackaddr == NULL) 
        {
            perror("malloc");
            exit(1);
        }
​
        stacksize = SIZE;
        pthread_attr_setstack(&attr, stackaddr, stacksize); //设置线程的栈地址
        err = pthread_create(&tid, &attr, th_fun, NULL); //创建线程
        if (err != 0) 
        {
            printf("%s\n", strerror(err));
            exit(1);
        }
        printf("%d\n", i++);
    }pthread_attr_destroy(&attr); //销毁线程属性所占用的资源函数return 0;
}

3.7 线程使用注意事项

  1. 主线程退出其他线程不退出,主线程应调用pthread_exit

  2. 避免僵尸线程

    a) pthread_join

    b) pthread_detach

    c) pthread_create指定分离属性

被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;

  1. malloc和mmap申请的内存可以被其他线程释放

  2. 应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程t在子进程中均pthread_exit

  3. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

04. 互斥锁

4.1 同步与互斥概述

现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:

  • 都需要访问/使用同一种资源
  • 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务

这两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。

互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

同步:是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。

显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要按照某种次序来运行相应的线程(也是一种互斥)!因此互斥有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步的任务之间则有顺序关系。

4.2 为什么需要互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。

下面我们用程序模拟一下这个过程,线程一需要打印“ hello ”,线程二需要打印“ world ”,不加任何处理的话,打印出来的内容会错乱:

测试程序:


// 打印机,公共资源
void printer(char *str)
{
    while (*str != '\0')
    {
        putchar(*str);
        fflush(stdout);
        str++;
        sleep(1);
    }
    printf("\n");
}// 线程一
void *thread_fun_1(void *arg)
{
    char *str = "hello";
    printer(str); //打印
}// 线程二
void *thread_fun_2(void *arg)
{
    char *str = "world";
    printer(str); //打印
}int main()
{
    pthread_t tid1, tid2;// 创建 2 个线程
    pthread_create(&tid1, NULL, thread_fun_1, NULL);
    pthread_create(&tid2, NULL, thread_fun_2, NULL);// 等待线程结束,回收其资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);return 0;
}

实际上,打印机是有做处理的,我在打印着的时候别人是不允许打印的,只有等我打印结束后别人才允许打印。这个过程有点类似于,把打印机放在一个房间里,给这个房间安把锁,这个锁默认是打开的。当 A 需要打印时,他先过来检查这把锁有没有锁着,没有的话就进去,同时上锁在房间里打印。而在这时,刚好 B 也需要打印,B 同样先检查锁,发现锁是锁住的,他就在门外等着。而当 A 打印结束后,他会开锁出来,这时候 B 才进去上锁打印。

4.3 互斥锁Mutex介绍

而在线程里也有这么一把锁:互斥锁(mutex),也叫互斥量,互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即加锁( lock )和解锁( unlock )。

互斥锁的操作流程如下:

1)在访问共享资源后临界区域前,对互斥锁进行加锁。

2)在访问完成后释放互斥锁导上的锁。

3)对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

互斥锁的数据类型是: pthread_mutex_t。

安装对应帮助手册:

sudo apt-get install manpages-posix-dev

3.4 pthread_mutex_init 函数

初始化互斥锁:

#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *restrict mutex,
    const pthread_mutexattr_t *restrict attr);
功能:
    初始化一个互斥锁。
参数:
    mutex:互斥锁地址。类型是 pthread_mutex_t 。
    attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL。
​
    可以使用宏 PTHREAD_MUTEX_INITIALIZER 静态初始化互斥锁,比如:
    pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;
​
这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_mutex_init() 来完成动态初始化,不同之处在于 PTHREAD_MUTEX_INITIALIZER 宏不进行错误检查。
​
返回值:
    成功:0,成功申请的锁默认是打开的。
    失败:非 0 错误码
restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,
对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。

4.5 pthread_mutex_destroy函数

#include <pthread.h>int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:
    销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非 0 错误码

4.6 pthread_mutex_lock函数

#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
    对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后再上锁。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非 0 错误码
​
int pthread_mutex_trylock(pthread_mutex_t *mutex);
   调用该函数时,若互斥锁未加锁,则上锁,返回 0;
   若互斥锁已加锁,则函数直接返回失败,即 EBUSY。

4.7 pthread_mutex_unlock函数

#include <pthread.h>int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:
    对指定的互斥锁解锁。
参数:
    mutex:互斥锁地址。
返回值:
    成功:0
    失败:非0错误码

测试代码


pthread_mutex_t mutex; //互斥锁// 打印机
void printer(char *str)
{
    pthread_mutex_lock(&mutex); //上锁
    while (*str != '\0')
    {
        putchar(*str);
        fflush(stdout);
        str++;
        sleep(1);
    }
    printf("\n");
    pthread_mutex_unlock(&mutex); //解锁
}// 线程一
void *thread_fun_1(void *arg)
{
    char *str = "hello";
    printer(str); //打印
}// 线程二
void *thread_fun_2(void *arg)
{
    char *str = "world";
    printer(str); //打印
}int main(void)
{
    pthread_t tid1, tid2;pthread_mutex_init(&mutex, NULL); //初始化互斥锁// 创建 2 个线程
    pthread_create(&tid1, NULL, thread_fun_1, NULL);
    pthread_create(&tid2, NULL, thread_fun_2, NULL);// 等待线程结束,回收其资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);pthread_mutex_destroy(&mutex); //销毁互斥锁return 0;
}

原子操作时需要加锁
在这里插入图片描述

4.9 死锁(DeadLock)

在这里插入图片描述

1)什么是死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

2)死锁引起的原因

竞争不可抢占资源引起死锁

也就是我们说的第一种情况,而这都在等待对方占有的不可抢占的资源。

竞争可消耗资源引起死锁(环形死锁)

有p1,p2,p3三个进程,p1向p2发送消息并接受p3发送的消息,p2向p3发送消息并接受p1的消息,p3向p1发送消息并接受p2的消息,如果设置是**先接到消息后发送消息**,则所有的消息都不能发送,这就造成死锁。

进程推进顺序不当引起死锁

有进程p1,p2,都需要资源A,B,本来可以p1运行A --> p1运行B --> p2运行A --> p2运行B,但是顺序换了,p1运行A时p2运行B,容易发生第一种死锁。互相抢占资源。

3)死锁的必要条件

互斥条件

某资源只能被一个进程使用,其他进程请求该资源时,只能等待,直到资源使用完毕后释放资源。

请求和保持条件

程序已经保持了至少一个资源,但是又提出了新要求,而这个资源被其他进程占用,自己占用资源却保持不放。

不可抢占条件

进程已获得的资源没有使用完,不能被抢占。

循环等待条件

必然存在一个循环链。

4)处理死锁的思路

预防死锁

破坏死锁的四个必要条件中的一个或多个来预防死锁。

避免死锁

和预防死锁的区别就是,在资源动态分配过程中,用某种方式防止系统进入不安全的状态。

检测死锁

运行时出现死锁,能及时发现死锁,把程序解脱出来

解除死锁

发生死锁后,解脱进程,通常撤销进程,回收资源,再分配给正处于阻塞状态的进程。

5)预防死锁的方法

破坏请求和保持条件

协议1:

所有进程开始前,必须一次性地申请所需的所有资源,这样运行期间就不会再提出资源要求,破坏了请求条件,即使有一种资源不能满足需求,也不会给它分配正在空闲的资源,这样它就没有资源,就破坏了保持条件,从而预防死锁的发生。

协议2:

允许一个进程只获得初期的资源就开始运行,然后再把运行完的资源释放出来。然后再请求新的资源。

破坏不可抢占条件

当一个已经保持了某种不可抢占资源的进程,**提出新资源请求不能被满足时,它必须释放已经保持的所有资源,**以后需要时再重新申请。

破坏循环等待条件

对系统中的所有资源类型进行线性排序,然后规定每个进程必须按序列号递增的顺序请求资源。假如进程请求到了一些序列号较高的资源,然后有请求一个序列较低的资源时,必须先释放相同和更高序号的资源后才能申请低序号的资源。多个同类资源必须一起请求。

死锁示例代码:

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

#include<pthread.h>

//线程互斥量
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;

// 线程一
void *thread_fun_1(void *arg)
{   
    //线程1先申请资源1 再申请资源2
    pthread_mutex_lock(&mutex1);
    printf("线程1加锁资源1ok。。。。\n");
    pthread_mutex_lock(&mutex2);
    printf("线程1加锁资源2ok。。。。\n");
    printf("线程1执行临界区代码。。。。\n");

    //解除互斥锁
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
}

// 线程二
void *thread_fun_2(void *arg)
{
    //线程1先申请资源2 再申请资源1
    pthread_mutex_lock(&mutex2);
    printf("线程2加锁资源2ok。。。。\n");
    pthread_mutex_lock(&mutex1);
    printf("线程2加锁资源1ok。。。。\n");
    
    printf("线程2执行临界区代码。。。。\n");

    //解除互斥锁
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
}

int main(void)
{
    pthread_t tid1, tid2;
    //初始化互斥锁
    pthread_mutex_init(&mutex1, NULL); 
    pthread_mutex_init(&mutex2, NULL); 

    // 创建 2 个线程
    pthread_create(&tid1, NULL, thread_fun_1, NULL);
    pthread_create(&tid2, NULL, thread_fun_2, NULL);


    // 等待线程结束,回收其资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    //销毁互斥锁
    pthread_mutex_destroy(&mutex1); 
    pthread_mutex_destroy(&mutex2); 

    return 0;
}

大概率会死锁
在这里插入图片描述

5. 读写锁

5.1 读写锁概述

当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。

在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。

读写锁的特点如下:

  • 1)如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。

  • 2)如果有其它线程写数据,则其它线程都不允许读、写操作。

读写锁分为读锁和写锁,规则如下:

  • 1)如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁。

  • 2)如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。

POSIX 定义的读写锁的数据类型是: pthread_rwlock_t

5.2 pthread_rwlock_init函数

#include <pthread.h>int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
    const pthread_rwlockattr_t *restrict attr);
功能:
    用来初始化 rwlock 所指向的读写锁。
​
参数:
    rwlock:指向要初始化的读写锁指针。
    attr:读写锁的属性指针。如果 attr 为 NULL 则会使用默认的属性初始化读写锁,否则使用指定的 attr 初始化读写锁。
​
    可以使用宏 PTHREAD_RWLOCK_INITIALIZER 静态初始化读写锁,比如:
    pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER;
​	这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_rwlock_init() 来完成动态初始化,不同之处在于PTHREAD_RWLOCK_INITIALIZER 宏不进行错误检查。
​
返回值:
    成功:0,读写锁的状态将成为已初始化和已解锁。
    失败:非 0 错误码。

5.3 pthread_rwlock_destroy函数

#include <pthread.h>int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:
    用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由 pthread_rwlock_init() 自动申请的资源) 。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码
​

5.4 pthread_rwlock_rdlock函数

#include <pthread.h>int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:
    以阻塞方式在读写锁上获取读锁(读锁定)。
    如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
    如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。
    线程可以成功调用 pthread_rwlock_rdlock() 函数 n 次,但是之后该线程必须调用 pthread_rwlock_unlock() 函数 n 次才能解除锁定。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
   用于尝试以非阻塞的方式来在读写锁上获取读锁。
   如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。

5.5 pthread_rwlock_wrlock函数

#include <pthread.h>int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:
    在读写锁上获取写锁(写锁定)。
    如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。
    如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
   用于尝试以非阻塞的方式来在读写锁上获取写锁。
   如果有任何的读者或写者持有该锁,则立即失败返回。

5.6 pthread_rwlock_unlock函数

#include <pthread.h>int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:
    无论是读锁或写锁,都可以通过此函数解锁。
参数:
    rwlock:读写锁指针。
返回值:
    成功:0
    失败:非 0 错误码

5.7 测试程序示例

下面是一个使用读写锁来实现 4 个线程读写一段数据是实例。

在此示例程序中,共创建了 4 个线程,其中两个线程用来写入数据,两个线程用来读取数据。当某个线程读操作时,其他线程允许读操作,却不允许写操作;当某个线程写操作时,其它线程都不允许读或写操作

5.7 测试程序示例

下面是一个使用读写锁来实现 8 个线程读写一段数据是实例。

在此示例程序中,共创建了 8个线程,其中3个线程用来写入数据,5个线程用来读取数据。当某个线程读操作时,其他线程允许读操作,却不允许写操作;当某个线程写操作时,其它线程都不允许读或写操作。
在这里插入图片描述

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

#include<pthread.h>

//读写锁变量
pthread_rwlock_t rwlock;

int num = 0;

//读线程
void *fun_read(void *arg){
    //获取线程的编号
    int index = (int)(long)arg;
    while(1){
        //加读锁
        pthread_rwlock_rdlock(&rwlock);
        printf("线程 %d  读取num的值 %d\n",index,num);
        //解锁 在睡眠之前解锁 防止永远在读
        pthread_rwlock_unlock(&rwlock);

        //随机睡眠1-3秒
        sleep(random()%3+1);
    }

    return NULL;
}

//写线程
void *fun_write(void *arg){
    //获取线程的编号
    int index = (int)(long)arg;
    while(1){
        //加写锁
        pthread_rwlock_rdlock(&rwlock);

        num++;
        printf("线程 %d  修改num的值 %d\n",index,num);

        //解锁
        pthread_rwlock_unlock(&rwlock);

        //随机睡眠1-3秒
        sleep(random()%3+1);
    }

    return NULL;
}

int main(){
    int i =0;
    int ret = -1;
    pthread_t tid[8];

    //设置随机种子
    srandom(getpid());

    //初始化读写锁
    ret = pthread_rwlock_init(&rwlock,NULL);
    if(0!=ret){
        printf("pthread_rwlock_init falied....\n");
        return 1;
    }

    //创建8个线程
    for(int i=0;i<0;i++){
        //创建读线程
        if(i<5){
            pthread_create(&tid[i],NULL,fun_read,(void *)(long)i);
        }else{
            // 创建写进程
            pthread_create(&tid[i],NULL,fun_write,(void *)(long)i);
        }
    }
    //回收八个线程的资源
    for(int i=0;i<8;i++){
        pthread_join(tid[i],NULL);
    }

    //销毁读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

06. 条件变量

6.1 条件变量概述

与互斥锁不同,条件变量是用来等待而不是用来上锁的,条件变量本身不是锁!

条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

条件变量的两个动作:

  • 条件不满, 阻塞线程
  • 当条件满足, 通知阻塞的线程开始工作

条件变量的类型: pthread_cond_t

6.2 pthread_cond_init函数

#include <pthread.h>int pthread_cond_init(pthread_cond_t *restrict cond,
    const pthread_condattr_t *restrict attr);
功能:
    初始化一个条件变量
参数:
    cond:指向要初始化的条件变量指针。
    attr:条件变量属性,通常为默认值,传NULL即可
        也可以使用静态初始化的方法,初始化条件变量:
        pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:
    成功:0
    失败:非0错误号

6.3 pthread_cond_destroy函数

#include <pthread.h>int pthread_cond_destroy(pthread_cond_t *cond);
功能:
    销毁一个条件变量
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号

6.4 pthread_cond_wait函数

#include <pthread.h>int pthread_cond_wait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex);
功能:
    阻塞等待一个条件变量
    a) 阻塞等待条件变量cond(参1)满足
    b) 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
            a) b) 两步为一个原子操作。
    c) 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
​
参数:
    cond:指向要初始化的条件变量指针
    mutex:互斥锁
​
返回值:
    成功:0
    失败:非0错误号
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex,
    const struct
                           .*restrict abstime);
功能:
    限时等待一个条件变量
​
参数:
    cond:指向要初始化的条件变量指针
    mutex:互斥锁
    abstime:绝对时间
​
返回值:
    成功:0
    失败:非0错误号
​
abstime补充说明:

struct timespec {
    time_t tv_sec;      /* seconds */ // 秒
    long   tv_nsec; /* nanosecondes*/ // 纳秒
}time_t cur = time(NULL);        //获取当前时间。
struct timespec t;              //定义timespec 结构体变量t
t.tv_sec = cur + 1;             // 定时1秒
pthread_cond_timedwait(&cond, &t);

6.5 pthread_cond_signal函数

唤醒至阻塞在条件变量上的线程

#include <pthread.h>int pthread_cond_signal(pthread_cond_t *cond);
功能:
    唤醒至少一个阻塞在条件变量上的线程
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号
​
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:
    唤醒全部阻塞在条件变量上的线程
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号
​

在这里插入图片描述

6.6 生产者消费者条件变量模型

线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。

假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。

在这里插入图片描述

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

#include <pthread.h>

// 条件变量
pthread_cond_t cond;
// 互斥变量
pthread_mutex_t mutex;

// 链表节点
typedef struct _node_t
{
    int data;
    struct node_t *next;
} node_t;

node_t *head = NULL;

// 生产者进程
void *producer(void *arg)
{
    // 循环生产产品
    while (1)
    {
        // 加锁
        pthread_mutex_lock(&mutex);

        // 分配节点空间
        node_t *new = malloc(sizeof(node_t));
        if (NULL == new)
        {
            printf("malloc falied...\n");
            break;
        }
        memset(new, 0, sizeof(node_t));

        new->data = random() % 100 + 1;
        new->next = NULL;
        printf("生产者生产产品%d\n", new->data);

        // 头插法 让head指针始终指向链表头部
        new->next = head;
        head = new;

        // 解锁
        pthread_mutex_unlock(&mutex);
        // 唤醒因为条件变量而阻塞的线程
        pthread_cond_signal(&cond);

        sleep(rand() % 3 + 1);
    }
    pthread_exit(NULL);
}

// 消费者进程
void *customer(void *arg)
{
    node_t *tmp = NULL;
    // 循环生产产品
    while (1)
    {
        // 加锁
        pthread_mutex_lock(&mutex);

        if (NULL == head)
        {
            // 等待
            printf("产品链表为空。。。\n");
            // 如果链表为空 就阻塞
            pthread_cond_wait(&cond, &mutex);
        }
        // 删除第一个节点
        tmp = head;
        head = head->next;

        printf("消费者消费产品%d\n", tmp->data);
        free(tmp);
        // 解锁
        pthread_mutex_unlock(&mutex);
        sleep(rand() % 3 + 1);
    }
    pthread_exit(NULL);
}

// 生产者和消费者模型 条件变量的模型
int main()
{
    pthread_t tid1 = -1, tid2 = -1;
    int ret = -1;
    // 设置随机种子
    srandom(getpid());

    // 初始化条件变量
    ret = pthread_cond_init(&cond, NULL);
    if (0 != ret)
    {
        printf("pthread_cond_init failed....\n");
        return 1;
    }

    // 初始化互斥量
    ret = pthread_mutex_init(&mutex, NULL);
    if (0 != ret)
    {
        printf("pthread_mutex_init failed....\n");
        return 1;
    }

    // 创建两个线程 生产者线程 消费者线程
    pthread_create(&tid1, NULL, producer, NULL);

    // 创建消费者进程
    pthread_create(&tid2, NULL, customer, NULL);

    // 等待两个线程结束
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    // 销毁
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    return 0;
}

在这里插入图片描述

07. 信号量

7.1 信号量概述

信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。

编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。

PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。

信号量主要用于进程或线程间的同步和互斥这两种典型情况。

信号量数据类型为:sem_t。

信号量用于互斥:
在这里插入图片描述
信号量用于同步:
在这里插入图片描述

7.2 sem_init函数

初始化信号量:

#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:
    创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。
参数:
    sem:信号量的地址。
    pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。
    value:信号量的初始值。
返回值:
    成功:0
    失败: - 1

7.3 sem_destroy函数

销毁信号量:

#include <semaphore.h>int sem_destroy(sem_t *sem);
功能:
    删除 sem 标识的信号量。
参数:
    sem:信号量地址。
返回值:
    成功:0
    失败: - 1

7.4 信号量P操作(减1)

#include <semaphore.h>int sem_wait(sem_t *sem);
功能:
    将信号量的值减 1。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作。
参数:
    sem:信号量的地址。
返回值:
    成功:0
    失败: - 1int sem_trywait(sem_t *sem);
   以非阻塞的方式来对信号量进行减 1 操作。
   若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。
​
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
   限时尝试将信号量的值减 1
   abs_timeout:绝对时间

abs_timeout补充说明:

struct timespec {
    time_t tv_sec;      /* seconds */ // 秒
    long   tv_nsec; /* nanosecondes*/ // 纳秒
}time_t cur = time(NULL);        //获取当前时间。
struct timespec t;              //定义timespec 结构体变量t
t.tv_sec = cur + 1;             // 定时1秒
sem_timedwait(&cond, &t);

7.5 信号量V操作(加1)

#include <semaphore.h>int sem_post(sem_t *sem);
功能:
    将信号量的值加 1 并发出信号唤醒等待线程(sem_wait())。
参数:
    sem:信号量的地址。
返回值:
    成功:0
    失败:-1

7.6 获取信号量的值

#include <semaphore.h>int sem_getvalue(sem_t *sem, int *sval);
功能:
    获取 sem 标识的信号量的值,保存在 sval 中。
参数:
    sem:信号量地址。
    sval:保存信号量值的地址。
返回值:
    成功:0
    失败:-1

7.7使用信号量实现打印机(信号量用于互斥)

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

#include <pthread.h>

//信号量变量
 sem_t sem;

// 打印机,公共资源
void printer(char *str)
{
    while (*str != '\0')
    {
        putchar(*str);
        fflush(stdout);
        str++;
        sleep(1);
    }
    printf("\n");
}

// 线程一
void *thread_fun_1(void *arg)
{
    //申请资源 将可用资源-1
    sem_wait(&sem);

    char *str = "hello";
    printer(str); //打印

    //释放资源 将可用资源+1
    sem_post(&sem);
}

// 线程二
void *thread_fun_2(void *arg)
{
    //申请资源 将可用资源-1
    sem_wait(&sem);

    char *str = "world";
    printer(str); //打印

    //释放资源 将可用资源+1
    sem_post(&sem);
}

int main()
{
    pthread_t tid1, tid2;
    int ret = -1;

    // 初始化信号量
    ret = sem_init(&sem, 0,1);
    if (0 != ret)
    {
        printf("sem_init failed....\n");
        return 1;
    }
    printf("初始化信号量ok....\n");

    // 创建 2 个线程
    pthread_create(&tid1, NULL, thread_fun_1, NULL);
    pthread_create(&tid2, NULL, thread_fun_2, NULL);

    // 等待线程结束,回收其资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    //销毁信号量
    sem_destroy(&sem);
    printf("main exit....\n");
    return 0;
}

在这里插入图片描述

7.8信号量实现生产者和消费者(信号量用于同步)

在这里插入图片描述

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

#include <pthread.h>
#include <semaphore.h>

// 信号量变量 1为生产者可以再生产商品容器的个数 2为消费者可以买的个数
sem_t sem1, sem2;

// 链表节点
typedef struct _node_t
{
    int data;
    struct node_t *next;
} node_t;

node_t *head = NULL;

// 生产者进程
void *producer(void *arg)
{
    // 循环生产产品
    while (1)
    {
        // 申请一个容器资源
        sem_wait(&sem1);
        // 分配节点空间
        node_t *new = malloc(sizeof(node_t));
        if (NULL == new)
        {
            printf("malloc falied...\n");
            break;
        }
        memset(new, 0, sizeof(node_t));

        new->data = random() % 100 + 1;
        new->next = NULL;
        printf("生产者生产产品%d\n", new->data);

        // 头插法 让head指针始终指向链表头部
        new->next = head;
        head = new;

        // 通知消费者消费 将可以卖的商品+1
        sem_post(&sem2);
        sleep(rand() % 3 + 1);
    }
    pthread_exit(NULL);
}

// 消费者进程
void *customer(void *arg)
{
    node_t *tmp = NULL;
    // 循环生产产品
    while (1)
    {
        // 消费一个商品 商品可以卖的个数减1
        sem_wait(&sem2);

        if (NULL == head)
        {
            // 等待
            printf("产品链表为空。。。\n");
        }
        // 删除第一个节点
        tmp = head;
        head = head->next;

        printf("消费者消费产品%d\n", tmp->data);
        free(tmp);
        // 释放资源 容器个数+1
        sem_post(&sem1);
        sleep(rand() % 3 + 1);
    }
    pthread_exit(NULL);
}

// 生产者和消费者模型 条件变量的模型
int main()
{
    int i = 0;
    pthread_t tid[6];
    int ret = -1;
    // 设置随机种子
    srandom(getpid());

    // 初始化
    ret = sem_init(&sem1, 0, 8);
    if (0 != ret)
    {
        printf("sem_init failed....\n");
        return 1;
    }
    ret = sem_init(&sem2, 0, 0);
    if (0 != ret)
    {
        printf("sem_init failed....\n");
        return 1;
    }
    //创建2个生产者 4个消费者
    for(int i=0;i<6;i++){
        // 创建生产者线程
        if(i<2){
            pthread_create(&tid[i], NULL, producer, NULL);
        }else{
            // 创建消费者进程
            pthread_create(&tid[i], NULL, customer, NULL);
        }
    }
    for(int i=0;i<6;i++){
        pthread_join(tid[i], NULL);
    }

    // 销毁
    sem_destroy(&sem1);
    sem_destroy(&sem2);
    return 0;
}

在这里插入图片描述

08. 哲学家进餐问题

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>pthread_mutex_t mutex[5];void* dine(void* arg)
{
    int num = (int)arg;
    int left, right;if(num < 4)
    {
        // 前4个人,右手拿自己筷子
        right = num;
        left = num+1;
    }
    else if(num == 4)
    {
        // 最后一个人,右手拿别人筷子
        right = 0;
        left = num;
    }// 吃饭
    while(1)
    {
        // 右手加锁
        pthread_mutex_lock(&mutex[right]);
        // 尝试抢左手筷子
        if(pthread_mutex_trylock(&mutex[left]) == 0)
        {
            // 吃面。。。
            printf("%c 正在吃面。。。。。。\n", num+'A');
            // 吃完放筷子
            pthread_mutex_unlock(&mutex[left]);
        }
        // 解锁
        pthread_mutex_unlock(&mutex[right]);
        sleep(rand()%5);
    }
}int main(int argc, const char* argv[])
{
    pthread_t p[5];for(int i=0; i<5; ++i)
    {
        pthread_mutex_init(&mutex[i], NULL);
    }for(int i=0; i<5; ++i)
    {
        pthread_create(&p[i], NULL, dine, (void*)i);
    }for(int i=0; i<5; ++i)
    {
        pthread_join(p[i], NULL);
    }
    
    for(int i=0; i<5; ++i)
    {
        pthread_mutex_destroy(&mutex[i]);
    }
    
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值