1.什么叫线程
定义:线程是一个进程内部的控制序列
为了让我们更好地理解线程的概念,我们看看下面的图
2.线程的优缺点
优点
- 创建一个线程的代价比创建进程小得多
- 与进程间切换相比,线程间切换OS做的工作少很多
- 线程占用资源比进程少很多
- 充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序执行其他计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
这里解释一下计算密集型和I/O密集型
- 计算密集型:执行流的大部分任务以计算 为主。如加密解密,排序查找
- I/O密集型:执行流的大部分任务以I/O为主,如刷磁盘,访问数据库,访问网络
缺点
- 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
- 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
3.进程VS线程
进程是承担分配系统资源的基本实体,线程是调度的基本单位
线程共享进程的数据,但也拥有自己的一部分数据:
- 线程id
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
4.创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
thread:返回线程ID 是一个输出型参数
attr:设置线程的属性,attr为NULL表示使用默认属性,一般我们使用NULL即可
start_routine:是个函数指针,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
下面我们使用这个函数写一段代码测试一下
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* Routine(void* arg)
{
char* msg = (char*)arg;
while(1)
{
printf("%s\n", msg);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, Routine, "thread 1");
sleep(1000);//这里我们让主线程sleep,否则主执行流向下执行直接return 0了,这样的话进程退出了,我们什么也看不到
return 0;
}
还有一点我们要注意的是,我们在使用gcc编译的时候,使用了pthread库,gcc本身是不认识的。所以在编译的时候必须带上**-lpthead**
#include <pthread.h>
pthread_t pthread_self(void);
下面我们使用一下这个函数试试
5.线程等待
线程也是需要被等待的,这点类似于我们之前学过的父进程等子进程。一般情况下,我们想要知道我们创建出来的线程把我们交给其的任务完成的怎么样,因此我们可以通过线程等待得到线程退出码,进而判断线程的代码执行情况
线程等待使用的函数是这个
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
thread:被等待的线程id
retval:拿到被等待线程的退出码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* Routine(void* arg)
{
char* msg = (char*)arg;
int count = 5;//5s以后让线程退出
{
printf("%s\n", msg);
sleep(1);
}
return (void*)123;//随便写一个
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, Routine, "thread 1");
void* ret = NULL;//用于接收线程的退出码
pthread_join(tid, &ret);
printf("pthread 1 exit code: %d\n", (int)ret);
return 0;
}
6.线程终止
这里我们只讨论正常终止。
return xxx 代表线程退出,xxx就是线程的退出码。这里不意味着整个进程退出。只有在主函数里面return xxx 才意味进程退出
pthread_exit().同样是线程退出,括号里面就是退出码
pthread_cancel().线程取消,取消成功,退出码一般是-1。通常是主线程取消其他线程
我们来测试一下线程取消
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* Routine(void* arg)
{
char* msg = (char*)arg;
int count = 5;
while(count--)
{
printf("%s\n", msg);
sleep(1);
}
pthread_exit(112);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, Routine, "thread 1");
void* ret = NULL;
sleep(2);
pthread_cancel(tid);//两秒后取消创建处的线程,看线程退出码是否为-1
pthread_join(tid, &ret);
printf("pthread 1 exit code: %d\n", (int)ret);
return 0;
}
7.线程分离
一般情况下,线程是必须被等待的,不过当某些时候,我们不关心线程的返回值,线程等待反而是一种负担,这时候,我们可以告诉系统,当线程退出时,自动释放线程资源。这就要用到函数pthread_detach()
#include <pthread.h>
int pthread_detach(pthread_t thread);
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* Routine(void* arg)
{
char* msg = (char*)arg;
int count = 5;
while(count--)
{
printf("%s\n", msg);
sleep(1);
}
pthread_exit(112);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, Routine, "thread 1");
// void* ret = NULL;
sleep(10);
if(pthread_detach(tid) == 0)
{
printf("线程分离成功\n");
}
return 0;
}
8.线程id及进程地址空间布局
所谓的线程id其实就是一个地址。那么这个地址是哪里的地址呢,请看下图