Linux多线程

多线程

越深入的学习之后,经常能听到这么一个词—-多线程。之前的学习经常会提到多进程,父进程在忙不过来的情况下,会创建子进程进行帮忙,这样就是一个多进程的任务。那么什么是多线程呢?

线程概念

在传统的操作系统中,进程就是一个运行中程序的描述信息—-pcb,控制程序的运行。

在Linux操作系统下,pcb是进程,因为Linux下线程是以进程pcb模拟实现线程;也有人称为轻量级进程。但是Linux下没有为线程设计一个pcb来控制线程的运行。

线程1

上图介绍了线程组是个什么组合。

线程组理解

进程就是线程组,包含了一个或多个线程

从上图来看,Linux线程是PCB,因为CPU调度程序运行是调度pcb,所以线程是CPU调度的基本单位。

因为进程是线程组,程序运行时,资源是分配给整个线程组的,因此线程是资源分配的基本单位。

vfork()创建一个子进程共用同一个虚拟地址空间,怕出现调用栈混乱,因此子进程运行完毕或者程序替换后父进程才开始运行。

多线程和多进程的比较

多线程和多进程都可以并行多任务,那么哪个执行起来比较好呢

从线程的角度来看,优点:

1、一个进程中的线程共用同一个虚拟地址空间

2、线程间通信更为方便

3、线程的创建/销毁成本更低

4、同一个进程间的线程调度成本要更低

5、执行力度更加细致

缺点:

线程缺乏访问控制—-健壮性低。

比如exit(),异常针对的是整个进程,进程退出,那么线程也就不存在了。这样的话线程的可控性比较低

共同优点:都可以并发/并行处理任务,提高处理效率

多进程/多线程进行多任务处理的优势体现与细节:

CPU密集型程序:程序中都是大量的运算操作

IO密集型程序:程序中都是大量的IO操作

共同缺点:对临界资源操作需要考虑的更多,编码更加复杂

线程创建,线程终止,线程等待,线程分离

先回顾一下进程创建。在之前的总结中我们通过fork()函数和vfork()函数创建子进程。

  • fork 是 创建一个子进程,并把父进程的内存数据copy到子进程中。

  • vfork是 创建一个子进程,并和父进程的内存数据share一起用。

    这两个的区别是,一个是copy,一个是share。

    fork函数是在父进程执行到子进程创建的位置,将内存数据拷贝进入,等待子进程执行,子进程执行完后,退出后父进程在继续执行。

    vfork函数是保证子进程先执行,当子进程调用exit()或exec()后,父进程往下执行

操作系统并没有为用户提供直接创建线程的系统调用接口。但是大佬封装了一套库线程控制

用户创建的进程是一个用户态线程,在内核中对应了一个轻量级进程实现程序的调度运行

线程创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <pthread.h>
//注意!!!
//注意!!!
//注意!!!
因为是库函数,所以编译链接的时候需要加上-pthread/-lpthread链接线程库
pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void (start_routine) (void *), void *arg);
pthread_t *thread unsigned long int 输出型参数,获取线程的PID
const pthread_attr_t *attr,线程属性,大部分时间都设为NULL
void(start_routine)(void*) 线程入口函数
void* arg 通过线程入口函数传递给线程的参数 作为实参传给函数
返回值:成功返回 0,失败返回 errno>0

pthread_create()函数在调用过程中启动一个新线程。新线程通过调用start_routine()开始执行,arg作为start_routine()的唯一参数传递。

pthread_t pthread_self(void);
返回调用线程ID;

线程创建Demo,最近正好学习了C++,将代码都改成了C++的风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>                      
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

void * thr_start(void *arg){
while(1){
std::cout<<"I am child thread-----"<<(char*)arg<<std::endl;
sleep(1);
}
return NULL;
}
int main(int argc, char* argv[]){
pthread_t tid;
int ret = pthread_create(&tid,NULL,thr_start,(void*)"I am hujun!");
if(ret != 0){
std::cout<< "thread create error"<<std::endl;
return -1;
}
sleep(5);
pthread_cancel(tid);
while(1){
std::cout<<"I am main thread "<< tid << std::endl;
sleep(1);
}
return 0;
}

线程2

如图为运行结果

在新线程创建之后,执行线程函数中的操作。父进程休眠上五秒,此时在这五秒内,线程不断执行操作。在这之后,时间结束后,线程退出,父进程继续执行操作。

在打印结果中可以看到tid打印的是一串数字,为什么不是地址呢?

其实打印的tid是线程地址空间的首地址,也有其他的说法称为该线程的真实pid

通过指令ps -L查看轻量级进程

线程3

可以看到PID和LWP是相同的,这是因为

此处的PID是线程组ID—-tgid(thread group ID),可以理解为线程组中主线程pid

此处的LWP显示的就是线程ID,也可以说是tid。

线程4

线程终止

在线程入口函数中return;main函数中不能return,否则退出的是进程

1
2
3
4
5
void pthread_exit(void* retval);
退出调用线程,retval作为返回值;
主动退出,谁调用谁退出;
int pthread_cancel(pthread_t thread);
取消一个指定的线程,被动退出;

主线程退出,进程并不会退出,线程退出也会成为僵尸线程。线程地址空间无法被回收再利用,造成内存泄漏。

线程退出Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>                          
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

void *thr_start(void *arg){
while(1){
printf("child thread----\n");
sleep(1);
pthread_exit(NULL);
}
return NULL;
}

int main(int argc , char *argv[]){
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start,NULL);
if(ret != 0){
printf("thread create error\n");
return -1;
}
//使指定的线程退出tid == thread
pthread_cancel(tid);
while(1){
printf("main thread -------\n");
sleep(1);
}
return 0;
}

线程5

打印的结果如上。

如果根据代码来看,可能会认为线程创建完之后,应该先打印child pthread。但是却不是这样,这是因为对于主函数来说线程创建完成后,它将继续往下走,而重新创建的新线程需要去创建或寻找接口函数等一系列操作。当这些执行完成之后,才打印线程的操作。

在代码中有pthread_cancel这个函数的使用。线程取消的方法是向目标线程发Cancel信号(pthread_cancel函数发送Cancel信号),但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态(pthread_setcancelstate函数设置状态)决定。

所以目标线程选择了退出,之后继续执行主函数的操作

线程等待

功能:等待线程退出,获取指定线程的返回值,允许系统回收线程资源

1
2
3
4
pthread_join(pthread_t, void **retval);
要等待的线程id
retval 输出型参数-用于获取退出线程的返回值成功返回0,失败返回一个非0
返回值:0 失败:!0---errno

一个线程创建起来,默认有一个属性:joinable。**关键!!!!**

说明:

  • 调用线程将一直阻塞, 直到指定的线程调用pthread_exit, 从启动例程返回或被取消.

  • 如果线程从它的启动例程返回, rval_ptr包含返回码.

  • 如果线程被取消, 由rval_ptr指定的内存单元置为: PTHREAD_CANCELED.

  • 如果对返回值不关心, 可把rval_ptr设为NULL.

线程分离

将线程的一个属性从joinable设置为detach属性

功能:分离一个线程,线程退出后系统将自动回收资源;被分离的线程无法被等待,若是非要pthread_join则会直接报错

man手册说明:将线程标识的线程标记为已分离。当分离的线程终止时,其资源将自动释放回系统,而不需要另一个线程与终止的线程联接。

处于detach属性的线程,退出后资源直接自动被回收,这类线程不能被等待

1
2
3
4
int pthread_detach(pthread_t thread);
//thread 要被分离的线程ID
//功能:分离一个线程(设置线程的属性从joinable->detach属性),线程退出后系统将自动回收资源,被分离的线程无法被等待,若是非要pthread_join则会直接报错返回
注意:线程被分离的前提是用户不关心,线程的退出返回值

线程的分离,对于一个线程来说,任意线程再任意位置调用都可以!!!

用法:通常如果用户对线程的返回值并不关心,则在创建线程之后直接分离线程或者在线程入口函数中第一时间分离自己

处于joinable状态的线程退出后不会自动释放资源需要被等待

处于detach状态的线程退出后系统会自动回收资源并且不需要等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//一个线程等待和线程分离的Demo
#include <iostream>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
using std::cout;
using std::endl;

extern void *thr_start(void* arg){
//pthread_detach(pthread_self());
//pthread_exit((void*)"leihou");
while(1){
cout<<"i am thread"<<endl;
sleep(1);
}
return NULL;
}

int main(int argc, char* argv[]){
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if(ret != 0){
cout<<"pthread create error"<<endl;
return -1;
}
sleep(1);
}
return NULL;
}

int main(int argc, char* argv[]){
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if(ret != 0){
cout<<"pthread create error"<<endl;
return -1;
}
sleep(1);
char* retval = NULL;
int err = pthread_join(tid,(void**)&retval);
if(err == EINVAL){
cout<<"thread can not be waited"<<endl;
}
//cout<<retval<<endl;
printf("retval:%s\n",retval);
while(1){
cout<<"i am main thread"<<endl;
sleep(1);
}
return 0;
}

首先将线程创建函数的两个接口注释掉。

当线程没有退出时,pthread_join函数没有立即返回,主函数继续执行,而且没有retval返回值。

当添加pthread_exit()函数后,此时线程等待结束退出后立即返回retval返回值“leihou”

当添加pthread_detach()函数后,此时线程分离当前线程,此时将不再有线程等待这么一个操作,返回值retval此时为NULL,并且分离后资源交由系统回收,此时主函数继续循环执行。

下一篇好好分析线程安全和各种锁和消费者与生产者模型

-------------The End-------------