Linux下的线程(线程的同步与互斥)

news2024/11/24 15:27:54

目录

Linux下线程创建函数pthread_ create()

 线程的等待函数pthread_ join()

线程终止

函数pthread exit() 

函数pthread_cancel()

分离线程pthread_detach()

线程间的互斥

线程间同步

死锁


进程和线程
线程和进程是一对有意义的概念,主要区别和联系如下:

  • 进程是操作系统进行资源分配的基本单位,进程拥有完整的虚拟空间。进行系统资源分配的时候,除了CPU资源之外,不会给线程分配独立的资源,线程所需要的资源需要共享。
  • 线程是进程的一部分,如果没有进行显式地线程分配,可以认为进程是单线程的;如果进程中建立了线程,则可以认为系统是多线程的。
  • 多线程和多进程是两种不同的概念,虽然二者都是并行完成功能。但是,多个线程之间像内存、变量等资源可以通过简单的办法共享,多进程则不同,进程间的共享方式有限。
  • 进程有进程控制表PCB,系统通过PCB对进程进行调度;线程有线程控制表TCB.但是,TCB所表示的状态比PCB要少得多。

Linux下线程创建函数pthread_ create()

函数pthread_create()用于创建一个线程。

在pthread_create()函数调用时,传入的参数有线程属性、线程函数、线程函数变量,用于生成一个某种特性的线程, 线程中执行线程函数。创建线程使用函数pthread_ create(),它的原型为:

 #include <pthread.h>

       int pthread_create( pthread_t *thread,

                                        const pthread_attr_t *attr,
                                         void *(*start_routine) (void *),

                                         void *arg );

  • thread:用于标识一个线程,它是一个pthread_ t类型的变量,在头文件pthreadtypes.h中定义:typedef unsigned long int pthread t;
  • attr:这个参数用于设置线程的属性,设置为空,采用了默认属性。
  • start_ routine:当线程的资源分配成功后,线程中所运行的单元,一般设置为自己编写的一个函数start_routine()。
  • arg: 线程函数运行时传入的参数,将一个run的参数传入用于控制线程的结束。

当创建线程成功时,函数返回0;若不为0,则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。错误代码EAGAIN表示系统中的线程数量达到了上限,错误代码EINVAL表示线程的属性非法。
线程创建成功后,新创建的线程按照参数3和参数4确定一一个运行函数,原来的线程在线程创建函数返回后继续运行下一行代码。 

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

using namespace std;

void *threadRun(void *args)
{
    while (true)
    {
        sleep(1);
        cout << "我是子线程..." << endl;
    }
}

int main()
{
    pthread_t t1;
    pthread_create(&t1, nullptr, threadRun, nullptr);

    while (true)
    {
        sleep(1);
        cout << "我是主线程..." << endl;
    }
}

在编译时,我们需要链接线程库libpthread

g++ -o  myphread  myphread .cc  -lpthread 

 从运行结果可以看到结果并不规律,主要是两个线程争夺CPU资源造成的。

 线程的等待函数pthread_ join()

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

函数pthread_join()用来等待一 个线程运行结束。这个函数是阻塞函数,一直等到被等待的线程结束为止,函数才返回并且收回被等待线程的资源。函数原型为:

 #include <pthread.h>

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

  •  thread:线程的标识符,即pthread_create()函数创建成功的值。
  • retval:线程返回值,它是一个指针,可以用来存储被等待线程的返回值。

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的:

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

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

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

4. 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。 

线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。

2. 线程可以调用pthread_ exit终止自己。

3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

函数pthread exit() 

void pthread_exit(void *retval);

 线程终止函数,跟进程一样,无返回值,线程结束的时候无法返回到它的调用者(自身)。

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

函数pthread_cancel()

取消一个执行中的线程

 int pthread_cancel(pthread_t thread);

返回值:成功返回0;失败返回错误码 。

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

using namespace std;
static int retvalue;//线程返回值

void *threadRun1(void *args)
{
    cout << "我是子线程1..." << endl;
    retvalue = 1;
    return (void *)&retvalue;
}
void *threadRun2(void *args)
{
    cout << "我是子线程2..." << endl;
    retvalue = 2;
    pthread_exit((void *)&retvalue);
}
void *threadRun3(void *arg)
{
    while (1)
    { 
        cout << "我是子线程3..." << endl;
        sleep(1);
    }
    return NULL;
}
int main()
{
    pthread_t tid;
    void *ret;

    // threadRun1 return
    pthread_create(&tid, NULL, threadRun1, NULL);
    pthread_join(tid, &ret);
    cout << "子线程1返回... 返回值:" << *(int *)ret << endl;

    // threadRun2 exit
    pthread_create(&tid, NULL, threadRun2, NULL);
    pthread_join(tid, &ret);
    cout << "子线程2返回... 返回值:" << *(int *)ret << endl;

    // threadRun3 cancel by other
    pthread_create(&tid, NULL, threadRun3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &ret);
    if (ret == PTHREAD_CANCELED)
        cout << "子线程3返回... 返回值:PTHREAD_CANCELED" << endl;
    else
        cout << "子线程3返回... 返回值:NULL" << endl;

}

分离线程pthread_detach()

int pthread_detach(pthread_t thread);

线程的分离状态决定线程的终止方法。线程的分离状态有分离线程和非分离线程两种。
在上面的例子中,线程建立的时候没有设置属性,默认终止方法为非分离状态。在这种情况下,需要等待创建线程的结束。只有当pthread_join()函数返回时,线程才算终止,并且释放线程创建的时候系统分配的资源。
分离线程不用其他线程等待,当前线程运行结束后线程就结束了,并且马上释放资源。线程的分离方式可以根据需要,选择适当的分离状态。
当将一个线程设置为分离线程时,如果线程的运行非常快,可能在pthread_create()函数返回之前就终止了。由于一个线程在终止以后可以将线程号和系统资源移交给其他的线程使用,此时再使用函数pthread_create(获得 的线程号进行操作会发生错误。

线程间的互斥

进程线程间的互斥相关背景概念

临界资源:多线程执行流共享的资源就叫做临界资源

临界区:每个线程内部,访问临界自娱的代码,就叫做临界区

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用

原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

互斥量mutex

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来一些问题。

互斥锁是用来保护一段临界区的,它可以保证某时间段内只有一个线程在执行一段代码或者访问某个资源。下面一段代码是一个生产者/消费者的实例程序,生产者生产数据,消费者消耗数据,它们共用一个变量, 每次只有一个线程访问此公共变量。

线程互斥的函数介绍
与线程互斥有关的函数原型和初始化的常量如下,主要包含互斥的初始化方式宏定义、互斥的初始化函数pthread_mutx_init()、 互斥的锁定函数pthread_mutex_lock()、 互斥的预锁定函数pthread_mutx_trylock()、互斥的解锁函数pthread_mutx_unlock()、 互斥的销毁函数pthread_mutex_destroy()。

函数pthread_mutx_init(),初始化一 个 mutex变量,结构pthread_mutex_ t为系统内部私有的数据类型,在使用时直接用pthread_mutex_t就可以了,因为系统可能对其实现进行修改。属性为NULL,表明使用默认属性。
pthread_ mutex_lock()函数声明开始用互斥锁上锁,此后的代码直至调用pthread_mutx_unlock()函数为止,均不能执行被保护区域的代码,也就是说,在同时间内只能有一个线程执行。当一个线程执行到pthread_mutex _lock()函数处时,如果该锁此时被另一个线程使用,此线程被阻塞,即程序将等待另一个线程释放此互斥锁。互斥锁使用完毕后记得要释放资源,调用pthread_mutex_destroy()函数进行释放。

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

int buffer_has_item=0;//缓冲区计数值
pthread_mutex_t mutex;//互斥区
int running = 1;//线程运行控制


//生产者线程程序
void *producter_f (void *arg)
{
    while (running)//没有设置退出值
    {
        pthread_mutex_lock(&mutex);//进入互斥区
        buffer_has_item++;//增加计数值
        printf("生产,总数量:%d\n",buffer_has_item); //打印信息
        pthread_mutex_unlock (&mutex);//离开互斥区
    }
}

//消费者线程程序
void *consumer_f (void *arg)
{
    while (running)//没有设置退出值
    {
        pthread_mutex_lock(&mutex);//进入互斥区
        buffer_has_item--;//减少计数值
        printf("消费,总数量:%d\n",buffer_has_item); 
        pthread_mutex_unlock (&mutex);
    }
}
int main ()
{
    pthread_t consumer_t;//消费者线程参数
    pthread_t producter_t;//生产者线程参数
    pthread_mutex_init(&mutex , NULL);//初始化互斥
    pthread_create(&producter_t, NULL, producter_f, NULL);//建立生产者线程
    pthread_create(&consumer_t, NULL,consumer_f, NULL) ;//建立消费者线程
    usleep(1) ;//等待,线程创建完毕
    running =0;//设置线程退出值
    pthread_join(consumer_t, NULL);//等待消费者线程退出
    pthread_join (producter_t, NULL) ;//等待生产者线程退出
    pthread_mutex_destroy(&mutex);//销毁互斥
    return 0;

}

线程间同步

在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问 题,叫做同步。POSIX信号量是用于同步操作,达到无冲突的访问共享资源目的。 POSIX可以用于线程间同步。
线程的信号量与进程的信号量类似,但是使用线程的信号量可以高效地完成基于线程的资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量的值增加;公共资源消耗的时候,信号量的值减少;只有当信号量大于0的时候,才能访问信号量所代表的公共资源。
信号量的主要函数有信号量初始化函数sem_init()、 信号量的销毁函数sem_destroy()、
信号量的增加函数sem_pom()、信号量的减少函数sem_wait()等。 还有一个函数sem_trywait(),它的含义与互斥的函数pthread_mutex_trylock()是一致的,先对资源是否可用进行判断。函数的原型在头文件文件semaphore.h中定义。
1.线程信号量初始化函数sem_int()
sem_int()函数用来初始化一个信号量。它的原型为:

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

  • sem指向信号量结构的一个指针, 当信号量初始化完成的时候,可以使用这个指针进行信号量的增加减少操作;
  • 参数pshared用于表示信号量的共享类型,不为0时这个信号量可以在进程间共享,否则这个信号量只能在在当前进程的多个线程之间共享;
  • 参数value用于设置信号量初始化的时候信号量的值。

2.线程信号量增加函数sem_post()
sem_post(函数的作用是增加信号量的值,每次增加的值为1。当有线程等待这个信号量的时候,等待的线程将返回。函数的原型为:

#include <semaphore.h>
int sem_post (sem_t *sem) ;

3.线程信号量等待函数sem_wait()
sem_wait()函数的作用是减少信号量的值,如果信号量的值为0,则线程会一直阻塞到信号量的值大于0为止。sem_wait()函数每次使信号量的值减少1,当信号量的值为0时不再减少。函数原型为:

#include <semaphore. h>
int sem_wait (sem_t *sem) ;

4.线程信号量销毁函数sem_destroy()
sem_destroy()函 数用来释放信号量sem,函数原型为:

#include <semaphore.h>

int sem_destroy(sem_t *sem) ;

5.线程信号量的例子
下面来看一个使用信号量的例子。在mutex的例子中,使用了一个全局变量来计数,在这个例子中,使用信号量来做相同的工作,其中一个线程增加信号量来模仿生产者,另一个线程获得信号量来模仿消费者。

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

sem_t sem;
int running = 1;//线程运行控制
int cnt = 5;

//生产者
void *producter_f (void *arg)
{
    int semval = 0;
    while (running)
    {
        usleep(1);
        sem_post(&sem);//信号量增加
        sem_getvalue(&sem,&semval);//获取信号量的值
        printf("生产,总数量:%d\n",semval); 
    }
}

//消费者
void *consumer_f (void *arg)
{
    int semval = 0;
    while (running)
    {
        usleep(1);
        sem_wait(&sem);//信号量增加
        sem_getvalue(&sem,&semval);//获取信号量的值
        printf("消费,总数量:%d\n",semval); 
    }
}
int main ()
{
    pthread_t consumer_t;//消费者线程参数
    pthread_t producter_t;//生产者线程参数
    sem_init(&sem , 0, 16);//信号量初始化
    pthread_create(&producter_t, NULL, producter_f, NULL);//建立生产者线程
    pthread_create(&consumer_t, NULL,consumer_f, NULL) ;//建立消费者线程
    usleep(1) ;//等待
    running =0;//设置线程退出值
    pthread_join(consumer_t, NULL);//等待消费者线程退出
    pthread_join (producter_t, NULL) ;//等待生产者线程退出
    sem_destroy(&sem);//信号量销毁
    return 0;

}

 从执行结果可以看出,各个线程间存在竞争关系。而数值并未按产生一个消耗一个的顺序显示出来,而是以交叉的方式进行,有的时候产生多个后再消耗多个,造成这种现象的原因是信号量的产生和消耗线程对CPU竞争的结果。

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/530319.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Linux】常见指令以及权限理解

&#x1f307;个人主页&#xff1a;平凡的小苏 &#x1f4da;学习格言&#xff1a;别人可以拷贝我的模式&#xff0c;但不能拷贝我不断往前的激情 &#x1f6f8;C专栏&#xff1a;Linux修炼内功基地 家人们更新不易&#xff0c;你们的&#x1f44d;点赞&#x1f44d;和⭐关注⭐…

Vue 中动态引入图片为什么要是 require

在vue中动态的引入图片为什么要使用require&#xff1f; 因为动态添加src被当做静态资源处理了&#xff0c;没有进行编译&#xff0c;所以要加上require&#xff0c; 我倒着都能背出来...... emmm... 乍一看好像说的很有道理啊&#xff0c;但是仔细一看&#xff0c;这句话说…

《设计模式》之单例模式

文章目录 1、定义2、动机2、类结构3、单例的表现形式4、总结4、代码实现(C) 1、定义 保证一个类仅有一个实例&#xff0c;并提供一个该实例的全局访问点。 2、动机 在软件系统中&#xff0c;经常有这样一些特殊的类&#xff0c;必须保证它们在系统中只存在一个实例&#xff…

可道云上传文件后报错: Call to undefined function shell_exec()

宝塔面板中直接一键部署的可道云&#xff0c;使用的是PHP8.0环境&#xff0c;上传文件或者点击我刚上传好的文件夹就会报错以下错误&#xff1a; 出错了! (warning!) Call to undefined function shell_exec() 系统错误 fileThumb/app.php[376] fileThumbPlugin->checkB…

UML时序图详解

上篇文章&#xff0c;介绍了UML状态图&#xff0c;并通过visio绘制一个全自动洗衣机的UML状态图实例进行讲解。 本篇&#xff0c;来继续介绍UML中的另一种图——时序图。 1 时序图简介 时序图(Sequence Diagram)&#xff0c;也叫顺序图&#xff0c;或序列图&#xff0c;是一…

基于SpringBoot的招聘信息管理系统设计与实现

前言 本次设计任务是要设计一个招聘信息管理系统&#xff0c;通过这个系统能够满足管理员&#xff0c;用户和企业的招聘信息管理功能。系统的主要功能包括首页、个人中心、用户管理、企业管理、工作类型管理、企业招聘管理、投简信息管理、面试邀请管理、求职信息管理、社区留…

银行数字化转型导师坚鹏:银行数字化转型的5大发展趋势

银行数字化转型的发展趋势主要包括以下5个方面&#xff1a; 从过去的局部数字化转型向全面数字化转型转变&#xff1a;2022年1月&#xff0c;中国银保监会发布《关于银行业保险业数字化转型的指导意见》&#xff0c;标志着中国银行业的数字化转型已经不是过去银行自己主导的局…

简单理解正向代理和反向代理

上一篇文章说到反向代理是用来做负载均衡的&#xff0c;同时我就想到了那么正向代理是不是也可以说一说&#xff0c;可能还是有很多人是弄不清他俩的区别是什么的吧&#xff1f; 那么本次文章就用借钱的例子来阐述一下什么是正向代理&#xff0c;什么是反向代理 正向代理 正…

Android系统的问题分析笔记(4) - Android设备硬件基础

问题 典型的Android手机/平板硬件架构是怎么样的&#xff1f; 1 典型Android手机/平板硬件架构图 2 基带处理器 (Baseband Processor) 市场上大多数的手机采用了相互独立的处理单元来分别处理用户界面软件和射频功能。即&#xff1a;应用处理器 (Application Processor&#…

5年积淀,Mapmost打造连接无限的数字孪生平台

数字孪生是充分利用物理模型、传感器更新、运行历史等数据&#xff0c;集成多学科、多物理量、多尺度、多概率的仿真过程&#xff0c;在虚拟空间中完成映射&#xff0c;从而反映相对应的实体装备的全生命周期过程。在“数字中国”、“实景中国”战略指导下&#xff0c;数字孪生…

【Redis】IO多路复用机制

IO多路复用的概念 IO多路复用其实一听感觉很高大上&#xff0c;但是如果细细的拆分以下&#xff0c; IO&#xff1a;网络IO&#xff0c;操作系统层面指数据在内核态和用户态之间的读写操作。 多路&#xff1a;多个客户端连接(连接就是套接字描述符&#xff0c;即Socket) 复用&…

什么是零知识证明?

零知识证明&#xff08;Zero Knowledge Proof&#xff0c;以下简称ZKP&#xff09;是一种加密学中的重要技术&#xff0c;它可以让一个人向另一个人证明某个事情是真的&#xff0c;而不需要透露这个事情的具体内容&#xff0c;即不需要泄露任何信息。ZKP 技术可以在不牺牲隐私的…

难见的oracle 9i恢复---2023年----惜分飞

时过境迁,以前恢复大量oracle 8/9版本的库,现在一套oracle 9i的库都比较稀奇了.今天恢复客户一套9.2.0.6的aix环境rac库,通过分析确认主要问题: 1. 重建控制文件&#xff0c;resetlogs库遗漏数据文件 2. 数据库启动主要报错ORA-600 2663和ORA-600 kclchkblk_4 Tue Nov 8 09:…

Python dshelper:动动鼠标,搞定数据探索!

本次分享一个Python数据探索小工具dshelper&#xff0c;适合快速查看数据基本特征、数据可视化等使用场景。 无需代码&#xff0c;自动完成数据集描述统计&#xff1b; 无需代码&#xff0c;界面点鼠标绘制多种统计图&#xff1a; 支持命令行、jupyter notebook、docker三种…

RK3588平台开发系列讲解(进程篇)Linux 进程的数据结构

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、Linux 进程的数据结构二、创建 task_struct 结构三、Linux 进程地址空间四、Linux 进程文件表沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 本篇将介绍 Linux 如何表示进程。 一、Linux 进程的数据结构…

Java测试:OJ练习(字符串合并后返回按照先后顺序排列的不重复新字符串、合并数组并按升序排列、Arrays 类中的sort方法)

1、给定一个长度为n的字符 串&#xff0c;字符串中只包含大小写字母。 请你返回该字符串拥有那些字符。 并将它们按照出现的先后&#xff01;顺序拼接成一个新的字符串。 这是我最开始写的&#xff0c;代码有点问题&#xff1a; public String setString(String str) {char[]…

文本三剑客之——Awk

Awk Awk简介Awk语法格式Awk常见内置变量Awk实例演示按行输出文本BEGIN模式和END模式按字段输出文本通过管道&#xff0c;双引号调用shell命令date 的用法getline的用法awk数组 Awk简介 Awk是一个功能强大的编辑工具&#xff0c;用于在Linux/UNIX 下对文本和数据进行处理。数据…

代码随想录算法训练营第六天|242.有效的字母异位词 、349. 两个数组的交集 、202. 快乐数、1. 两数之和

哈希表的表示方式&#xff1a;数组、set、map 数组&#xff1a;范围可控的情况下&#xff0c;可以用数组 set&#xff1a;哈希值较大的情况下&#xff0c;或数值分布很分散的情况 map&#xff1a;key 和 value对应的情况下 能用数组尽量用数组&#xff0c;因为数组会比较快&…

Netty内存管理

关键概念 PoolArena——内存管理的统筹者 PoolArena是内存管理的统筹者。它内部有一个PoolChunkList组成的链表 PoolChunkList——对PoolChunk的管理 PoolChunkList内部有一个PoolChunk组成的链表。通常一个PoolChunkList中的所有PoolChunk使用率(已分配内存/ChunkSize)都在…