多线程编程(1)

线程概念


线程线程就是进程的若干个执行流,

因为一个进程在某一时刻只能去做一件事情,有了线程之后,我们可以在同一时间去做不同的事情,比如我正在边利用cmd markdown写博客。边用网易云音乐听音乐,这样多线程的情况下,能给我们带来很多好处。

多线程编程(1)_第1张图片

多线程编程(1)_第2张图片

在系统内核中其实是不存在线程的,linux使用进程模拟线程,线程的实现其实就是多个共享数据代码等信息的进程。所以我们把线程也叫做轻量级进程。

进程常常用来分配资源,线程用来调度资源。

线程中共享的资源:
- 文件描述符表
- 信号处理方式
- 当前工作目录
- uid,gid

线程中独立的资源:
- 线程id(tid)
- 线程的上下文信息,寄存器信息,PC制作,栈指针
- 栈空间
- errno变量
- 信号屏蔽字
- 调度优先级
- 线程私有数据

线程的函数大部分都放在pthread.h的头文件当中,并且在编译的时候我们需要注意的是加上-lpthread选项,这样就会去动态链接动态库。

线程控制


线程的控制

  • 创建线程
    线程的创建使用线程创建函数。
  int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

需要注意的是线程创建函数的参数,第三个参数start_rountine就是新创建的线程所要跑的函数,arg是你传入的参数。

因为在linux环境下,系统内部其实是不存在线程的,我们一般说线程叫做轻量级进程,线程创建以后,两个线程的pid和ppid都是一样的。操作系统会为之提供一个线程的tid,这个tid我们可以通过一个函数获取:

 pthread_t pthread_self(void);

需要注意的是pthread_self所获取到的是相对于进程的线程控制块的首地址,只是用来描述统一进程当中的不同的线程。而真正的内核当中的线程ID,对于多线程进程来说,每个tid实际是不一样的。

另外,在linux当中如果你要查询系统中的线程,也有一条命令:ps -aL

  • 线程等待
    在进程当中我们提到过,如果我们当子进程结束以后,这个时候需要让父进程得到信息,然后由父进程去进行资源的回收,以及后续的处理,所以我们要确保子进程先结束,然后父进程再结束,否则会出现僵死状态,当值内存泄漏的问题。所以进程当中我们提到了wait和waitpid函数。
    线程当中同样有上述的问题,当你的新线程结束,你的主线程也是需要等待,然后回收新线程的资源及其他信息。这样就能确保内存不泄露。所以这里使用一个函数pthread_join函数来进行等待。
int pthread_join(pthread_t thread, void **retval);

thread就是线程号,retval是一个二级指针,用途是用来获取线程的退出码。

  • 终止线程
    线程可以等待,当然也可以终止。终止线程可以使用三种方法:
方法
1 使用return返回,在主线程当中执行的时候类似于exit,直接结束进程
2 使用pthread_exit函数,终止自己的线程
3 使用pthread_cancel函数,可以用来终止统一进程的线程
void pthread_exit(void *retval);

pthread_exit用来终止线程自身,参数是返回的错误码,想要获得这个错误码,可以通过pthread_join来获得。

int pthread_cancel(pthread_t thread);

pthread_cancel参数为要终止线程的tid,

分离线程和结合线程


线程是可结合或者是分离的。
一个可结合的新线程需要被主线程回收资源和杀死。一个可结合的线程会容易出现类似于僵尸进程的问题,一般我们采用join来等待。否则就会出现主线程无法获取到新线程信息,无法回收新线程的资源,这样就会造成内存泄漏的问题。我们在默认的情况下,线程是可结合的。

int pthread_detach(pthread_t thread);

而对于分离线程,当我们把新线程设为可分离的时,这个时候主线程不再等待新线程,可分离以后,这个时候的新线程是由操作系统来进行考虑。不再由主线程来考虑,在主线程中分离,这个时候主线程知道与新线程分离,这样的话主线程是无法进行等待join的。

而在新线程分离了的话,这个时候主线程有可能不知道新线程的分离,这个时候的主线程可能会依然去join。

线程同步和互斥


当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,这个时候就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。其实实质就是当存储器读和存储器写这两个周期交叉的时候,就会使得得到与预想结果不一致的值。

线程的同步和互斥。当我们使用多线程的时候,多个线程对临界资源进行操作,这个时候如果非互斥的,那么这个时候对同一份临界资源进行操作的时候就会出现冲突的时候,比如当你对临界资源操作的时候,可能会中途进行线程的切换,这个时候原本你所读取的状态会随着硬件上下文和pc指针这些东西会保存下来,切换了线程以后,新切换的线程可能会去读取前一次你所读取的临界资源,然后对这份临界资源进行修改,然后这个时候新线程可能会再次切换,切换到你所原来保存的线程中,然后,回复了以前保存的硬件上下文和pc指针这些内容以后,这个时候线程所读取的临界资源的状态等信息还是在没有修改之前的,所以这个时候就会有隐患,造成一些缺点。

从一个变量增加的操作我们看待这个问题,首先我们要清楚一个增量操作分为三步骤:
- 1)从内存当中读入寄存器
- 2)寄存器进行增量操作
- 3)写回内存

正因为这三步操作,所以当不同步的时候,第一个线程已经对增量操作了,但是第二个线程读取到的依然是第一个线程增量操作之前的内容。这样就会出现问题,本来应该由1增加到3的,结果变为了由1到2。

所以从上面所说,可以发现多线程很容易发生上述的访问冲突,所以这里操作系统为我们提供了一个机制叫做互斥锁,这个互斥锁,我们可以去想前面所说的进程间通信的信号量,获得锁的线程可以对临界资源进行操作,没有获得锁的资源阻塞等待,二元信号量类似,获得锁的资源可以进行操作,没有获得锁的资源挂起进程放入挂起队列。

使用锁的时候我们需要对锁进行初始化:
可以去调用初始化函数或者定义锁利用宏进行初始化。

       int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

加锁的函数:

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

当我们建立了锁以后,我们就需要去销毁锁。

 int pthread_mutex_destroy(pthread_mutex_t *mutex);

这三个函数第一个lock进行阻塞式申请锁,就是当你没申请到,那么就是一直阻塞等待。第二个函数trylock是若锁资源无法获得,这个时候不阻塞,进行询问。没调用一次,进行询问一次,最后一个unlock,这个函数就是用来解锁的,无论是lock还是trylock最后都要调用unlock来进行解锁。

另外需要注意,锁的所有操作都应该是原子的。要么执行要么不执行,并且中间不能够被打断。

所以这里需要来说下关于互斥锁的实现:
为了实现对mutex便利的读取、判断、修改都是原子操作,所以大多数的体系结构都提供swap或exchange指令,这个指令把寄存器和内存单元的数据相交换,因为只有一条指令,所以保证了原子性。

如果你要让线程进行切换,那么就可以有两种方式,一种是跑完时间片,这样一个线程就会切换。另外一种方式就是模式的切换,从内核态切换到用户态。这个期间会检查内部信息,简单的模式切换就是调用系统调用,系统调用本身是操作系统暴露给用户的一些接口,这些接口大部分内容都是相关于内核的,所以会发生从用户切换到操作系统 。

说完了互斥,接下来我们需要说一下同步的概念,关于同步的概念:强调进程间协同,按照顺序的去访问临界区,一般都是在互斥下进行同步。

使用互斥锁的例子:

#include
#include
#include
#include
int count=0;

void * pthread_run(void *arg)
{
    int val=0;
    int i=0;
    while(i<5000)
    {
        //这里会出现问题,就是当两个线程进行操作的时候count的+是非原子的,
        i++;
        val=count;
        printf("pthread: %lu,count:%d\n",pthread_self(),count);
        count =val+1;

    }
    return NULL;
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;

    pthread_create(&tid1,NULL,&pthread_run,NULL);
    pthread_create(&tid2,NULL,&pthread_run,NULL);
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);

    printf("couint :%d\n",count);
    return 0;
}

多线程编程(1)_第3张图片
上述程序就会出现问题,就是两个线程tid1和tid2在跑while中的逻辑的时候,这个时候就会发生访问冲突,count本来应该是10000的,但是因为访问冲突,所以最终的结果count结果要小于10000,原因上面我已经介绍过了,就不再重复,要想实现互斥,我们加上互斥锁就好了。

#include
#include
#include
#include

//关于锁的初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int count=0;

void * pthread_run(void *arg)
{
    int val=0;
    int i=0;
    while(i<5000)
    {
        //在临界区加上互斥锁,这样就可解决线程访问冲突的问题了
        pthread_mutex_lock(&mutex);
        i++;
        val=count;
        printf("pthread: %lu,count:%d\n",pthread_self(),count);
        count =val+1;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;

    pthread_create(&tid1,NULL,&pthread_run,NULL);
    pthread_create(&tid2,NULL,&pthread_run,NULL);
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);

    printf("couint :%d\n",count);
    return 0;
}

这样最终count的结果就是100

多线程编程(1)_第4张图片

死锁


我们想一个问题,就是我们加锁以后,再次进行加锁,这样会发生什么呢?

当我们第二次申请锁的时候,这个时候锁已经被占用,该线程挂起等待直到线程锁释放。但是这个时候拥有锁的刚好是当前的这个线程,那么这个线程就这样永远挂起等待,这个我们就叫做死锁。

死锁发生的情形
一个线程两次申请锁
两个线程互相申请锁,不释放锁

死锁产生的必要条件:

死锁产生的必要条件
请求与保持 一个进程因请求资源而阻塞时,对已获得的资源保持不放
互斥条件 一个资源只能被一个进程使用
不可剥夺和不可抢占 进程已经获得的资源,未使用完之前,不能被处理器和调度器强行剥夺。资源只能由占有者自己释放,不可被优先级更高的进程
环路等待 若干进程之间形成一种头尾相接的循环等待资源关系

解决死锁的方法:

-解决死锁的方法-
破环互斥 破环了互斥,资源共享,这样就解决了死锁
破环不可剥夺不可抢占 放弃原先占有的资源,另外切换到比当前线程优先级高的线程
资源一次性分配 破坏请求和保持
资源有序分配 对资源按照编号进行申请锁,确保锁的分配顺序,防止循环

对于死锁,我们最常用的就是死锁预防和死锁避免,简单的讲死锁预防就是防止出现死锁的状况。

死锁预防:
1. 破环互斥条件:允许系统资源共享,系统不会进入死锁。
2. 破坏不可剥夺、不可抢占条件:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
3. 破坏请求和保持条件:采用静态分配方法,进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不投入运行。一进入运行,也就不再提出其他的资源请求。
4. 破环环路等待条件,采用资源有序分配法,破环环路条件。

死锁避免:
如果一个进程请求会导致死锁,则不启动它。
如果一个进程增加的资源请求会导致死锁,则不允许此分配。

解决死锁的基本方法:

预防死锁:

资源一次性分配:(破坏请求和保持条件)

可剥夺资源:即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件)

资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

避免死锁:

预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。

检测死锁

首先为每个进程和每个资源指定一个唯一的号码;

然后建立资源分配表和进程等待表,例如:

解除死锁:

当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;

撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

条件变量


条件变量的提出首先要涉及一个概念,就是生产者消费者模型,

多线程编程(1)_第5张图片
生产者消费者,是在多线程同步的一个问题,两个固定大小缓冲区的线程,在实际运行是会发生问题,生产者是生成数据放入缓冲区,重复过程,消费者在缓冲区取走数据。

生产者消费者的模型提出了三种关系,两种角色,一个场所

三种关系:
- 生产者之间的竞争关系
- 消费者之间的竞争关系
- 生产者和消费者之间的关系

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

一个场所:有效的内存区域。

我们就可以把这个想象成生活中的超市供货商,超市,顾客的关系,超市供货商供货,超市是摆放货物的场所,然后用户就是消费的。

条件变量属于线程的一种同步的机制,条件变量与互斥锁一起使用,可以使得线程进行等待特定条件的发生。条件本身是由互斥量保护的,线程在改变条件状态之前首先会锁住互斥量。其他线程在获得互斥量之前不会察觉这种改变,因此互斥量锁定后才能计算条件。

和互斥锁一样,使用条件变量,同样首先进行初始化:


int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

和互斥锁的初始化一样,它也可以采用init或者是直接利用宏进行初始化。

条件变量本身就是依赖互斥锁的,条件本身是由互斥量保护的,线程在改变条件状态钱先要锁住互斥量,它是利用线程间共享的全局变量进行同步的一种机制。

我们使用pthread_cond_wait进行等待条件变量变为真,如果在规定的时间不能满足,就会生成一个返回错误码的变量。


 int pthread_cond_wait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex);

把锁传递给wait函数,函数自动把等待条件的线程挂起,放入消费者等待队列,然后解锁挂起线程所占有的互斥锁,这个时候就可以去跑其他线程,然后当等待条件满足的时候,这个时候从等待队列中出来执行,获得刚才自己所占有的锁。

满足条件的时候可以使用函数pthread_cond_signal进行唤醒。

      int pthread_cond_broadcast(pthread_cond_t *cond);
       int pthread_cond_signal(pthread_cond_t *cond);

这两个函数都是用来进行唤醒线程操作的,signal一次从消费者队列中至少唤醒一个线程,broad_cast能唤醒等待该条件的所有线程。

当然和mutex类似,条件变量也需要清除。

int pthread_cond_destroy(pthread_cond_t *cond);

生产者消费者示例:


void * producer_run(void *arg)
{
    node_p list=(node_p)arg;    
    while(1)
    {
        sleep(1);
        pthread_mutex_lock(&mylock);
        int data=rand()%1000;
        push_head(list,data);
        pthread_cond_signal(&mycond);
        pthread_mutex_unlock(&mylock);
        printf("producer: data:%d\n",data);
    }

}

void * consumer_run(void *arg)
{   
    node_p list=(node_p)arg;    
    while(1)
    {
        pthread_mutex_lock(&mylock);
        while(list->next==NULL)
        {
            pthread_cond_wait(&mycond,&mylock);
        }
        int data=0;
        pthread_mutex_unlock(&mylock);
        pop_front(list,&data);
        printf("consumer :data:%d\n",data);
    }

}

int main()
{
    node_p list=NULL;
    init_list(&list);
    printf("init_list cuccessi\n");
    pthread_t proid;
    pthread_t conid;

    pthread_create(&proid,NULL,producer_run,(void *)list);
    pthread_create(&conid,NULL,consumer_run,(void *)list);

    pthread_join(proid,NULL);
    pthread_join(conid,NULL);

    destory_mutex_destroy(&mylock);
    destory_cond_destroy(&mycond);

    destory_list(list);

    return 0;
}

多线程编程(1)_第6张图片
结果我们发现生产者每次push一个节点,消费者每次去pop一个节点,上述代码链表的逻辑我就不给了,大家可以去我的github下载相关的代码。
https://github.com/wsy081414/linux_practice

未完。。。待续

你可能感兴趣的:(linux,一起学习C/C++)