线程
1. 线程的概念
1.1 什么是线程
线程是进程内部的一个控制序列
所有的进程都应该有着至少一个执行路线
1.2 进程与线程
进程是资源分配的最小单位
线程时程序执行的最小单位(调度)
进程的所有信息都是对线程共享的,包括可执行的程序文本,程序的全局内存和栈,堆内存以及文件描述符,但是线程也有着自己的一部分私有数据:
线程ID,寄存器,执行时栈,errno,信号屏蔽字,调度优先级
举个例子:
比如学校要举办一个运动会,然后对于运动会的举办肯定就要做各种各样的事情,买东西,策划等等的一些事情。对于买东西,策划这些事情肯定都是不同的人同时进行着。
学校就相当于进程,做不同事情的人们就相当于线程。
【注意】
对于Linux下来说,就根本没有线程这个概念,全都是进程。
所以对于一个线程的创建其实来说也就是创建了一个新的进程,然后该进程的PCB与以前进程的PCB指向同一个虚拟地址空间,这样的话,就可以共享数据,然后不同的PCB执行不一样的代码。
进程也就叫做轻量级进程
线程是以进程为模板创建出来的
1.3 一个进程的多个线程共享
对于线程都是同一个地址空间,所以Text Segment、Data Segment都是共享的。
如果定义一个函数,在各个线程都可以调用
定义一个全局变量,在各个线程都可以访问得到
除此之外,线程还共享:
文件描述符表
每种信号的处理方式
SIG_IGN(忽略),SIG_DFL(默认,一般情况下都是终止进程)或者用户自定义的信号处理函数
当前工作目录
用户id和组id
1.4 线程的优点
创建一个新的线程的代价要比一个进程的代价要小得多
因为创建一个进程需要开辟虚拟地址空间,页表,PCB等一系列的操作,而对于线程来说,虚拟地址空间,页表这些资源都已将开辟好了,只需要自己在开辟PCB即可。
与进程间切换相比较,线程间切换需要操作系统做的工作很少
对于进程间切换的话,需要切换虚拟地址空间,页表等等,但是对于线程切换的时候只需要,将PCB进行变换就好了
线程占用的资源要比进程小得多
线程的资源大部分都是共享进程的,只有一小部分自己的私有资源。如:线程ID,一组寄存器,运行时栈,errno,信号屏蔽字,调度优先级
能充分利用多处理器的可并行数量
对于多个处理器的话,如果只有一个进程的话,那么就会造成只有一个处理器在工作,其他的几个处理器就会造成闲置,这对于处理器是一种资源浪费。所以如果可以将一个进程要做的事情,划分为多个然后交给线程来做的话这样就会充分利用多处理器的资源了
在等待慢速I/O操作结束的同时,程序可执行其他的任务
比如对于一个网易云音乐来说,你可以一边下载一首歌曲,然后还可以听歌,这就是多个线程造成的现象
计算密集型应用,为了能够在多处理器系统上运行,将计算分解到多个线程中实现
对于计算密集型的应用为了提高他的运算效率,可以将其运算工作划分出来几个部分,然后将这几个部分交给不同的线程来进行计算,多个线程再放到不同的处理器下来记性计算,就可以提高效率了
I/O密集型应用,为了提高性能,可以将I/O操作重叠,线程可以同时等待不同的I/O操作
1.5 线程的缺点
性能损失
健壮性降低
对于多个线程的程序,由于时间分配上的细小误差或者因为共享了不该共享的变量造成了不良影响的可能性是非常大的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
编程难度提高
对于编写与调试一个多线程的程序要比一个单线程的复杂的多
2. 线程控制
对于线程控制来说首先要了解我们使用的是POSIX线程库。
对于该线程库多数函数的名字都是以“pthread”开头的
要使用这个线程库,必须要引入头文件phread.h
链接线程函数库是要使用命令
-l pthread
2.1进程ID与线程ID
在Linux中。当前线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又称为轻量级线程(Light Weighted Process),每个用户态的线程,在内核中都对应这一个调度实体,也拥有这自己的进程描述符(task_struct结构体)
没有线程之前,一个进程对应内核中的一个进程描述符。(1:1)
但是有了线程之后,一个进程对应着多个内核描述符。(1:N)
但是对于每一个线程对其调用getpid都要返回同一个进程ID,那么该如何解决上述的问题呢?
线程组:
struct task_struct { ... pid_t pid; tid_t tgid; ... struct task_struct *group_leader; ... struct list_head thread_group; ... };
多线程的进程又被称为线程组,线程组内的每一个线程在内核中都有着一个进程描述符(task_struct)与之对应。
进程描述符结构体中的pid,对应的是线程的ID
进程描述符里面的tgid,对应的是进程的ID
用户态 | 系统调用 | 内核进程描述符对应的结构 |
---|---|---|
线程ID | gettid(void) | pid_t pid |
进程ID | getpid(void) | pid_t tgid |
对于现在学的线程ID,不同于POSIX线程库的线程ID,那个线程ID的类型是pthread_t,线程ID和进程ID一样可以唯一表示线程或者进程
如何查看一个线程的ID呢?
ps -L
-L选项会显示
LWP:线程ID,即gettid()系统调用的返回值
NLWP:线程组内线程的个数
对于线程IDLinux提供了gettid的系统调用来返回ID,可以glibc并没有将该系统调用封装起来,在开放的接口中供成员使用。
可以采用以下的方式来获得线程ID
#include <sys/syscall.h> pid_t tid; tid = syscall(STS_gettid);
线程组内的第一个线程,在用户态被称为主线程,在内核中被称为group leader,在内核创建第一个线程时,会将线程组的ID设置成第一个线程的线程ID,group leader指针指向自身,即主线程的进程描述符。
线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程
【注意】:
线程和进程不一样,进程有父进程的的概念,但在线程组里面,所有的线程都是对等关系的
2.2线程ID及进程地址空间布局
pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址里面。
该线程ID与前面所说的线程ID是不一样。
前面的线程ID是属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_create函数所产生的线程ID,是属于NPTL线程库的范畴,该线程ID其作用就是供线程库的其他函数,来使用该线程ID操作该线程
NPTL线程库,提供了pthread_self函数,可以获得线程自身的ID
pthread_t pthread_self(void);
对于pthread_t这个类型来说,其实pthread_t的线程ID,其本质就是进程地址空间上的一个地址
如图:
mmap:可以将一个文件或者其他对象映射进内存。如果文件不是多个页的大小之和,最后一个页不被使用的空间将会清零。
3. 创建线程
功能:创建一个新的线程 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg); 参数: thread:返回线程的ID attr:设置线程的属性,attr为NULL表示使用默认的属性 start_routine:是一个函数地址,线程启动要执行的函数 arg:传给线程启动函数的参数 返回值: 成功返回0,失败返回错误码
错误检测:
对于pthread函数与其他的函数不一样的地方就是出错返回错误码
对于pthread同样也有着自己的errno变量,以支持使用errno的代码。但是对于pthread函数的错误,一般都是通过返回值来进行判定的,因为读取返回值的代价要比读取线程内errno变量的值的开销要小得多
例:
#include <iostream> #include <pthread.h> #include <unistd.h> void* routine(void* arg) { int i = 10; while (i > 0) { std::cout << (char*)arg << '\n'; i--; } return NULL;//线程终止 } int main() { pthread_t tid; int ret = pthread_create(&tid, NULL, routine, (void*)("thread1")); if (ret > 0) { std::cout << "pthread_create error!" << '\n'; } sleep(1); int i = 30; while (i > 10) { std::cout << "I am main thread!" << '\n'; i--; } std::cout << "main exit!" << '\n'; return 0; }
3. 线程终止
如果只是终止某个线程的话,而不终止整个进程,有着以下的四种方法:
从线程函数return。
这种方法对于主线程不适用,因为从main函数进行return的话,就相当于调用exit
一个线程可以调用pthread_cancel终止同一个进程中的另一个线程
线程可以调用pthread_exit函数终止自己
可以调用pthread_kill函数来终止一个线程
如果任意线程调用了exit,_exit和Exit,那么整个进程就会终止。
3.1 pthread_exit
功能:线程终止 void pthread_exit(void* retval); 参数: retval:对于是joinable的线程,是其他线程调用pthread_join函数所得到的输出型参数的返回值 返回值: 无返回值,和进程一样,线程结束无法返回到他的调用者
对于retval来说:
这是一个输出型参数,该线程终止所返回的信息是放在retval所指向的内存空间的
3.2 pthread_cancel
功能:向线程发送取消请求 int pthread_cancel(pthread_t thread); 参数: thread:需要取消的线程的ID 返回值: 成功返回0,失败返回错误码
3.3 pthread_kill
功能:向一个线程发送一个信号 #include <signal.h> int pthread_kill(pthread_t thread, int sig); 参数: thread:线程ID(pthread_create函数的输出型参数) sig:信号的编号 返回值: 成功返回0,失败返回错误码,并且没有信号发送
例:
#include <iostream> #include <pthread.h> #include <unistd.h> pthread_t main_tid; int retval; void* routine(void* arg) { pthread_cancel(main_tid); int i = 100000; pthread_detach(pthread_self());//自我分离 while (1) { std::cout << (char*)arg << ":" << i <<'\n'; i--; } pthread_exit((void*)&retval); //return NULL;//线程终止 } int main() { main_tid = pthread_self(); pthread_t tid; int ret = pthread_create(&tid, NULL, routine, (void*)("thread1")); if (ret > 0) { std::cout << "pthread_create error!" << '\n'; } sleep(5); int i = 30; while (i > 10) { std::cout << "I am main thread!" << '\n'; i--; } std::cout << "main exit!" << '\n'; while (1) { std::cout << "asd" << '\n'; } }
该程序展示了主线程退出后,子线程不一定非要退出,因为进程没有退出,所以子线程还是可以继续运行的。
【注意】:
一个线程的退出可以影响到另一个线程的原因在于,关键要看那个退出的线程有没有让进程也退出,如果进程也退出了的话,其余的所有线程都要进行退出。
因为线程是依赖于进程的
对于线程来说
当内核通过kill命令发送一个信号给线程的话,内核会默认将该信号添加到整个线程组上
所以为了区分发送给进程的信号还是发送给线程的信号,task_struct中有这两套signal_pending,一套是线程组公用的,一套是对于单个线程的。
当通过kill发送的信号时,会放在线程组共享的signal_pengding中,可以由任意一个线程进行处理。而由pthread_kill发送的信号,被放在线程私有的signal_pending中,只能由该线程进行处理。
4. 线程等待和分离
4.1 线程等待
为什么需要等待线程?(与进程类似)
已经退出的线程,其空间没有释放,仍然在进程的地址空间里
创建的新线程不回去复用刚才退出的地址空间
功能:等待线程结束 int pthread_join(pthread_t thread, void** retval); 参数: thread:线程的ID retval:他指向一个指针,后者指向线程的 返回值: 成功返回0,失败返回错误码
调用该函数的线程将挂起等待(阻塞式等待),直到ID为thread的线程终止。thread线程以不同的方式终止,通过pthraed_join得到的终止状态是不一样的。
如果thread线程通过return返回,retval所指向的单元里面存放的是thread线程函数的返回值
如果thread线程是被别的线程调用pthread_cancel异常终止掉的,rerval所指向的单元存放的是常数PTHREA_CANCELED
如果thread线程是自己调用pthread_exit终止的,retval所指向的单元里面存放的是pthread_exit的参数
如果对thread线程的终止状态不关心,可以传NULL给retval参数
如果thread线程是调用pthread_kill终止的,retval所指向的单元存放的是随机值
4.2 分离线程
默认情况下创建的线程都是joinable(结合的),线程退出后,需要对其进行pthread_join操作,否则就会无法释放资源,从而造成内存泄露
如果不关心线程的返回值的话,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
int pthread_detach(pthread_t thread); int pthread_detach(pthread_self());
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
【注意】:
joinable和分离是冲突的,一个线程既不能是joinable也是分离的
#include <iostream> #include <pthread.h> #include <unistd.h> void* routine(void* arg) { pthread_detach(pthread_self()); std::cout << (char*)arg << '\n'; return NULL; } int main() { pthread_t tid; if (pthread_create(&tid, NULL, routine, (void*)("thread 1")) > 0) { std::cout << "pthread_create error!" << '\n'; } sleep(1); int ret = 0; if (pthread_join(tid, NULL) == 0) { std::cout << "wait success!" << '\n'; } else { std::cout << "wait error!" << '\n'; ret = 1; } return ret; }
输出:
thread 1 wait error
说明pthread_join和pthread_deatach不能同时使用,这样是冲突的
5. 线程同步与互斥
大部分情况下,线程使用的变量都是局部变量,变量的地址空间在线程空间内,这种情况变量属于单个变量,其他的线程无法获得这个变量
但是有的时候,很多的变量要在线程内共享,这样的变量就称之为共享变量,可以通过数据的共享,完成多个线程的交互。
多个线程并发的操共享变量这样会带来一些问题。也即是对于数据的修该出错等问题?
#include <iostream>
#include <pthread.h>
#include <unistd.h>
//出现6的情况是这样的,有可能下面创建线程创建到第六个出错的时候,i已经变成6了,然后对于buyTicket函数再次从内存中取值的时候,所以取到的就是6了
//多个线程来的时候是对这个函数的重入
void* buyTicket(void *arg)
{
int ticket = 20;
while (ticket > 0)
{
usleep(1000);
ticket--;
std::cout << (char*)(arg) << "buy ticket!Have Ticket:" << ticket << std::endl;
}
return NULL;//线程结束
}
int main()
{
pthread_t tid[6];
//不能用这种方法创建,因为buyTicket读取的是地址值,就会导致,每次读取的都是最新的i值,不会看到以前的线程了
//创建了五个子线程
//for (int i = 1; i < 6; i++)
//{
// if (pthread_create(tid + i, NULL, buyTicket, (void*)(&(i))) > 0)
// {
// std::cout << "pthread_create error!" << std::endl;
// }
// sleep(1);
//}
if (pthread_create(tid + 1, NULL, buyTicket, (void*)("1")) > 0)
{
std::cout << "pthread_create error!" << std::cout;
}
if (pthread_create(tid + 2, NULL, buyTicket, (void*)("2")) > 0)
{
std::cout << "pthread_create error!" << std::cout;
}
if (pthread_create(tid + 3, NULL, buyTicket, (void*)("3")) > 0)
{
std::cout << "pthread_create error!" << std::cout;
}
if (pthread_create(tid + 4, NULL, buyTicket, (void*)("4")) > 0)
{
std::cout << "pthread_create error!" << std::cout;
}
if (pthread_create(tid + 5, NULL, buyTicket, (void*)("5")) > 0)
{
std::cout << "pthread_create error!" << std::cout;
}
for (int i = 1; i <= 5; i++)
{
if (pthread_join(tid[i], NULL) == 0)
{
std::cout << i << " thread quit!" << std::endl;
}
sleep(1);
}
return 0;
}
-
while判断条件为真后,代码可以并发的切换到其他进程
-
对于后面的sleep,可以将进程挂起,也就是对于可以让其他的线程有着充足的时间来进入该临界区,可以让很多个线程进入该临界区
-
--num
本身就不是一个原子操作
--操作并不是原子操作,而是对应三条汇编指令
第一步:将变量num加载到内存的寄存器中
第二步:更新寄存器里面的值,进行-1操作
第三步:将新值,从寄存器写回到变量num的内存空间
要解决以上问题,需要做到三点:
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他的进程进入该临界区
如果多个线程同时要求执行临界区的代码,并且这个时候临界区没有线程再执行,那么只能允许一个线程进入该临界区
如果线程不在临界区执行,那么该线程不能阻止其他的线程进入临界区
如图:
5.1 初始化互斥量有两种方法:
方法一:静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法二:动态分配
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr* restrict attr); 参数: mutex:要初始化的互斥量 attr:NULL
5.2 销毁互斥量
销毁互斥量
销毁互斥量需要注意:
使用PTHREAD_MUTEX_INITALIZER初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destory(pthread_mutex_t* mutex);
5.3 互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t* mutex); int pthread_mutex_unlock(pthread_mutex_t* mutex); 返回值: 返回值为0,失败返回错误号
调用pthead_lock时,可能会遇到一下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞,等待互斥量解锁