【Linux系列】线程控制管理、线程安全-同步与互斥

Linux下的多线程

一、线程的概念

1.什么是线程?

线程就是进程中的一条执行流,是cpu调度的基本单位(而进程是cpu资源分配的基本单位),在linux下是一个轻量级进程。
linux下的线程是通过pcb实现,是程序运行的动态描述,通过这个描述 操作系统实现程序运行的调度,一个进程中可以有多个线程,这些线程共享进程中大部分资源,相较于传统pcb更加轻量化,因此也成为轻量级进程。

2.线程的独有与共享

线程共享的环境包括:进程虚拟地址空间(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理方式、进程的当前目录和进程用户ID与进程组ID。
进程拥有这许多共性的同时,还独有自己的个性。有了这些个性,线程才能实现并发性。这些个性包括:
1、线程ID
同一进程中每个线程拥有唯一的线程ID。
2、寄存器组的值
由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线 程切换到另一个线程上 时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。
3、线程堆栈
线程可以进行函数调用,必然会使用大函数堆栈。
4、错误返回码 errno
线程执行出错时,必须明确是哪个线程出现何种错误,因此不同的线程应该拥有自己的错误返回码变量。
5、信号屏蔽码
由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。
6、线程的优先级
由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。

3.多进程与多线程的优缺点分析

多线程共享虚拟地址空间,因此通信更加灵活(还可以函数传参,全局变量)。创建销毁成本更低 ;同一个进程中线程调度切换成本更低;
但是一个线程挂掉将导致整个进程挂掉;有些系统调用和异常是针对整个进程产生的
多进程数据共享复杂,需要用IPC;数据是分开的,同步简单。占用内存多,切换复杂,CPU利用率低,创建销毁、切换复杂,速度慢,进程间不会互相影响
统一优点:多执行流进行多任务处理,提高处理效率
cpu密集型程序:程序中进行大量的数据运算,对cpu资源要求高
io密集型程序:程序中进行大量的I/O操作,对cpu资源要求不高

二、线程控制

1.创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

头文件:#include <pthread.h>

在编译时注意加上-lpthread参数,以调用静态链接库。因为pthread并非Linux系统的默认库

参数:

thread: 输出型参数,指向线程标识符的指针,用于获取创建的线程ID。

attr: 用来设置现成的属性,通常置为NULL

start_routine:函数指针,传入线程运行函数的起始地址。注意函数指针的类型必须是void* ()(void) 。既:函数返回值,参数的类型都是 void*

arg:线程运行函数的参数。

返回值:创建线程成功时,返回0,创建线程失败,返回错误号

测试用例

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<sys/syscall.h>
void * route1(void *arg)
{
    while(1)
    {
    printf("pthread1  %d \n",syscall(SYS_gettid));
    sleep(1);
        }
}
void * route2(void *arg)
{
        int i;
    pthread_t tid=*(pthread_t*)(arg);
    for( i=0;;i++){ 
    printf("pthread2  %d \n",syscall(SYS_gettid));
    sleep(1);
    }
}
int main()
{
    pthread_t tid1;
    pthread_create(&tid1,NULL,route1,NULL);
    pthread_t tid2;
    pthread_create(&tid2,NULL,route2,(void*)tid1);

    //printf("main %d \n",syscall(SYS_gettid));}
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    return 0;
}
~   

通过使用gcc pthread.c -lpthread 命令编译之后运行,得到下面结果
在这里插入图片描述
ps -eflL | grep a.out&&ps -elfL | head -n 1
在这里插入图片描述
PID-- 进程id
LWP-- 轻量级进程id,可以看到主线程的PID和LWP是一样的

2.终止

(1).线程入口函数运行完毕,线程就会自动退出–在线程入口函数中调用return (但是main函数中return,退出的是进程而不是主线程)。

(2)

void pthread_exit(void *retval);

那个线程调用就退出哪个线程。如果是主线程调用,主线程退出。但是主线程退出并不会导致进程退出,只有所有的线程都退出了,进程才会退出。

参数:线程的退出返回值。
(3)

int pthread_cancel(pthread t tid);

根据线程tid,终止一个指定的线程;退出的线程是被动取消的;线程的退出返回值是PTHREAD_CANCELED

3.等待

线程等待:等待一个线程的退出,获取退出线程的返回值,回收线程所占的资源

线程有一个属性,默认创建出来这个属性是joinable,处于这个属性的线程,退出后,需要被其它线程等待获取返回值回收资源。int pthread join(pthread t thread, void *retval); --阻塞等待指定线程退出,获取其返回值

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

thread:要等待退出的线程tid --阻塞函数,线程没有退出则一直等待。

retval:输出型参数,用于返回线程的返回值

线程的返回值是一个void*,是一个一级指针 若要通过一个函数的参数获取一级指针,就需要传入一个一级指针变量的地址进来。

默认情况下,一个线程是必须被等待,若不等待,则会造成资源泄漏,所以就有一个分离属性。

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
void* thr_start(void* ptr){
    //睡两秒退出线程
    sleep(2);
    printf("这里是创建的线程\n");
    char* ptr = "这是创建线程的返回值";
    pthread_exit((void*)ptr);
}
int main(){
    pthread_t tid; 
    int res = pthread_create(&tid, NULL, thr_start, NULL);
    if(res != 0){
        printf("线程创建失败\n");
        return -1;//进程退出
    }
    char* ptr;
    pthread_join(tid,(void**)&ptr);
    printf("%s\n",(char*)ptr);
return 0;
}
4.分离

设置线程为detach属性,处于detach的线程,不需要被等待,推出后自动释放。

int pthread_detach(pthread_t tid);

参数传入线程id

注意:只有当不关心线程返回值的时候,并且不想等待线程退出,就会设置为分离属性。

三、线程安全

1.概念

使多个线程对临界资源的访问是安全的。
实现 :同步与互斥

同步 :多个线程按照某种规则,实现对临界资源访问的合理性
互斥:同一时间只有一个线程能访问临界资源,实现资源访问的安全。

同步实现 : 条件变量、信号量
互斥实现:互斥锁。

什么是互斥锁?
本质上就是一个计数器(0/1),用于标记临界资源的访问状态、每个线程在访问临界资源之前,都应该先访问互斥锁,如果是可以访问的状态,便将状态置为不可访问、然后进行资源的访问,访问结束后,进行解锁,置为可访问状态。
如果不可访问,便进行阻塞、或者报错

2.互斥锁的使用:

在这里插入图片描述

pthread_mutex_t mutex 创建一个互斥量
pthread_mutex_init(&mutex,NULL) 在主线程中初始化锁为解锁状态
pthread_mutex_lock(&mutex)(阻塞加锁)访问临界区加锁操作
pthread_mutex_trylock( &mutex)(非阻塞加锁); 与pthread_mutex_lock() 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待。
pthread_mutex_unlock(&mutex): 访问临界区解锁操作
pthread_mutes_destory(&mutes) 销毁互斥量

对于这两种上锁方式不同场景的不同使用。
trylock 在锁是上锁状态时,返回EBUSY,可以看下面的伪代码

while( pthread_mutex_trylock( &mutex)==EBUSY)
[
//锁繁忙时可以去做别的事情,一旦跳出循环,说明可以访问。
}
//在这里进行临界资源的访问
{

}

思考这样一个问题,多个线程访问一个锁,所以锁本身是一个临界资源,假如两个线程同时访问这个锁,都要把它从1改为0,那这时候如果不是原子操作,肯定会有问题。所以怎么实现原子操作呢?

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

现在我们把lock和unlock的伪代码改一下:如下
每次访问时交换寄存器和内存单元的数据,这是一个原子操作。
在这里插入图片描述

3.可重入与不可重入

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。
一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为
可重入函数,否则,是不可重入函数。

常见不可重入的情况
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构

常见可重入的情况
不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

4.死锁

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

死锁四个必要条件

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

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

避免死锁算法
死锁检测算法
银行家算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值