多线程编程

来源:互联网 发布:剑网三曹雪阳脸型数据 编辑:程序博客网 时间:2024/05/22 06:16

一、什么是线程???
线程是在进程内部运行的控制流程。多线程的控制流程可以长期共存,操作系统会在各线程之间调度和切换,就像在多个进程之间调度和切换一样。
由于同一个进程的多个线程共享同一地址空间地址空间,因此代码段和数据段都是共享的,如果定义一个函数,在各线程之中都可以调用,如果定义一个全局变量,在各线程中都可以访问到的,除此之外,线程还共享以下资源:
1、文件描述符表
2、每种信号的处理方式
3、当前工作目录
4、用户id和组id
5、堆空间
但是有些资源是每个线程各有一份的
1、线程id
2、上下文,包括各种寄存器的值、程序计数器和栈指针
3、栈空间
4、errno变量
5、信号屏蔽字
6、调度优先级
下面介绍POSIX标准的的线程pthread。线程函数位与libpthread共享库中,由于使用了共享库因此在编译时要加上-lpthread选项。

二、线程的原理
下面所介绍的是linux中线程的原理。回想一下我们所说的线程的概念,线程是一个控制流程,那么一个单执行流的进程是不是一个控制流程呢???答案是肯定的,一个单执行流的进程也是一个线程。严格来说linux中是没有线程的概念的,linux中的线程都是用进程模拟的,所以linux中的线程又叫做轻量级进程。
我们知道,进程是操作系统分配资源的最小单位,每当内核中新创建一个PCB的时候,就表示内核中新创建了一个进程。如下图,内核会为新创建的进程分配一系列的数据结构用来管理进程。
这里写图片描述

在linux中可以通过vfork()函数来创建一个子进程,在vfork分流之后、如果子进程中没有使用exec函数进行程序替换的话,则子进程会在父进程的地址空间中运行父进程的代码。其效果如图:
这里写图片描述
这时候PCB1和PCB就拥有相同的虚拟内存和相同的物理内存了,如果我们这时候再为PCB1创建一些他私有的资源(比如为他分配栈空间,给他独立出来一部分页表),那么PCB1就变成一个线程了。

三、线程的使用
下面所介绍的函数都是POSIX标准制定的,所有函数都在线程共享库中,所以在使用的时候要引入头文件pthread.h。

1、线程的创建
int pthread_create(pthread_t tid,const pthread_attr_t *attr,void (start_routine)(void),void *arg);

返回值:成功返回0,失败返回错误码。可以通过char *strerror(int errnum)函数提取错误信息。
tid:用来保存线程的id。属于一个输出型参数,将线程的id保存起来。
attr:用来修改线程的属性。如果为NULL的时候表示创建的线程是默认属性。
start_routine:start_routine是一个函数指针,它指向一个返回值为void*,参数为void* 函数。该线程创建好了以后就会执行这个函数。当start_routine返回之后该线程就退出了。
arg:是start_routine所指向函数的参数。

2、线程的终止
线程的终止有三种方式:
2.1、return
使用return可以终止一个线程,但是如果是在main函数中调用return的话,相当于进程退出,那么所有线程都会退出,相当于调用exit或_exit函数。在任意一个线程中调用exit或_exit的话会使所有线程退出。

2.2、void pthread_exit(void *retval)
在线程中调用pthread_exit()可以使线程自己退出。退出的信息可以通过参数传出去,被等待他的线程获取。

2.3、int pthread_cancel(pthread_t tid)
在一个线程中调用pthread_cancel函数可以终止另一个线程。假设A线程是被pthread_cancel异常终止的,则pthread_join获取的线程A的退出码就是PTHREAD_CANCEL,这个宏的值是-1,可在pthread.h头文件中找到。
返回值:成功返回0,失败返回错误码。
tid:要终止的线程的id。

3、线程等待
int pthread_join(pthread_t tid,void** retval);
功能:用来等待线程退出,并回收该线程的退出信息以及释放该进程的资源。
举个栗子:在主线程之中创建一个新线程,假如在主线程要退出的情况下,新线程还没有执行完,那么当主线程退出后新线程也会立即退出,但是程序的运行结果可能就是错的。所以在这种情况下,需要主线程去等待新线程。
返回值:成功返回0,失败返回错误码。
tid:要等待线程的id。
retval:用来获取线程的退出信息。一般情况下,线程退出时不会立即释放它所占的所有资源,而是等待有人来获取它的退出信息之后才进行释放资源。否则会造成内存泄漏。

4、获取线程的id
pthread_t pthread_self();
在一个线程中调用这个函数会返回该线程的id。

四、线程的可结合和可分离
在任何一个时间点上,线程是可结合的或是可分离的。
可结合:
一个可结合的线程能够被其他线程回收资源和杀死。在被其他线程回收之前,它的存储资源是不释放的。
可分离:
一个可分离的线程是不能被其他线程回收或杀死的,它的存储器资源在他终止的时候由系统自动释放。一个可分离的线程是不能被等待的。
int pthread_detach(pthread_t tid):
这个函数可以将一个线程设置成可分离的。一个线程可以自己调用pthread_detach(pthread_self())将自己设置为可分离的。也可以通过别的线程将自己设置为可分离的。

默认情况下,线程被创建成可结合的。如果一个可结合的线程运行结束但没有被join,则它的状态类似于僵尸进程。也就是还有一部分资源没有被回收。 为了避免存储器泄漏,每一个可结合的线程都要么被显示地回收。即调用pthread_join等待进程运行结束,并可得到线程的退出码,回收其资源。或者是调用pthread_detach函数将线程分离。在调用pthread_join之后,如果该线程没有运行结束,则调用者会被阻塞。
当主线程退出后,就相当于进程退出了,这时候不管是可结合还是可分离的进程都会被回收结束掉。

五、线程的基本特点
1、进程是系统分配资源的基本单位,而线程是执行的基本单位。
2、线程在他在进程的内部运行,在linux中轻量级进程有可能就是线程。
3、由于进程之间代码段可数据段是共享的,所以线程间通信是很容易的。
4、由于线程是在一个进程内部运行的,所以线程的pid和组id是相同的,但是线程的id是不同的。
说到这,我们来介绍一条命令:ps -aL
这里写图片描述
5、一般情况下创建出来的线程的优先级都是平等的。
6、进程强调独立,线程强调共享。

栗子:有一个全局变量count=0,创建两个线程分别对这个count自增5000。

#include<stdio.h>#include<unistd.h>#include<pthread.h>int count=0;void * Counter(void *arg){    int i=0;    while(i<5000)    {        count++;        i++;    }        return NULL;}int main(){    pthread_t tid1,tid2;    pthread_create(&tid1,NULL,Counter,NULL);    pthread_create(&tid2,NULL,Counter,NULL);    pthread_join(tid1,NULL);    pthread_join(tid2,NULL);    printf("count=%d\n",count);    return 0;}

我只是为了测试它的输出结果,所以代码中许多地方写的都不规范。
以现在CPU的速度而言,一般情况下这个代码的结果都是正确的。
这里写图片描述

再看下面这个代码:

#include<stdio.h>#include<unistd.h>#include<pthread.h>int count=0;void * Counter(void *arg){    int i=0;    int val=0;    while(i<5000)    {        val=count;        printf("thread id=%lu,count=%d\n",pthread_self(),val+1);        i++;    }        return NULL;}int main(){    pthread_t tid1,tid2;    pthread_create(&tid1,NULL,Counter,NULL);    pthread_create(&tid2,NULL,Counter,NULL);    pthread_join(tid1,NULL);    pthread_join(tid2,NULL);    printf("count=%d\n",count);    return 0;}

这里写图片描述
对于这个代码而言结果又是正确的。

这到底是怎么回事呢???
这就牵扯到线程间的同步与互斥了。对于第一份代码来说,由于现在cpu性能比较高,所以一个线程还没有被切换出去就被执行完了,所以不会破坏线程间的同步与互斥性。
而对第二份代码来,由于while循环中的操作多了,还调用了printf函数(这势必会使得cpu从内核态切换到用户态),在这个过程中会将线程切出去,这过程中有可能就会导致一个线程还没有来得及将count写回内存,而另一个线程又从内存中读取值,这就造成了错误。
有关这部分的内容,请看下一篇:线程间的同步与互斥。

2 0
原创粉丝点击