Linux多线程

Linux多线程

一、线程的概念

线程是进程中的一条执行流,线程在进程的地址空间内运行但是因为Linux下执行流是通过PCB实现的,一个进程中可以有多个执行流,也就是一个进程中可以有多个PCB,并且这些PCB共享了很多程序运行所需资源,因此Linux下的PCB就起了一个新名字—轻量级进程。

二、进程和线程的区别

进程是操作系统进行资源分配的基本单元。

线程是操作系统进行CPU调度的基本单元。

线程是进程中的一条执行流,是CPU调度的基本单元,因为Linux下执行流是通过PCB实现的,因此Linux下的线程实际上就是一个PCB,一个进程中可以有多个执行流PCB,这些PCB共享进程的大部分资源,因此也被称作轻量级进程。也有人说Linux下没有真正的线程,线程实际上就是一个轻量级进程。

不同学习阶段的理解:

​ 以前的进程是只有一个线程/轻量级进程的进程,而现在所说的进程是可以具有多个线程/轻量级进程的进程。

​ 在以前学习进程的时候进程就是一个PCB,就是一个task_struct结构体,有很多描述信息实现操作系统对程序运行的管理和调度。

​ 现在学习进程的时候,发现线程就是进程中的一条执行流,因而在Linux下的程序调度是通过PCB来完成的,所以理解PCB就是Linux下的执行流,反推到Linux下的PCB就是一个线程,只不过Linux下的线程通常被称为轻量级进程。

三、线程之间的独有与共享

共享:

  1. 虚拟地址空间。
  2. 信号处理方式。
  3. IO信息
  4. 工作路径

独有:

  1. 独有一个栈。(共享一个虚拟地址空间,如果栈不独有的话就会造成栈混乱)
  2. 寄存器(上下文数据)
  3. errno(不同的线程可能调用同一个系统调用接口,如果一个成功一个失败,返回值就会混乱)
  4. 信号屏蔽字(信号阻塞集合)
  5. 线程ID

四、多进程与多线程在多任务处理中的优缺点

多线程:

  • 优点:
    1. 线程间通信非常灵活(通过进程间通信方式,全局变量,函数传参等)
    2. 线程的创建于销毁的成本更低。(线程资源大多共享)
    3. 线程间的切换调度成本稍低。

多进程:

  • 优点:
    1. 独立性高,稳定性强。

**共同优点:**多任务使用多执行流处理的优点

  1. CPU密集型操作:程序中几乎都是CPU数据运算。

    多核CPU:更加充分利用CPU资源。但执行流不是越多越好,多了反而增加切换调度成本。

  2. IO密集型程序:程序中几乎都是IO操作。

    IO操作:等待IO就绪,拷贝数据。

五、线程控制

在Linux系统中没有直接提供线程的操作接口,因此大佬们就在上层封装实现了一套线程库。实现在用户态创建一个线程,但是Linux程序下,程序调度只能通过PCB完成,因此在内核创建了一个PCB。这时候我们上层创建的线程叫做用户态线程,而把内核的PCB叫做轻量级进程。

**pthread_t tid:**实际上 pthread_t 在底层是无符号长整形,保存的是线程相对独立空间的首地址,这块空间开在共享区。tid实际上就是线程的操作句柄。

用户空间:通过线程库pthread,描述线程tcb,组织线程,完成的是用户空间的线程管理功能。

内核空间:创建一个tcp就会一对一对应的在内核中创建一个LWP(轻量级进程),完成的是线程的执行功能。

1.线程创建

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

  • pthread_t *thread:用于获取上层线程ID。
  • const pthread_attr_t *attr:用于设置线程属性,通常置空。
  • void *(*start_routine) (void *):线程入口函数。
  • void *arg:传递给线程入口函数的参数。
  • 返回值:成功返回0,失败返回错误码。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
void* thread_entry(void* arg)
{
    while(1)
    {
        printf("i an a nuw process %s \n",(char*)arg);
        sleep(1);
    }
    return NULL;
}
int main(int argc, char *argv[])
{
    pthread_t tid;
    char buf[32] = "hello world";
    int ret = pthread_create(&tid, NULL, thread_entry, (void*)buf);
    if(ret != 0)
    {
        printf("thread creat error:%s",strerror(ret));
        return -1;
    }
    while(1)
    {
        printf("i am main process %s \n",buf);
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

2.线程终止

一个线程的入口函数运行完毕了,则这个线程就会退出。

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

  1. 从线程入口函数return。(main函数中的return退出的是整个进程)

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

    void pthread_exit(void *retval);

    函数没有返回值,但是参数retval这是线程的退出返回值。

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

  3. 一个线程可以调用pthread_ cancel,可以在任意位置退出指定线程。

    int pthread_cancel(pthread_t thread);

    如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED (-1)。

3.线程等待

等待一个指定的线程退出,获取这个线程的退出返回值,释放资源。

默认情况下,线程退出,为了保存自己的退出返回值,因此线程所占用的资源在线程退出之后也不会完全被释放,需要被其他线程等待。

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

  • tid:表示要等待哪个线程退出。
  • retval:是一个void*空间的地址,用于接收线程的返回值。
  • 返回值:成功返回0,失败返回错误码。
  • 这是一个阻塞接口,直到有线程退出才会继续运行。

但是,当我们不关心一个线程的返回值的时候,又不需要等待线程线程退出才往下运行,这时候等待会导致性能降低,在这种场景下,等待就不合适了,但是不等待就会造成资源泄露,基于这个需求就有了线程分离。

4.线程分离

在设计线程的时候,线程有很多属性,其中一个属性就叫做分离属性,分离默认属性值为joinable,表示线程退出之后不会自动释放资源,需要被等待。

如果将线程的分离属性设置为detach,这时候线程退出后就不需要被等待,而是直接释放资源。一旦设置了分离属性,退出后就会自动释放资源,则等待将毫无意义,所以设置了分离属性的线程是不能被等待的。

int pthread_detach(pthread_t tid);

当一个线程既不关系返回值,也不想等待的时候,这种线程就适合被分离。

五、线程安全

1.概念:多线程之间对同一个临界资源的访问操作是安全的。多个 线程同时修改同一个临界资源可能会造成数据二义。

2.实现:如何实线程操作是安全的

  • 互斥:保证执行流在同一时间对临界资源的唯一访问。(互斥锁,也叫互斥量)
  • 同步:在保证数据安全的情况下(一般使用加锁操作),使不同执行流对临界资源的访问有一定的顺序性,实现对资源获取的合理操作。(条件变量,信号量)

同步和互斥协同,可以使多线程高效完成某些事物。

3.互斥锁:本质就是一个0 / 1 的计数器,进行一个状态的描述,用于标记资源的访问状态。0表示不可访问,1表示可以访问。

操作:

  • 加锁:将状态置为不可访问状态

  • 解锁:将状态置为可访问状态

  • 一个执行流在访问资源前进行加锁操作,如果不能加锁则阻塞;在访问资源完毕之后解锁。

互斥锁实现互斥,本质上自己也是一个临界资源(同一个资源所有线程在访问的时候 是加同一把锁)。

因此互斥锁必须必须先保证自己是安全的:所以互斥锁的操作是一个原子操作。

实现:

  • 定义互斥锁变量

    pthread_mutex_t mutex

  • 初始化互斥锁

    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    参数:
    mutex:要初始化的互斥量。
    attr:通常置NULL。

  • 加锁

    int pthread_mutex_lock(pthread_mutex_t *mutex); 阻塞接口,不能加锁就阻塞。

    int pthread_mutex_trylock(pthread_mutex_t *mutex); 非阻塞接口,不能加锁就报错(EBUSY)返回。

  • 解锁

    int pthread_mutex_unlock(pthread_mutex_t *mutex);

  • 释放销毁锁

    int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意

  • 在使用锁的过程中,加锁后,在任何可能退出的位置都要解锁。
  • 锁只能保证安全操作,无法保证操作合理。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int tickets = 100;
pthread_mutex_t mutex;
void* scalpers(void* arg)
{
    pthread_t tid = pthread_self();
    while(1)
    {
        pthread_mutex_lock(&mutex);
        if(tickets > 0)
        {
            printf("%p get ticket %d\n", tid, tickets);
            tickets--;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
        	pthread_mutex_unlock(&mutex);
        	pthread_exit(NULL);     
        }
    }
    return 0;
}
int main(int argc, char *argv[])
{  
    pthread_mutex_init(&mutex, NULL);
    pthread_t tid[4];
    for(int i = 0; i < 4; i++)
    {
        int ret = pthread_create(&tid[i], NULL, scalpers, NULL);
        if(ret != 0)
        {
            printf("pthread_create error\n");
            return -1;
        }
    }
    for(int i = 0; i < 4; i++)
    {
        pthread_join(tid[i], NULL);
    }
    pthread_mutex_destroy(&mutex);
    return 0;
}

在这里插入图片描述

4.死锁

概念:程序流程无法继续前进,卡死的情况叫做死锁。由于锁资源的争抢不当所导致的。

死锁产生的四个必要条件。

  1. 互斥条件:一个资源每次只能被一个执行流使用。(一个线程对资源加锁了,其他线程就不能对这个线程加锁了)
  2. 不可剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。(一个线程对资源加锁了,只能由这个线程来解锁,不能由其他进程解锁)
  3. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
  4. 环路等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

产生原因:

  1. 加解锁顺序不一致。
  2. 阻塞加锁。

死锁的预防: 编写代码过程中,破坏死锁产生的必要条件(加解锁顺序保持一致,使用非阻塞接口加锁)

**死锁的避免:**银行家算法。

  • 定义系统运行状态:安全、非安全
  • 定义表:所有资源表、已分配资源表、资源请求表
  • 思想:查看资源请求表,哪个线程要请求哪个锁,根据前两张表进行判断,这个锁分配给线程是否有可能造成环路等待,如果有则不予分配。

5.同步的实现

通过一些条件判断,保证执行流对于资源获取的合理。

条件变量: 提供了一个PCB等待队列,以及使线程阻塞和唤醒进程的两个接口。

实现:

  • 定义条件变量

    pthread_cond_t cond

  • 初始化条件变量

    int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
    参数:
    cond:要初始化的条件变量
    attr:NULL

  • 使线程阻塞

    int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
    参数:
    cond:要在这个条件变量上等待
    mutex:互斥量

    这个接口涉及三个操作:解锁,阻塞,被唤醒后加锁;

    并且解锁和陷入阻塞是与原子操作,一步完成不会被打断。

  • 唤醒阻塞的线程
    int pthread_cond_signal(pthread_cond_t *cond); 将cond的PCB队列中的线程至少唤醒一个。

    int pthread_cond_broadcast(pthread_cond_t *cond);

    将cond的PCB队列中的线程全部唤醒。

  • 释放销毁条件变量

    int pthread_cond_destroy(pthread_cond_t *cond )

条件变量使用注意事项:

  1. 是否满足条件的判断,应该使用循环操作。
  2. 多种角色线程,等待应该分开等待,分开唤醒,防止唤醒角色错误。多种角色定义多个条件变量。
  3. 信号量值提供了使线程阻塞,和唤醒线程的接口,至于什么时候阻塞,什么时候唤醒,条件变量本身并不关心,全部由用户自己控制。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int flag = 0;
pthread_cond_t cond;
pthread_mutex_t mutex;
void* printa(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        while(flag == 0)
        {
            pthread_cond_wait(&cond, &mutex);
        }
        flag = 0;
        printf("我是线程A\n");
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
    }
}
void* printb(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        while(flag == 1)
        {
            pthread_cond_wait(&cond, &mutex);
        }
        flag = 1;
        printf("我是线程B\n");
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
    }
}

int main(int argc, char *argv[])
{
    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);
    pthread_t tid1,tid2;
    pthread_create(&tid1, NULL, printa, NULL);
    pthread_create(&tid2, NULL, printb, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

信号量: 本质就是一个计数器,描述资源的有效个数,用于实现进程或线程之间的互斥与同步。

操作:

  • P操作:计数-1,判断计数是否大于等于0,成立则返回,否则阻塞。
  • V操作:计数+1,唤醒一个阻塞的进程或线程。

同步的实现:通过计数器对资源数量进行计数,获取资源之前进行P操作,产生资源之后进行V操作。通过这种方式实现对资源的合理获取。

互斥的实现:计数器初始值为1(资源只有一个),访问资源之前进行P操作,访问完毕进行V操作,实现类似于加锁和解锁的操作。

接口:

  • 定义

    sem_t sem;

  • 初始化

    int sem_init(sem_t *sem, int pshared, unsigned int value);
    参数:
    pshared:0表示线程间共享,非零表示进程间共享;
    value:信号量初始值;

  • 功能:等待信号量,会将信号量的值减1(P操作)
    int sem_wait(sem_t *sem);

  • 功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1(V操作)
    int sem_post(sem_t *sem);

  • 销毁信号量

    int sem_destroy(sem_t *sem) ;

条件变量与信号量实现同步上的区别:

  1. 本质上的不同,信号量十个计数器,条件变量没有计数器,因此条件变量的资源访问合理性需要用户自己定义,但是信号量可以通过自身计数完成。
  2. 条件变量需要搭配互斥锁一起使用,而信号量不用。

线程池

线程池适用于有大量任务需要处理的场景,使用多执行流可以提高效率。若一个任务到来就创建一个线程进行处理,处理完之后销毁线程有很大的缺陷。

  • 一个任务的处理成本(线程创建时间+任务处理时间+线程销毁时间=总耗时),若任务处理时间较短,则大量时间被线程的创建与销毁消耗了。
  • 若线程无限制创建,则在峰值压力下,会有资源耗尽系统崩溃的风险。保证服务器中处理任务的线程的个数是稳定的。

线程池其实就是一堆创建好的线程和任务队列,有任务来了就抛入线程池,分配一个线程进行处理,节省了任务处理过程中线程的创建与销毁的时间成本。而且线程池中的线程与任务节点数量都有最大限制,避免资源耗尽。

线程池的作用

  1. 减少资源的开销:减少了每次创建线程和销毁线程的开销。
  2. 提高响应速度:每次当任务到来的时候,由于线程已经被创建完毕,可以直接从阻塞队列中读取任务进行处理,因此提高了响应的速度。
  3. 提高线程的可管理性:线程是一种稀缺资源,若不加以限制,不仅会占用大量资源,而且会影响系统的稳定性。因此,线程池可以对线程的创建与停止、线程数量等等因素加以控制,使得线程在一种可控的范围内运行,不仅能保证系统稳定运行,而且方便性能调优。

线程池的实现原理

线程池一般由两种角色构成:多个工作线程和一个阻塞任务队列。

  • 工作线程:是已经被创建好的一组线程,不断地向阻塞队列中取出任务进行处理。
  • 阻塞队列:用于存储线程需要处理的任务,使生产任务的线程与处理任务的线程不直接通信,而是通过这个阻塞队列,达到解耦合的作用。

threadPool.hpp

#ifndef __THREADPOOL_H__
#define __THREADPOOL_H__
#include <iostream>
#include <queue>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <math.h>
#define NUM 5
class Task
{
    public:
        int base;
    public:
        Task()
        {}
        Task(int _b)
            :base(_b)
        {}
        void run()
        {

            std::cout<<"Task run....done"<< base << "的平方 = "<< pow(base, 2) <<std::endl;
        }
        ~Task()
        {}
};

class threadPool
{
    private:
        std::queue<Task*>q;
        int max_cap;
        pthread_mutex_t mutex;
        pthread_cond_t cond;//只能让消费者进行等待,生产者不能等,因为还要从外部获取数据
    public:
        threadPool(int _max = NUM)
            :max_cap(_max)
        {}

        static void* _enter(void* arg)
        {
            threadPool *tp = (threadPool*)arg;
            while(1)
            {
                pthread_mutex_lock(&(tp->mutex));
                while(tp->q.empty())
                {
                    pthread_cond_wait(&(tp->cond), &(tp->mutex));
                }
                Task t;
                tp->Get(t);
                pthread_mutex_unlock(&(tp->mutex));
                t.run();
            }
        }

        void threadPoolInit()
        {
            pthread_mutex_init(&mutex, nullptr);
            pthread_cond_init(&cond, nullptr);

            pthread_t tid;
            for(int i = 0; i < max_cap; i++)
            {
                pthread_create(&tid, nullptr, _enter, this);
                /*关于_enter 入口函数:
                1. 由于_enter函数是类的成员函数,会有一个隐藏的this指针,所以_enter函数实际上
                   有两个参数,this和arg,但是pthread_creat要求传入的函数只有一个参数,所以会
                   报错,所以_enter函数要定义成static函数,这个函数只属于类,并不属于某个对象。
                2. 但是这样的话,static函数就不能调用类内部成员变量和函数,所以第四个参数要传给
                   arg的值就是this指针,将this传入arg,在_enter中通过arg访问类内部成员。
                */
            }
        }

        void Put(Task &t)
        {
            pthread_mutex_lock(&mutex);
            q.push(&t);
            pthread_mutex_unlock(&mutex);
            pthread_cond_signal(&cond);
            //pthread_cond_broadcast(&cond);
            /*为什么不使用broadcast这个接口:
            	这个接口是唤醒阻塞队列中的所有线程,会造成惊群现象。而signal会唤醒一个。
            */
        }

        void Get(Task &t)
        {
            Task* T = q.front();
            q.pop();
            t = *T;
        }

        ~threadPool()
        {
            pthread_mutex_destroy(&mutex);
            pthread_cond_destroy(&cond);
        }
};

main.cpp

#include "threadpool.hpp"
int main()
{
    threadPool *tp = new threadPool();
    tp->threadPoolInit();
    
    while(1)
    {
        int x = rand()%10 + 1;
        Task task(x);
        tp->Put(task);
        sleep(1);
    }

    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值