linux系统编程(7)--线程

这篇具有很好参考价值的文章主要介绍了linux系统编程(7)--线程。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1.线程

1.1 Linux下的线程概念

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

所以,线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。为了让进程完成一定的工作,进程必须至少包含一个线程。

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

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

进程有自己的地址空间,线程使用进程的地址空间,进程里的资源,线程都是有权访问的。

sudo apt-get install manpages-posix-dev

manpages-posix-dev 包含 POSIX 的 header files 和 library calls 的用法

1.2 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.3 线程的特点

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

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

  1. 线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
  2. 从内核里看进程和线程是一样的,都有各自不同的PCB
  3. 进程可以蜕变成线程
  4. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位

查看指定进程的LWP号:ps -Lf pid

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

  • 如果复制对方的地址空间,就产生一个“进程”;
  • 如果共享对方的地址空间,就产生一个“线程”。

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

1.4 线程所共享的资源

  1. 文件描述符表
  2. 每种信号的处理方式
  3. 当前工作目录
  4. 用户ID和组ID
  5. 内存地址空间 (.text/.data/.bss/heap/共享库)

1.5 线程不共享的资源

  1. 线程id
  2. 处理器现场和栈指针(内核栈)
  3. 独立的栈空间(用户空间栈)
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级

1.6 线程的优缺点

优点:

  • 提高程序并发性
  • 开销小
  • 数据通信、共享数据方便

缺点:

  • 库函数,不稳定
  • 调试、编写困难、gdb不支持
  • 对信号支持不好

2.线程常用操作

2.1 pthread_self

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

进程号用 pid_t 数据类型表示,是一个非负整数。线程号则用 pthread_t 数据类型来表示,Linux 使用无符号长整数表示。有的系统在实现pthread_t 的时候,用一个结构体来表示,所以在可移植的操作系统实现不能把它做为整数处理。

#include <pthread.h>

pthread_t pthread_self(void);
功能:
    获取线程号。
参数:
    无
返回值:
    调用线程的线程ID。

2.2 pthread_equal

#include <pthread.h>

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

示例:

#include <stdio.h>

#include <pthread.h>
int main()
{
    pthread_t tid;
    tid = pthread_self();
    printf("tid:%lu\n", tid);
    
    if(pthread_equal(tid, pthread_self()))
    {
        printf("相同\n");
    }
    else
    {
        printf("不同\n");
    }
    
    return 0;
}

注意:线程函数的程序在 pthread 库中,故链接时要加上参数 -pthread。

2.3 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()把错误码转换成错误信息再打印。

示例:不带参数的线程

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

void *fun(void *arg)
{
    printf("新线程id:%lu\n", pthread_self());
    return NULL;
}
int main()
{
    int ret = -1;
    pthread_t tid = -1;
    
    ret = pthread_create(&tid, NULL, fun, NULL);
    if(0 != ret)
    {
        printf("pthread_create error\n");
        printf("error information: %s\n", strerror(ret));
        return 1;
    }
    printf("主线程id:%lu\n", pthread_self());
    
    printf("按下任意键主线程退出\n");
    getchar();
    
    return 0;
}

示例:带参数的线程

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

void *fun(void *arg)
{
    int var = (int)(long)arg;
    printf("新线程id:%lu\n", pthread_self());
    printf("新线程参数 var = %d\n", var);
    return NULL;
}
int main()
{
    int ret = -1;
    pthread_t tid = -1;
    
    ret = pthread_create(&tid, NULL, fun, (void*)0x3);
    if(0 != ret)
    {
        printf("pthread_create error\n");
        printf("error information: %s\n", strerror(ret));
        return 1;
    }
    printf("主线程id:%lu\n", pthread_self());
    
    printf("按下任意键主线程退出\n");
    getchar();
    
    return 0;
}

2.4 pthread_join

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
功能:
    等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。如果线程已经结束,那么该函数会立即返回。
参数:
    thread:被等待的线程号。
    retval:用来存储线程退出状态的指针的地址。
返回值:
    成功:0
    失败:非0

示例:

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

void *fun(void *arg)
{
    int i = 0;
    for(i = 0;i < 5;i++)
    {
        printf("新线程工作第 %d 秒\n", i);
        sleep(1);
    }
    return (void *)0x3;
}

int main()
{
    int ret = -1;
    pthread_t tid = -1;
    void *value = NULL;
    
    ret = pthread_create(&tid, NULL, fun, NULL);
    if(0 != ret)
    {
        printf("pthread_create error\n");
        return 1;
    }
    printf("主线程运行中\n");
    
    //等待新线程结束,会发生阻塞
    ret = pthread_join(tid, &value);
    if(0 != ret)
    {
        printf("pthread_join error\n");
        return 1;
    }
    printf("value: %p\n", value);
    printf("主线程退出\n");
      
    return 0;
}

注意:thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的:

  1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。

2.5 pthread_detach

#include <pthread.h>

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

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

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

示例:

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

void *fun(void *arg)
{
    int i = 0;
    for(i = 0;i < 5;i++)
    {
        printf("新线程工作第 %d 秒\n", i);
        sleep(1);
    }
    return NULL;
}

int main()
{
    int ret = -1;
    pthread_t tid = -1;
    
    ret = pthread_create(&tid, NULL, fun, NULL);
    if(0 != ret)
    {
        printf("pthread_create error\n");
        return 1;
    }
    printf("主线程id:%lu\n", pthread_self());
    
    //设置线程分离
    ret = pthread_detach(tid);
    if(0 != ret)
    {
        printf("pthread_detach error\n");
        return 1;
    }
    
    //测试分离的线程是否可以被join
    ret = pthread_join(tid, NULL);
    if(0 != ret)
    {
        printf("pthread_join error\n");
    }
    else
    {
        printf("pthread_join success\n");
    }
    
    printf("按下任意键退出\n");
    getchar();
    
    return 0;
}

2.6 pthread_exit

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

  • 线程从执行函数中返回。
  • 线程调用pthread_exit退出线程。
  • 线程可以被同一进程中的其它线程取消。
#include <pthread.h>

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

示例:

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

void *thread(void *arg)
{
    static int num = 123; //静态变量
    int i = 0;
    while (1)
    {
        printf("I am runing\n");
        sleep(1);
        i++;
        if (i == 3)
        {
            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.7 pthread_cancel

#include <pthread.h>

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

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

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

示例:

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

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);
    pthread_cancel(tid); //取消tid线程

    pthread_join(tid, NULL);

    return 0;
}

※ pthread_kill(待完善)

线程对信号的支持较差,暂时不写。

⭐案例

创建两个子线程, 第一个子线程输出所有的大写字母(A-Z),第二个子线程输出所有小写字母(a-z),每输出一个字母都要睡眠100ms。

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

void *fun1(void *arg)
{
    int i = 0;
    for(i = 'A'; i <= 'Z'; i++)
    {
        putchar(i);
        fflush(stdout);
        usleep(10000);
    }
    return NULL;
}

void *fun2(void *arg)
{
    int i = 0;
    for(i = 'a'; i <= 'z'; i++)
    {
        putchar(i);
        fflush(stdout);
        usleep(10000);
    }
    return NULL;
}

int main()
{
    pthread_t tid1,tid2;
    pthread_create(&tid1, NULL, fun1, NULL);
    pthread_create(&tid2, NULL, fun2, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    printf("\n");
    printf("主线程退出\n");
    
    return 0;
}

3.线程属性设置

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;

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

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

3.2 pthread_attr_init

#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);
功能:
    初始化线程属性函数,注意:应先初始化线程属性,再pthread_create创建线程
参数:
    attr:线程属性结构体
返回值:
    成功:0
    失败:错误号

3.3 pthread_attr_destroy

#include <pthread.h>

int pthread_attr_destroy(pthread_attr_t *attr);
功能:
    销毁线程属性所占用的资源函数
参数:
    attr:线程属性结构体
返回值:
    成功:0
    失败:错误号

3.4 pthread_attr_setdetachstate

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

  • 非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当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

问题:

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

解决:

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

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

3.5 pthread_attr_getdetachstate

#include <pthread.h>

int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
功能:获取线程分离状态
参数:
    attr:已初始化的线程属性
    detachstate:    分离状态
        PTHREAD_CREATE_DETACHED(分离线程)
        PTHREAD _CREATE_JOINABLE(非分离线程)
返回值:
    成功:0
    失败:非0

3.6 pthread_attr_setstack

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
    失败:错误号

3.7 pthread_attr_getstack

#include <pthread.h>

int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr,  size_t *stacksize);
功能:获取线程的栈地址
参数:
    attr:指向一个线程属性的指针
    stackaddr:返回获取的栈地址
    stacksize:返回获取的栈大小
返回值:
    成功:0
    失败:错误号

3.8 pthread_attr_setstacksize

当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用。

当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。

#include <pthread.h>

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
功能:设置线程的栈大小
参数:
    attr:指向一个线程属性的指针
    stacksize:线程的堆栈大小
返回值:
    成功:0
    失败:错误号

3.9 pthread_attr_getstacksize

#include <pthread.h>

int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
功能:获取线程的栈大小
参数: 
    attr:指向一个线程属性的指针
    stacksize:返回线程的堆栈大小
返回值:
    成功:0
    失败:错误号

3.10 pthread_attr_setguardsize

设置栈溢出保护区大小

3.11 pthread_attr_getguardsize

获取栈溢出保护区大小

3.12 pthread_attr_setstackaddr

3.13 pthread_attr_getstackaddr

3.14 注意

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

  2. 避免僵尸线程

    • pthread_join
    • pthread_detach
    • pthread_create指定分离属性

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

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

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

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

⭐案例

#define SIZE 0x100000

void *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;
}

4.互斥锁

4.1 同步和互斥的概念

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

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

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

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

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

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

4.2 互斥锁

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

互斥锁的操作流程如下:

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

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

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

互斥锁的数据类型是:pthread_mutex_t

4.3 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语言中的一种类型限定符,用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。

4.4 pthread_mutex_destroy

#include <pthread.h>

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

4.5 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.6 pthread_mutex_unlock

#include <pthread.h>

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

⭐案例

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

#include <pthread.h>

pthread_mutex_t mutex;				//互斥锁

void *thread_fun_1(void *arg)
{
    int i = 0;
    pthread_mutex_lock(&mutex);
    for(i = 'A'; i <= 'Z'; i++)
    {
        putchar(i);
        fflush(stdout);
        usleep(10000);
    }
    pthread_mutex_unlock(&mutex);
    
    return NULL;
}
void *thread_fun_2(void *arg)
{
    int i = 0;
    pthread_mutex_lock(&mutex);
    for(i = 'a'; i <= 'z'; i++)
    {
        putchar(i);
        fflush(stdout);
        usleep(10000);
    }
    pthread_mutex_unlock(&mutex);
    
    return NULL;
}

int main()
{
    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);			//销毁互斥锁
    
    printf("\n");
    return 0;
}

4.7 死锁

4.7.1 什么是死锁?

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

死锁产生的原因

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

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

  • 竞争可消耗资源引起死锁

有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,容易发生第一种死锁。互相抢占资源。

4.7.2 死锁产生的条件

虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件:

  1. **互斥条件:**指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
  2. **请求和保持条件:**指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  3. **不剥夺条件:**指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  4. **循环等待条件:**指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

4.7.3 死锁的处理方式

4.7.3.1 预防死锁
  • 打破互斥条件

    把只能互斥使用的资源改造为允许共享使用,但大部分资源已无法改造。

  • 打破请求和保持条件

    采用静态分配方法,即进程在运行前一次申请完它所需的全部资源,在资源未满足前不让其投入运行。

    当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。

  • 打破不剥夺条件

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

    当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。(剥夺调度方式)

  • 打破循环等待条件

    采用顺序资源分配法,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。

4.7.3.2 避免死锁

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

  • 安全序列
  • 银行家算法
4.7.3.3 检测死锁

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

4.7.3.4 解除死锁

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

  • 资源剥夺法
  • 撤销进程法
  • 进程回退法

4.7.4 死锁代码示例

#include <stdio.h>
#include <unistd.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成功申请资源1\n");
    pthread_mutex_lock(&mutex2);
    printf("线程1成功申请资源2\n");
    
    printf("线程1执行临界区代码\n");
    
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    
    return NULL;
}
void *thread_fun_2(void *arg)
{
    //线程2先申请资源2,再申请资源1
    pthread_mutex_lock(&mutex2);
    printf("线程2成功申请资源2\n");
    pthread_mutex_lock(&mutex1);
    printf("线程2成功申请资源1\n");
    
    printf("线程2执行临界区代码\n");
    
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    
    return NULL;
}

int main()
{
    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 错误码

⭐ 案例

#include <stdio.h>
#include <string.h>
#include <stdlib.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);
        
        sleep(random() % 3 + 1);
    }
    return NULL;
}
//写线程
void *fun_write(void *arg)
{
    int index = (int)(long)arg;
    while(1)
    {
        //加读写锁写锁
        pthread_rwlock_wrlock(&rwlock);
        num++;
        printf("线程%d 修改num的值 %d\n", index, num);
        //解锁
        pthread_rwlock_unlock(&rwlock);
        
        sleep(random() % 3 + 1);
    }
    return NULL;
}

int main()
{
    srandom(getpid());
    int ret = -1;
    int i = 0;
    pthread_t tid[8];
    
    ret = pthread_rwlock_init(&rwlock, NULL);
    if(0 != ret)
    {
        printf("pthread_rwlock_init error\n");
        return 1;
    }
    
    //创建8个线程
    for(i = 0; i < 8; 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(i = 0; i < 8; i++)
    {
        pthread_join(tid[i], NULL);
    }
    
    //销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    
    return 0;
}

6.条件变量

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错误号

6.5 pthread_cond_timedwait

#include <pthread.h>

int pthread_cond_timedwait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex,
    const struct *restrict abstime);
功能:
    限时等待一个条件变量
参数:
    cond:指向要初始化的条件变量指针
    mutex:互斥锁
    abstime:绝对时间
返回值:
    成功:0
    失败:非0错误号

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.6 pthread_cond_signal

#include <pthread.h>

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

6.7 pthread_cond_broadcast

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
功能:
    唤醒全部阻塞在条件变量上的线程
参数:
    cond:指向要初始化的条件变量指针
返回值:
    成功:0
    失败:非0错误号

⭐ 案例

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

int flag = 0;
pthread_mutex_t mutex;
pthread_cond_t cond;

//改变条件的线程
void *fun1(void *arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        flag = 1;
        pthread_mutex_unlock(&mutex);
        
        //唤醒因为条件而阻塞的进程
        pthread_cond_signal(&cond);
        
        sleep(2);
    }
    return NULL;
}
//等待条件的线程
void *fun2(void *arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        
        if(0 == flag)
        {
            //等待条件满足 会阻塞
            pthread_cond_wait(&cond, &mutex);
        }
        printf("线程2条件满足 开始运行\n");
        
        flag = 0;
        
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main()
{
    int ret = -1;
    pthread_t tid1, tid2;
    
    //初始化条件变量
    ret = pthread_cond_init(&cond, NULL);
    if(0 != ret)
    {
        printf("pthread_cond_init error\n");
        return 1;
    }
    //初始化互斥锁
    ret = pthread_mutex_init(&mutex, NULL);
    if(0 != ret)
    {
        printf("pthread_mutex_init error\n");
        return 1;
    }
    
    pthread_create(&tid1, NULL, fun1, NULL);
    pthread_create(&tid2, NULL, fun2, NULL);
    
    ret = pthread_join(tid1, NULL);
    if(ret != 0)
    {
        printf("pthread_join error\n");
        return 1;
    }
    ret = pthread_join(tid2, NULL);
    if(ret != 0)
    {
        printf("pthread_join error\n");
        return 1;
    }
    
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    
    return 0;
}

6.8 生产者消费者基本模型

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

typedef struct _node_t
{
    int data;
    struct _node_t *next;
}node_t;

node_t *head = NULL;

//生产者线程
void *producer(void *arg)
{
    //循环生产产品
    while(1)
    {
        node_t *new =malloc(sizeof(node_t));
        if(NULL == new)
        {
            printf("malloc error\n");
            break;
        }
        memset(new, 0, sizeof(node_t));
        
        new->data = random() % 100 + 1;
        new->next = NULL;
        
        //头插
        new->next = head;
        head = new;
        printf("生产者生产产品%d\n", new->data);
        //随机睡眠
        sleep(random() % 3 + 1);
    }
    pthread_exit(NULL);
}
//消费者线程
void *customer(void *arg)
{
    node_t *tmp = NULL;
    while(1)
    {
        if(NULL == head)
        {
            printf("产品链表为空,请等待2秒\n");
            sleep(2);
        }
        //删除第一个结点(消费者消费产品)
        tmp = head;
        head = head->next;
        
        printf("消费者消费产品%d\n", tmp->data);
        free(tmp);
    }
    pthread_exit(NULL);
}
int main()
{
    pthread_t tid1, tid2;
    
    pthread_create(&tid1, NULL, producer, NULL);
    pthread_create(&tid2, NULL, customer, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    return 0;
}

此时会发生“某产品消费在前,生产在后的情况”。

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

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

typedef struct _node_t
{
    int data;
    struct _node_t *next;
}node_t;

node_t *head = NULL;
pthread_mutex_t mutex;
pthread_cond_t cond;

//生产者线程
void *producer(void *arg)
{
    //循环生产产品
    while(1)
    {
        pthread_mutex_lock(&mutex);					//-----------加锁-------------
        node_t *new =malloc(sizeof(node_t));
        if(NULL == new)
        {
            printf("malloc error\n");
            break;
        }
        memset(new, 0, sizeof(node_t));
        
        new->data = random() % 100 + 1;
        new->next = NULL;
        
        //头插
        new->next = head;
        head = new;
        printf("生产者生产产品%d\n", new->data);
        
        pthread_mutex_unlock(&mutex);				//-----------解锁------------
        
        //唤醒因为条件变量而阻塞的线程
        pthread_cond_signal(&cond);
        
        //随机睡眠
        sleep(random() % 3 + 1);
    }
    pthread_exit(NULL);
}
//消费者线程
void *customer(void *arg)
{
    node_t *tmp = NULL;
    while(1)
    {
        pthread_mutex_lock(&mutex);					//-----------加锁-------------
        if(NULL == head)
        {
            //如果链表为空 就阻塞
            pthread_cond_wait(&cond, &mutex);
        }
        //删除第一个结点(消费者消费产品)
        tmp = head;
        head = head->next;
        
        printf("消费者消费产品%d\n", tmp->data);
        free(tmp);
        
        pthread_mutex_unlock(&mutex);				//-----------解锁------------
    }
    pthread_exit(NULL);
}
int main()
{
    int ret = -1;
    pthread_t tid1, tid2;
    srandom(getpid());
    
    //初始化条件变量和互斥量
    ret = pthread_mutex_init(&mutex, NULL);
    if(0 != ret)
    {
        printf("pthread_mutex_init error\n");
        return 1;
    }
    ret = pthread_cond_init(&cond, NULL);
    if(0 != ret)
    {
        printf("pthread_cond_init error\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;
}

6.10 条件变量的优缺点

相较于mutex而言,条件变量可以减少竞争。

如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。

有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

7.信号量

7.1 概述

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

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

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

信号量数据类型为:sem_t文章来源地址https://www.toymoban.com/news/detail-403493.html

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
    失败: - 1

int sem_trywait(sem_t *sem);
以非阻塞的方式来对信号量进行减 1 操作。
若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
限时尝试将信号量的值减 1
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 sem_getvalue

#include <semaphore.h>

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

7.7 信号量用于互斥场景

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

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

sem_t sem;

void *thread_fun_1(void *arg)
{
    int i = 0;
    //P操作
    sem_wait(&sem);
    for(i = 'A'; i <= 'Z'; i++)
    {
        putchar(i);
        fflush(stdout);
        usleep(10000);
    }
    //V操作
    sem_post(&sem);
    
    return NULL;
}
void *thread_fun_2(void *arg)
{
    int i = 0;
    //P操作
    sem_wait(&sem);
    for(i = 'a'; i <= 'z'; i++)
    {
        putchar(i);
        fflush(stdout);
        usleep(10000);
    }
    //V操作
    sem_post(&sem);
    
    return NULL;
}

int main()
{
    pthread_t tid1, tid2;
	
    sem_init(&sem, 0, 1);		//初始化信号量

    //创建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("\n");
    return 0;
}

7.8 生产者消费者信号量模型

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

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

typedef struct _node_t
{
    int data;
    struct _node_t *next;
}node_t;

node_t *head = NULL;

sem_t sem_producer;
sem_t sem_customer;

//生产者线程
void *producer(void *arg)
{
    //循环生产产品
    while(1)
    {
        sem_wait(&sem_producer);
        
        node_t *new =malloc(sizeof(node_t));
        if(NULL == new)
        {
            printf("malloc error\n");
            break;
        }
        memset(new, 0, sizeof(node_t));
        
        new->data = random() % 100 + 1;
        new->next = NULL;
        
        //头插
        new->next = head;
        head = new;
        printf("生产者生产产品%d\n", new->data);
        
        sem_post(&sem_customer);
        
        //随机睡眠
        sleep(random() % 3 + 1);
    }
    pthread_exit(NULL);
}
//消费者线程
void *customer(void *arg)
{
    node_t *tmp = NULL;
    while(1)
    {
        sem_wait(&sem_customer);
        
        if(NULL == head)
        {
            printf("产品链表为空\n");
        }
        //删除第一个结点(消费者消费产品)
        tmp = head;
        head = head->next;
        
        printf("消费者消费产品%d\n", tmp->data);
        free(tmp);
        
        sem_post(&sem_producer);
    }
    pthread_exit(NULL);
}
int main()
{
    int i = 0;
    pthread_t tid[6];
    srandom(getpid());
    
    sem_init(&sem_producer, 0, 8);		//最多生产4个商品
    sem_init(&sem_customer, 0, 0);		//初始化现有产品为0个
    
    
    for(i = 0; i < 6; i++)
    {
        if(i < 2)
        {
            pthread_create(&tid[i], NULL, producer, NULL);
        }
        else
        {
            pthread_create(&tid[i], NULL, customer, NULL);
        }
    }
    
    for(i = 0; i < 6; i++)
    {
        pthread_join(tid[i], NULL);
    }
    
    //销毁
	sem_destroy(&sem_producer);
    sem_destroy(&sem_customer);
    
    return 0;
}

到了这里,关于linux系统编程(7)--线程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 《Linux操作系统编程》 第十章 线程与线程控制: 线程的创建、终止和取消,detach以及线程属性

    🌷🍁 博主 libin9iOak带您 Go to New World.✨🍁 🦄 个人主页——libin9iOak的博客🎐 🐳 《面试题大全》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 🌊 《IDEA开发秘籍》学会IDEA常用操作,工作效率翻倍~💐 🪁🍁 希望本文能够给您带来一定的帮助🌸文章粗浅,敬

    2024年02月11日
    浏览(90)
  • 【Linux】Linux线程概念和线程控制

    线程是进程内的一个执行流。 我们知道,一个进程会有对应的PCB,虚拟地址空间,页表以及映射的物理内存。所以我们把这一个整体看做一个进程,即进程=内核数据结构+进程对应的代码和数据。我们可以这样看待虚存:虚拟内存决定了进程能够看到的\\\"资源\\\"。因为每一个进

    2024年02月04日
    浏览(44)
  • Linux之多线程(上)——Linux下的线程概念

    本文介绍了地址空间和二级页表、Linux下的线程、线程的优缺点以及线程与进程的关系等概念。 地址空间是进程能看到的资源窗口 :一个进程可以看到代码区、堆栈区、共享区、内核区等,大部分的资源是在地址空间上看到的。 页表决定进程真正有用资源的情况 :进程认为

    2024年02月09日
    浏览(45)
  • 【Linux】线程-线程概念

    实际上,线程是一个进程内部的控制序列,一个程序的一个执行线路就是一个线程。 并且一个进程中至少有一个线程,本质上,一个进程内部如果有多个线程,那么这些线程实际上是指向同一块地址空间的。而不论进程还是线程,从CPU看来都是一个PCB,只是说线程的PCB要比进

    2023年04月25日
    浏览(40)
  • Linux 多线程 ( 多线程概念 )

    在一个程序里的一个执行路线叫做线程 thread ),更准确的定义为:“线程是一个进程内部的控制序列\\\"。 一切进程至少有一个执行线程。 线程在进程内部运行,本质上是在进程地址空间中运行。 在linux系统中,CPU看到的PCB比传统的进程更加轻量化。 透过进程虚拟地址空间,可

    2024年02月09日
    浏览(65)
  • 【Linux】多线程 --- 线程概念 控制 封装

    从前种种,譬如昨日死。从后种种,往如今日生。 1.1 进程资源如何进行分配呢?(地址空间+页表) 1. 首先我们来看一个现象,当只有第一行代码时,编译是能通过的,但会报warning,当加了第二行代码时,编译无法通过,报error。 第一行代码能编过的原因是权限缩小,虽然

    2024年02月03日
    浏览(64)
  • 学习系统编程No.28【多线程概念实战】

    北京时间:2023/6/29/15:33,刚刚更新完博客,目前没什么状态,不好趁热打铁,需要去睡一会会,昨天睡的有点迟,然后忘记把7点到8点30之间的4个闹钟关掉了,恶心了我自己一早上,真的是罪过呀!极度没睡好加没睡够,由于上篇博客马上就可以完成,所以中午没有选择睡觉

    2024年02月16日
    浏览(34)
  • 【Linux】多线程1——线程概念与线程控制

    📝 个人主页 :超人不会飞) 📑 本文收录专栏 :《Linux》 💭 如果本文对您有帮助,不妨 点赞、收藏、关注 支持博主,我们一起进步,共同成长! 💭理解线程需要和进程的概念紧密联系。 线程是一个执行分支,执行粒度比进程更细,调度成本更低; 进程是分配系统资源的

    2024年02月12日
    浏览(29)
  • Linux之线程概念

    目录 一、细粒度划分 1、堆区细粒度划分 2、物理内存和可执行程序细粒度划分 3、虚拟地址到物理地址的转化 二、线程的概念 1、基本概念 2、线程的优点 3、线程的缺点 4、线程异常  5、线程用途 三、Linux下的进程和线程 在语言中,我们知道,用户自己申请的空间是存在于

    2024年03月10日
    浏览(41)
  • 【Hello Linux】线程概念

    作者:@小萌新 专栏:@Linux 作者简介:大二学生 希望能和大家一起进步! 本篇博客简介:简单介绍linux中的多线程 一般在书上我们会这么介绍一个线程 线程是进程内部的一个执行流 他是进程的一部分 粒度要比进程更加细和轻量化 那么我们应该怎么理解呢? 下面是我的梳理

    2023年04月09日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包