声明:
虚拟机版本:Centos 7.4
环境:gcc编写
操作系统进行调度运算的基本单位。说太多的概念容易混淆,我们直接通过图片来直观感受。
从上图中可以得出一些结论:
相比于进程(有对比才有伤害)
这都是因为线程共用同一块虚拟地址空间。
理论上来说,进程是资源分配的基本单位,线程是调度分配的基本单位。
从资源的角度来看,在同一个进程中的线程之间既有共用的资源,又有独占的资源。
注意:(1)所说的栈并不是虚拟地址空间中的栈(这个是主线程的栈),这个栈(包括所有的不共用资源)都存在放在栈和堆中间那段共享内存区里面,里面包含了很多关于线程的信息。
那么将上图继续完善,就涵盖了进程与线程的区别。(详细例子参考滑稽吃鸡、工厂和工厂生产线)
请注意:线程控制相关函数不是系统调用而是库函数,需要包含头文件:pthread.h p代表posix线程库
创建函数:
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//设置线程属性,一般为NULL
void *(*start_routine) (void *)//参数和返回值都为void*的函数指针,相当于这个新线程的入口函数,将指定这个新线程执行哪段代码
void *arg//上一个参数--入口函数的参数
ps:如果创建一个进程,需要两个入口函数,应该怎么办?
将函数手动包含在一个结构体中,将结构体地址传进去即可。
下面通过一段代码来创建一个进程,熟悉一个这个函数。
#include
#include
#include
void* ThreadEntry(void* arg)
{
while(1)
{
printf("In ThreadEntry\n");
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,NULL);
while(1)
{
printf("In MainThread\n");
sleep(1);
}
return 0;
}
写好后,用gcc编译会发现报错,为什么?
这是典型的链接错误(对于函数只看到了声明,未看到定义)该函数的定义在一个静态库或者动态库里面(如果需要知道在哪种库中,ldd+编译生成的可执行文件名:查得所在库路径为:/lib64/libpthread.so.0,是一个动态链接库)。
对于这种报错,使用gcc -l表示链接一个库,直接在l后面加上库名即可。
所以我们在后面加上一个-lpthread:执行语gcc thread.c -o test -lpthread
运行结果:虽然看上去两个线程是交替执行的,但是如果再执行到后面就会发现,有可能会连续出现多个MainThread或者多个ThreadEntry,因为线程之间是抢占式执行的,用户无法决定一个线程是执行还是休眠,被成为多线程编程的万恶之源。
为了能宏观上看线程,再新建一个窗口,先使用命令ps -eLf | head -n 1打印表头,
再使用命令ps -eLf |grep test查看当前所有的线程信息,LWP表示线程id。第一个线程和第二个线程的PID和PPID都相同,但LWP不同,证明是一个进程下的两个不同的线程。
下面我再通过pthread_self()来打印一下线程的pid,需要将上面代码中的两条输出语句改写成如下:
printf("In ThreadEntry:%lu\n",pthread_self());//使用lu与pthread类型对应
printf("In MainThread:%lu\n",pthread_self());
再次运行后,就可以看到打印出来的线程id,发现线程的id是这样一串串数字。
此时我再用ps -eLf查看一下当前的线程,发现了这样的情况:当前线程的id与打印出来的线程id并不一样!到底哪个是真的?
通过一番折腾,原来这两个都是线程的id,只不过是站在两个不同的角度;ps得到的线程id是站在内核角度给PCB加了一个编号,而pthread_self()得到的线程id是站在posix线程库的角度得到的,一般无特殊情况以第二个为主。
除此之外,查看线程的方法还有:
(1)线程入口函数结束(最主要)
假如将上面代码中创建出来的线程的入口函数改写成一个有限循环:
void* ThreadEntry(void* arg)
{
int count=5;
while(count)
{
printf("In ThreadEntry\n");
count--;
sleep(1);
}
}
那么当while循环结束,函数退出的时候,这个线程就结束了。
(2)调用函数pthread_exit()–结束本线程
哪个线程调用该函数,哪个线程就结束。注意不要与结束进程的函数exit()混淆
void pthread_exit(void *retval);//void* 是线程结束的返回结果,一般置为NULL
那么我现在来改变一下代码,在之前的这个线程入口函数调用该函数,其他地方保持不变,从而该线程结束。
void* ThreadEntry(void* arg)
{
int count=5;
while(count)
{
printf("In ThreadEntry\n");
count--;
sleep(1);
}
pthread_exit(NULL);
}
改变后,执行结果发生了变化:从箭头处开始,ThreadEntry这个线程就结束了。(还可以通过ps再次验证查看该线程是否还存在)
(3)调用pthread_cancel()–结束任意线程(不太推荐)
注意:这里的任意线程是指本进程中的线程
int pthread_cancel(pthread_t thread);//传入需要结束的线程号
那么再来稍微修改一下代码,尝试一下这种方法。
#include
#include
#include
void* ThreadEntry(void* arg)
{
while(1)
{
printf("In ThreadEntry\n");
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,NULL);
while(1)
{
printf("In MainThread\n");
sleep(1);
pthread_cancel(tid);
}
return 0;
}
程序运行后,ThreadEntry只会执行一次,在main线程中会将该线程结束。
那么当pthread_cancel()函数出现的时候,假设这个要被结束的线程内的程序还没有执行完,那么会出现什么情况?
依旧在之前的基础上改进一下代码,加上一个全局数组,在新线程里面执行一个遍历赋值的操作,那么,在主线程pthread_cancel()执行的时候,新线程对于数组的操作还没有执行完,就会出现只改动一半的尴尬情况,就违背了原子性。所以这种方法不太推荐使用。
#include
#include
#include
int arr[1000000]={0};
void* ThreadEntry(void* arg)
{
(void) arg;
for(size_t i=0;i<sizeof(arr)/sizeof(arr[0]);i++)
{
arr[i]=i;
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,NULL);
printf("In MainThread");
pthread_cancel(tid);
return 0;
}
举个例子:还是基于原来的代码:将线程入口函数修改一下,改成一个while死循环。
#include
#include
#include
void* ThreadEntry(void* arg)
{
(void) arg;
while(1)
{
printf("In ThreadEntry\n");
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,NULL);
printf("In MainThread");
pthread_cancel(tid);
return 0;
}
本来正常情况下,新线程永远都不会结束,但由于pthread_cancel函数的执行,会使得新线程强制结束,就会导致问题(这就类似于进程中,子进程结束时,如果父进程不回收子进程的资源,将会造成僵尸进程)所以,当一个线程结束时,会将线程结束返回的结果保存到PCB中,其他线程都可以去查看。防止出现类似于僵尸进程的内存泄漏的情况。
等待函数(阻塞函数)
int pthread_join(pthread_t thread, void **retval);
pthread_t thread//要等待的线程
void **retval//输出型参数--一般为NULL
//如果等待的线程一直不结束,那么这个函数会一直阻塞,实质是为了等待线程结束再执行后面的代码,控制执行逻辑---计算一个庞大的矩阵相乘,每个线程计算其中一部分,主线程使用pthread_join来保证所有线程都执行完
由于当pthread_join函数等待的线程不结束时,pthread_join函数会一直等待阻塞,然而我们并不希望这样的情况产生,但是线程结束后的资源又必须得回收,所以这时候就可以采用线程分离,该线程结束后会自动释放资源。它类似于忽略SIGCHLD 信号,即父进程扔下子进程不管。
分离函数:
int pthread_detach(pthread_t thread);
pthread_t thread//分离的线程号
该函数一般在创建该新线程后使用,不需要再通过pthread_join来回收资源
#include
#include
#include
void* ThreadEntry(void* arg)
{
(void) arg;
while(1)
{
printf("In ThreadEntry\n");
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,NULL);
pthread_detach(tid);
printf("In MainThread");
return 0;
}
了解了这些函数后,我来写一些相关代码熟悉一下这些函数
#include
#include
#include
int g_val=0;
void* ThreadEntry(void* arg)
{
(void)arg;
while(1)
{
printf("In ThreadEntry\n");
g_val++;//新线程的使用
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,NULL);
pthread_detach(tid);
while(1)
{
printf("In MainThread:%d\n",g_val);//主线程的使用
sleep(1);
}
return 0;
}
从打印结果可以看出g_val的++和打印操作确实在同时交替进行,可以证明,两个线程在同时访问这份空间。
第二次:在main函数内部定义一个局部变量val,同时打印这个val,那么如果此时我需要新线程也访问这个变量,是不是就访问不到了?
我们可以利用pthread_create函数的第四个参数,因为这个参数是新线程入口函数的参数,所以这个参数写成&val就ok了。再来写成代码来尝试一下。
#include
#include
#include
void* ThreadEntry(void* arg)
{
int *p=(int*)arg;//记得转换类型
while(1)
{
printf("In ThreadEntry\n");
(*p)++;
sleep(1);
}
return NULL;
}
int main()
{
int val=0;
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,&val);
pthread_detach(tid);
while(1)
{
printf("In MainThread:%d\n",val);
sleep(1);
}
return 0;
}
执行打印后,结果与全局变量完全一致。
第三次:前两次证明都是开辟的在堆上的空间,那么如果是堆上面的一块空间,线程之间还能共享吗?那么再次通过修改代码来验证一下。
#include
#include
#include
#include
void* ThreadEntry(void* arg)
{
int *p=(int*)arg;//记得转换类型
while(1)
{
printf("In ThreadEntry\n");
(*p)++;
sleep(1);
}
return NULL;
}
int main()
{
int* p=(int*)malloc(4);
*p=0;
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,p);
pthread_detach(tid);
while(1)
{
printf("In MainThread:%d\n",*p);
sleep(1);
}
return 0;
}
执行后,与前两次结果一模一样,可以证明,线程之间能够共享虚拟内存地址空间。
恢复main函数里面的内容,让新线程里面执行一个指针的越界访问,使得新线程异常终止。
#include
#include
#include
#include
void* ThreadEntry(void* arg)
{
(void)arg;
while(1)
{
sleep(1);
int* p=NULL;
*p=10;
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,NULL);
while(1)
{
printf("In ThreadEntry\n");
sleep(1);
}
return 0;
}
由于多线程存在的目的就是为了利用cpu的多核资源,现在可以通过程序来验证一下,首先要保证代码运行环境下有多核,我的是内核数为2(如果没有,将处理器数量设置成多个就ok了)
先简单写一个代码,体会一下没有线程时cpu的资源利用。写一个while(1)的循环。
#include
int main()
{
while(1);
return 0;
}
再新复制一个会话,用top查看一下当前系统资源占用情况,结果发现,进程号为16963的进程占用了当前一个cpu的所有资源。那么这个进程是不是在执行while(1)的这个进程?用ps aux | grep test查看一下即可证明。
接下来引入一个新线程,产生两个执行流,让它利用cpu的多核资源,也就是将另外一个处理器也利用起来,让资源利用率达到200%
#include
#include
#include
#include
void* ThreadEntry(void* arg)
{
(void)arg;
while(1);
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,NULL);
while(1);
return 0;
}
可以看到,此时cpu利用率已经达到200,说明我的线程已经利用了多核的c资源。
如果此时继续增加线程数,对于我的机器来说,cpu利用率不会再增高。因为我的机器内核总数为2,所以两个线程数就已经使得cpu利用率达到上限。如果一台机器内核总数为4,那么它的线程数最多可以为4就使得cpu利用率达到最大。
总结: 虽然多线程可以利用cpu的多核资源,但线程数不是越多越好,当cpu利用率达到上限后,如果继续曾加线程数,加大调度的开销,反而会降低效率。
说到效率,就想起时间,这里我构造了一个场景:假设存在一个很大的数组,将数组中每个元素都进行一个乘方运算,再赋值回数组。 可以通过对引入多线程前后程序执行时间的计算来比较效率。
//单线程:
#include
#include
#include
#include
#include
#define SIZE 100000000
//获得当前精确时间--微秒
int64_t GetUs()//int64_t:64位上的long long
{
struct timeval tv;
gettimeofday(&tv,NULL);
return tv.tv_sec*1000000+tv.tv_usec;
}
void Calu(int* arr,int begin,int end)
{
int i=begin;
for(;i<end;i++)
{
arr[i]=arr[i]*arr[i];
}
}
int main()
{
srand(time(NULL));//时间种子
//由于在栈上无法开辟一个很大的数组,所有在堆上开辟
int* arr=(int*)malloc(sizeof(int)*SIZE);
//当前想计算从这个程序的执行时间
//用时间戳
//先记录开始的时间
int begin=GetUs();
Calu(arr,0,SIZE);
//再记录结束的时间
int end=GetUs();
//两个时间做差得到执行时间
printf("time=%ld\n",end-begin);
return 0;
}
程序执行后,这里我就以1.5秒为准。
引入多线程后:多个线程同时计算,每个线程执行程序的一部分。这样就可以大大提高程序的执行效率。
//多线程
typedef struct Arg
{
int begin;
int end;
int* arr;
}Arg;
void* ThreadEntry(void* arg)
{
Arg* p=(Arg*)arg;
Calu(p->arr,p->begin,p->end);
return NULL;
}
int main()
{
int* arr=(int*)malloc(sizeof(int)*SIZE);
Arg args[THREAD_NUM];
int i=0;
int base=0;
//给每个线程分配任务,使得它们执行不同的模块
for(;i<THREAD_NUM;i++)
{
args[i].begin=base;
args[i].end=base+SIZE/THREAD_NUM;
args[i].arr=arr;
base+=SIZE /THREAD_NUM;
}
pthread_t tid[THREAD_NUM];
int64_t begin=GetUs();
i=0;
for(;i<THREAD_NUM;i++)
{
pthread_create(&tid[i],NULL,ThreadEntry,&args[i]);
}
i=0;
for(;i<THREAD_NUM;i++)
{
pthread_join(tid[i],NULL);
}
int64_t end=GetUs();
printf("time= %ld\n",end-begin);
return 0;
}
虽然我们在多线程的编写中会感到一些困难,但是在实际中有一些库或者框架能让我们很方便的书写多线程(Open MPI)
线程的同步和互斥
(1)当线程数目过多时,可能多个线程会争抢同一份资源(互斥)
(2)当线程使用同一个资源时,导致同一份资源释放多次(互斥)
(2)资源分配不均匀,导致某个线程一直得不到执行的机会(同步)