多线程

发布于:2024-11-29 ⋅ 阅读:(28) ⋅ 点赞:(0)

线程是什么?

1、线程是进程的执行分支,一个进程内部的控制程序

2、一个进程至少有一个执行线程

3、从CPU角度来看,线程就是一个更轻量化的线程

4、线程在进程内部运行,所以本质就是在进程地址空间上运行

注意:

一个程序至少有一个进程,一个进程至少有一个线程?

程序是静态的,进程是程序一次运行

线程异常

当一个进程中的某个线程出现除0或者野指针错误时,线程异常退出,会导致进程一起退出

线程是进程的执行分支,所以当线程出现异常,进程也会出现类似异常

进程VS线程

1、线程拥有系统资源吗?

进程是资源分配的基本单位,所以线程不拥有系统资源

进程是资源分配的基本单位,线程是调度的基本单位

2、线程和进程都可以并发执行

进程比线程安全的原因是:同一进程下的线程共享同一片资源,虽然每个线程看起来只能访问自己那部分资源,但其实线程之间资源是共享的,进程之间不会数据共享

线程函数

创建线程

参数:

thread:返回线程ID

attr:设置线程的属性,attr为NULL表示使用默认属性

start_routine:是一个函数地址,线程启动后要执行的函数

arg:传给线程启动函数的参数

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

第一个返回的线程ID和之前进程调度标识线程的ID不是一个东西

1、这里的第一个参数指向虚拟内存,内存单元的地址就是这个线程ID(tid)

        所以这里pthread_t类型,本质就是进程地址空间上的一个地址

2、进程调度中的线程ID的意思是,线程是操作系统调度器的最小单位,所以需要一个数值来标识唯一的线程

获取线程ID

pthread_self函数

获取用户级线程的tid,不是轻量级进程的ID,用户级线程是由用户级空间实现的线程,而轻量级进程是由操作系统管理,一个线程可能映射一个或多个轻量级进程

线程终止

1、return返回,在main函数中return 就相当于pthread_exit

2、pthread_exit终止自己

这里参数不能指向局部变量,如果需要可以使用pthread_join()来接收

pthread_exit或者return返回的指针所指向的内存单元必须是全局或者malloc申请的,不能在线程函数的栈上分配,因为如果其他线程访问这个指针时,线程函数已经退出了,找不到指针

3、pthread_cancel

终止同一进程中的线程

返回值:成功返回0,失败返回非0

注意:

在有多个线程的情况下,主线程调用pthread_cancel(pthread_self()), 则主线程状态为Z, 其他线程正常运行

在有多个线程的情况下,主线程从main函数的return返回或者调用pthread_exit函数,则整个进程退出

线程等待

为什么需要线程等待?

退出的线程,不会释放进程地址空间中的内容

创建新线程不会复用之前退出线程的地址空间

退出方式不同,第二个参数获取的状态也会不同

1、return返回,获取线程函数的返回值

2、被别的线程pthread_cancel()之后,存放常数PTHREAD_CANCELED

3、pthread_exit()自己终止之后,获取传给pthread_exit的参数

4、不关心退出状态,设置为NULL

线程分离

默认情况下,新创建的线程都是joinable,使用pthread_join()来释放资源

但是如果对退出状态不关心时,pthread_join()就会成为负担,所以当线程退出时,自动释放资源

注意:线程分离和等待是冲突的,所以不能两个都使用

互斥

相关概念

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

临界区:每个线程内部访问临界资源的代码

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

原子性:要么完成,要么没完成,没有正在做的概念

互斥量mutex

当一个线程访问共享数据时,可能其他线程也要访问共享数据,这个时候共享数据可能就会出现二义性

这就需要引入锁,也就是互斥量来解决上述问题

1、保证当一个线程访问共享资源,其他线程不能访问共享资源(加锁)

2、如果多个线程都要访问共享资源时,只允许一个线程访问

3、当一个线程没有访问共享资源时,不能阻止其他线程访问(解锁)

mutex简单理解成一个0\1计数器

0,表示已经有执行流加锁成功,资源不可访问

1,没有加锁,资源可以访问

初始化互斥量

1、静态初始化

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

2、动态初始化

pthread_mutex_t mtx;
pthread_mutex_init(&mtx, NULL);

销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);

静态创建的互斥量不需要销毁

动态创建的互斥连更需要使用销毁函数来销毁

加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

 当一个线程加锁时,如果其他线程已经申请锁了,那么该线程加锁就会和其他线程竞争锁,陷入阻塞,等待解锁

可重入函数和线程安全

线程安全:多个线程并发同一片代码不会出现不同的结果,对静态或者全局变量进行操作,并且没有锁时,就可能会造成线程安全

可重入函数:当一个函数被不同的执行流调用,当一个函数流程还没有走完,其他线程进入,这称为重入,当一个函数再重入的情况下,结果不会出现任何不同或者任何问题,这就称为可重入函数

注意:

可重入函数是线程安全的,但是线程安全不一定是可重入函数

当对临时资源进行加锁,那么这个临时资源就是线程安全的,但是如果重入函数在加锁时,再次调用就可能会造成死锁,变成线程不安全的

死锁

死锁是指一组线程中各个线程占有不会释放的资源,但是因为互相申请别人占用的资源而处于永久等待状态

死锁的四个必要条件

1、互斥条件:一个资源只能被一个执行流使用

2、请求和保持条件:一个执行流申请资源而阻塞时,但是不释放自己的资源

3、不剥夺条件:当一个执行流获得资源,未使用完之前,不能强行剥夺

4、循环等待条件:若干执行流形成的一种头尾相连的循环等待资源关系

避免死锁

1、破环四个必要条件

2、加锁顺序一致(也就是破环循环等待条件,每个线程按照相同顺序加锁,可以避免循环等待)

3、避免锁未释放

4、资源一次性分配(避免加锁之后,再次申请资源,减少加锁场景)

Linux线程同步

条件变量:Linux同步机制,允许线程在条件不满足时进入等待状态,直到条件满足时,使线程可以进入睡眠状态,从而避免不必要占用CPU资源

同步:在保证数据安全的前提下,线程按照某种特定顺序访问临界资源,从而避免饥饿问题

竞态条件:由于操作执行顺序不当,造成的程序异常

饥饿问题:由于竞争条件不平等,导致一些线程访问共享资源被调度器忽略,从而无法访问共享资源

多线程的同步机制

1、信号量

信号量只支持两种操作,等待和信号,因为在Linux中这两个有特殊含义,所以更常用的是PV操作

信号量只有0,1

当申请信号量进行P操作,信号量-1,如果为0,在进行P操作,线程被挂起

当信号量释放时进行V操作,信号量+1

2、互斥锁

3、条件变量

既然有了互斥锁保护共享资源,那么为什么还需要信号量?信号量和互斥锁区别是什么

信号量和互斥锁最本质的区别就是互斥锁会进行锁竞争,当申请锁时,如果会进行竞争。但是如果申请信号量,当信号量被占用时,线程会被挂起,下次再调用时,该线程直接获得信号量资源

所以互斥锁可能永远也申请不到,信号量会在线程被挂起后,下次直接获取

条件变量函数

初始化和销毁

pthread_cond_init()参数arr为NULL,标识使用默认属性

条件等待

pthread_cond_timedwait()多出来的参数就是等待条件变量超时时间,如果超时就会返回错误码

这里为什么等待要在加锁和解锁之间?

1、一个线程加锁之后进入等待,其他线程如何访问临界资源?

pthread_cond_wait()会自动解锁

2、如何知道线程是否需要休眠?

临界资源不就绪,临界资源也是有状态的

3、如何知道临界资源是否就绪?

通过判断,判断也是访问临界资源,所以等待必须在加锁之后

唤醒等待

pthread_cond_signal()唤醒指定线程

 pthread_cond_broadcast()唤醒所有线程

伪唤醒

在没有明确条件触发的情况下被条件变量唤醒,这种情况就可能导致在唤醒时,条件不满足,导致程序异常

解决办法:在pthread_cond_wait()处,使用while循环来充当判断条件,当伪唤醒时,能够再次进行判断,如果条件不满足就会再次进入等待

生产消费者模型

使用场景

321原则

3种关系:生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥和同步)

2个角色:生产者和消费者

一个交易场所:特定结构的内存空间

优点:

1、生产和消费者之间解耦合

2、支持忙先不均

3、支持并发

线程池

 线程过多会带来调度开销,降低内存局部性和整体性能,线程池维护这多个线程,防止在处理多个较短任务时,对线程的申请和销毁。

同时也能够解决大量不受控制的线程占用资源导致资源耗尽

单例模式

饿汉模式

在程序初始化时,加载所有资源, 无论资源是否用到都要加载

懒汉模式

在程序初始化时,只加载核心资源,其他资源在使用时才加载

所以懒汉模式下的,“延迟加载”能够优化服务器的启动速度

这两种模式下执行流/线程都是共享同一份资源的

线程安全下的懒汉模式

1、volatile关键字防止inst被编译器优化,对inst修改不要放到寄存器中,直接反映到内存,让所有线程看到

2、双if,降低锁冲突的概率 ,提高性能

3、互斥锁,保证只会new一个inst

typename <class T>
class Singleton
{
public:
    volatile static T* inst;
    std::mutex lock;

    T* Getinstance()
    {
        if(inst==NULL)
        {
            lock.lock();
            if(inst==NULL)
                inst=new T();    
            lock.unlock();
        }

        return inst;
    }

};

STL容器默认不是线程安全的