在程序里,一个执行线路,或者说一个执行流就叫做线程。一个线程是“一个进程内部的控制序列”。一个进程至少有一个线程。
线程是比进程更轻量级的概念。
类比进程的管理方式,进程有进程控制块(PCB),用PCB管理进程。那么线程也应该有线程控制块(TCB),用这样的数据结构来管理线程。事实上在有些操作系统下也的确是这样做的,比如Windows。但Linux作为一款非常优秀的操作系统,做出了与其它操作系统不同的选择,因为在Linux下没有TCB这样的数据结构,它的TCB是用PCB模拟实现的。
进程是分配资源的基本单位,线程是程序执行的最小单位。
线程实际上是在进程的地址空间内运行的,因为操作系统不会单独为一个线程去分配资源,所以线程拿到的资源来自于进程,这也意味着多个线程在同一个进程下会有很多的共享资源,如文件描述符表,一个线程可以访问到的文件,其他线程也可以访问的到,每种信号的处理方式,当前工作目录,用户ID和组ID等。但是线程也有一部分自己的私有数据,私有的上下文数据,私有栈结构,调度优先级等。
那么既然已经有了进程,为何要如此大费周章的搞出来一个线程呢?
- 创建一个新线程的代价要比创建一个新进程小得多,因为线程比进程的量级更轻
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 指针等待慢速I/O操作结束的同时,程序课执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I.O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
但任何事物都有它的两面性,新的方式必然会带来新的问题。
线程的缺点:
- 健壮性降低,编写多线程需要更全面更深入的考虑,在一个多线程程序里,因为时间分配上的细微偏差或者因为共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说,线程之间是缺乏保护的
- 编程难度提高,编写与调试一个多线程比单线程程序困难得多
POSIX线程库:
绝大多数以pthread_开头
链接这些线程函数库哟啊使用编译器命令的-lpthread
选项引入函数库
创建线程:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg)
//thread:返回线程ID
//attr:设置线程的属性,attr为NULL表示使用默认属性
//start_routione:是个函数地址,线程启动后要执行的函数
//arg:传给线程启动函数的参数
//返回值:成功返回0,失败返回错误码
既然刚刚说了Linux下没有真正的线程,那么线程的ID是什么呢?
在Linux中,目前线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又称轻量级进程(Light Weight Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct)。
一个进程对应内核里的一个进程描述符,对应一个进程ID。但引入线程后,一个用户进程管辖下有N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就编程1-N的关系,而进程内所有线程调用getpid()函数时又要返回相同的进程ID,这是如何解决的?
Linux引入了线程组的概念。
struct task_struct{
pid_t pid;
pid_t tgid;
...
struct task_struct *group_lerder;
...
struct list_head thread_group;
...
};
多线程的进程,又被称为线程组,线程组的每一个线程在内核之中都存在一个进程描述符与之对应。
其中的LWP(Light Weight Process):线程ID
NLWP:线程组内线程的个数。
创建出线程ID:
- pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
- 前面的线程ID属于进程调度范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程
- pthread_create函数产生并标记在第一个参数实现的地址中的线程ID,属于NPTL线程库的范畴。线程的后续操作是根据这个线程ID来操作线程的。
- 线程库NPTL提供了pthread_self函数,可以获得线程自身的ID
那pthread_t到底是一个什么类型?取决于实现方式,在NPTL下,pthread_t类型的线程ID,本质上就是一个地址空间上的一个地址。
我们知道进程终止有三种方式:
- 代码跑完,结果正确
- 代码跑完,结果不正确
- 代码没跑完,异常结束
那么线程终止有几种方式呢?
- 从线程函数return。这种方法对主线程不使用,从main函数return相当于调用exit()
- 线程可以调用pthread_exit终止自己
- 一个线程可以调用pthread_cancel终止同一进程中的另一个进程
void pthread_exit(void *value_ptr);
//value_ptr不要指向一个局部变量
//此函数无返回值,就像进程一样,线程结束的时候无法返回到它的调用者
线程的等待与分离:
同进程一样,线程也需要等待,否则可能会造成类似僵尸进程的内存泄露问题。
int pthread_join(pthread_t thread, void **value_ptr);
//thread:线程ID
//value_ptr:指向一个指针,后者指向线程的返回值
//返回值:成功返回0,失败返回错误码
分离线程
- 默认情况下,新创建的线程是joinable的,线程退出虎,需要对其pthread_join,否则无法释放2资源,从而造成系统泄露。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以让系统在线程退出的时候自动释放线程资源,这就是分离线程。
int pthread_detach(pthread_t thread);
joinable和线程分离是冲突的,一个线程不能既是joinable又是分离的。
线程的同步与互斥,这在概念上与进程的同步与互斥并没有什么不同。只是提供的函数接口有所区别,POSIX更为轻便简单一些。只需要进行加锁与开锁便可完成互斥。
我们用代码来显示线程的这些函数接口做出的简易的售票系统:
#include<pthread.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sched.h>
#include<string.h>
int ticket = 100;
pthread_mutex_t mutex; //互斥锁
void* Buyticket(void* arg)
{
char *id = (char*)arg;
while(1){
pthread_mutex_lock(&mutex); //临界区
if(ticket > 0)
{
usleep(1000);
printf("%s, %d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex); //出临界区
}else{
pthread_mutex_unlock(&mutex); //出临界区
break;
}
}
}
int main()
{
pthread_t tid1, tid2, tid3, tid4;
pthread_mutex_init(&mutex, NULL);//初始化锁
pthread_create(&tid1, NULL, Buyticket, "thread1");//创建线程
pthread_create(&tid2, NULL, Buyticket, "thread2");
pthread_create(&tid3, NULL, Buyticket, "thread3");
pthread_create(&tid4, NULL, Buyticket, "thread4");
pthread_join(tid1, NULL);//等待线程
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_join(tid4, NULL);
pthread_mutex_destroy(&mutex);//销毁锁
return 0;
}