一、Linux线程的概念
1.什么是线程
1.一个进程的一个执行线路叫做线程,线程的一个进程内部的控制序列。
2.一个进程至少有一个执行线程
3.线程在进程内部,本质是在进程地址空间内运行
4.操作系统将进程虚拟地址空间的资源分配给每个执行流,就成了线程执行流。
2.线程的优点
1.创建新线程的成本远低于创建新进程。
2.与进程切换相比,线程切换需要较少的操作系统资源。
3.线程比进程占用更少的资源。
4.它们能有效地利用多处理器并行处理的能力。
5.在等待缓慢的I/O操作时,程序还可以进行其他计算任务。
6.在多处理器系统中,计算密集型应用通过分解计算任务到多个线程中来运行。
7.I/O密集型应用通过重叠I/O操作来提升性能,允许线程同时等待多个I/O操作。
3.线程的缺点
a.增加额外开销
计算密集型线程,若很少受外部事件阻塞,通常不能与其他线程共享同一处理器。当计算密集型线程的数量超过可用处理器时,可能会导致显著的性能损失。这种性能损失主要是由于增加了额外的同步和调度开销,而可用资源保持不变。
b.线程安全
多线程编程需要更全面和深入的考量。在多线程程序中,由于时间分配上的微小偏差或共享了不应共享的变量,可能会产生不良影响,这意味着线程之间缺乏必要的保护。
c.缺乏访问控制
进程是访问控制的基本单位。在一个线程中调用某些操作系统函数可能会影响整个进程。
d.编程难度提高
编写和调试多线程程序比单线程程序更加困难。
e.线程异常
如果单个线程出现除零错误或野指针问题导致崩溃,整个进程也会因此崩溃。线程是进程的执行分支,一旦线程发生异常,就相当于进程发生异常,这将触发信号机制并终止进程。一旦进程终止,该进程内的所有线程也将随之退出。
二、Linux下进程与线程的区别
进程是资源分配的基本单位 线程是调度的基本单位
1.资源的区别
线程共享进程的数据,同时也有它们自己独立的数据部分:
线程ID
一组寄存器
栈
errno
信号屏蔽字
调度优先级
进程中的多个线程共享同一个地址空间,因此Text Segment、Data Segment都是共享的。如果定义了一个函数,它可以在所有线程中被调用;如果定义了一个全局变量,它也可以在所有线程中被访问。除此之外,所有线程还共享其他进程资源和环境:
文件描述符表
每种信号的处理方式
当前工目录
用户id和组id
2.切换开销
从一个进程切换到另一个进程,需要保存当前进程的状态并恢复新进程的状态,这个过程称为上下文切换。而线程切换的开销较小,因为线程共享相同的地址空间,不需要改变地址空间的映射关系。
3.同步与通信
进程之间的通信需要进行进程间通信(IPC),如管道、消息队列等,这些方法效率较低。而线程之间的通信可以直接通过共享内存、全局变量等方式进行,效率较高。
4.单元性
进程是独立的执行单位,一个进程崩溃不会影响其他进程。而线程共享同一个进程的资源,一个线程崩溃可能导致整个进程崩溃。
三、Linux线程控制
1.POSIX线程库
POSIX线程库(POSIX Threads)也被称为pthread库,是一套线程创建、同步、调度和管理的标准接口,定义了在多线程程序中使用线程的API和语义。POSIX线程库通常用于UNIX和类UNIX操作系统(如Linux)上。
POSIX线程库的主要特点包括:
-
标准化接口: POSIX线程库定义了一套标准的线程操作接口,包括线程的创建、销毁、同步等操作,使得多线程程序能够以跨平台的方式编写。
-
线程管理: POSIX线程库提供了对线程的管理功能,包括线程创建、退出、同步、互斥锁、条件变量、信号量等。
-
线程调度: POSIX线程库定义了线程的调度接口,可以设置线程的调度策略、优先级和调度参数。
-
线程同步: 提供了各种线程同步的机制,确保多个线程之间的协作和同步。
在使用POSIX线程库时,需要包含 <pthread.h>
头文件,并链接 -lpthread
库。
2.创建线程
使用pthread_create()函数
函数声明:
参数解析
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
传统的一些函数在成功时返回0,在失败时返回-1,并且会对全局变量errno赋值以指示错误类型。
而pthreads函数在出错时不会设置全局变量errno(尽管大多数其他POSIX函数会这样做),它们会通过返回值来传递错误代码。
pthreads也提供了线程内的errno变量,以便支持其他依赖errno的代码。但对于pthreads函数的错误处理,建议通过检查返回值来判断,因为这比读取线程内的errno变量要节省开销。
示例代码:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"
void *Printf(void *arg)
{
printf("wohu!, 我是一个线程...\n");
sleep(2);
}
using namespace std;
int main()
{
pthread_t pid;
int ret = pthread_create(&pid, nullptr, Printf, nullptr);
if(ret != 0)
{
cerr << " 线程创建错误" << endl;
exit(EXIT_FAILURE);
}
pthread_join(pid,nullptr);
return 0;
}
3、线程ID与虚拟地址空间的关系
pthread_create函数生成一个线程ID,存储在第一个参数所指的地址。这个线程ID与之前提到的不同。
先前提到的线程ID是进程调度的一部分。由于线程是轻量级的进程,是操作系统调度的基本单位,因此它需要一个唯一的数值来标识。
pthread_create函数的第一个参数指向一个虚拟内存单元,这个内存单元的地址就是新线程的线程ID,这属于NPTL线程库的范围。线程库的操作都是基于这个线程ID进行的。
NPTL线程库还提供了pthread_self函数,用于获取线程自身的ID。
对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
4.线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数中返回。这种方法不适用于主线程,因为从main函数中返回等同于调用exit。
2. 线程可以通过调用pthread_exit来结束自己。
3. 一个线程可以通过调用pthread_cancel来终止同一进程中的另一个线程。
5.线程等待
为什么需要线程等待?
已退出的线程,其空间未被释放,仍在进程地址空间内。
新创建的线程将不会复用之前退出线程的地址空间。
使用pthread_join来进行线程等待
调用该函数的线程会挂起等待,直到标识为thread的线程终止。thread线程的终止方式不同,通过pthread_join获得的终止状态也不同,具体如下:
1. 如果thread线程通过return返回,value_ptr所指向的单元中存放的是thread线程函数的返回值。
2. 如果thread线程被其他线程以pthread_cancel调用异常终止,value_ptr所指向的单元中存放的是常量PTHREAD_CANCELED。
3. 如果thread线程通过调用pthread_exit自行终止,value_ptr所指向的单元中存放的是传递给pthread_exit的参数。
4. 如果不关心thread线程的终止状态,可以向value_ptr参数传递NULL值。
6.线程分离
默认情况下,新创建的线程是可连接的。线程结束后,必须执行pthread_join操作以释放资源,否则可能导致系统资源泄露。如果不需要线程的返回值,join操作可能显得多余。在这种情况下,我们可以设置线程在退出时自动释放其资源。
使用pthread_detach进行线程分离
int pthread_detach(pthread_t thread);
也可以自己分离
pthread_detach(pthread_self());
四、Linux线程互斥
1.线程互斥的前置概念
临界资源:指多线程执行流中共享的资源。
临界区:是指在每个线程中访问临界资源的那部分代码。
互斥:确保任何时刻只有一个执行流能进入临界区访问临界资源,起到保护临界资源的作用。
原子性:指的是不可被任何调度机制中断的操作,这种操作只有完成或未完成两种状态。
2.互斥锁mutex
多个执行流同时访问同一个临界资源会带来一些问题,这个时候就需要用到锁。
2.1比如说下面这个简单的卖票场景:
四个线程同时卖100张票
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"
int t = 100;
void *tick(void *arg)
{
while(1)
{
if(t < 0)
{
break;
}
else
{
usleep(1000);
printf("线程%d卖: %d\n", pthread_self(), t);
t--;
}
}
return nullptr;
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1, nullptr, tick, nullptr);
pthread_create(&t2, nullptr, tick, nullptr);
pthread_create(&t3, nullptr, tick, nullptr);
pthread_create(&t4, nullptr, tick, nullptr);
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
return 0;
}
运行结果:
为什么会出现这样的情况?
1.if 语句判断条件为真以后,代码可以并发的切换到其他线程
2.usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
3.t-- 操作本身就不是一个原子操作
t--的汇编代码
可以看到t--是由三条汇编代码组成的,所以t--不是原子的。
要解决上述问题,需确保三个条件得到满足:
1.代码必须实现互斥,当一个线程进入临界区执行时,其他线程不得进入此临界区。
2.若多个线程同时请求执行临界区代码,而且临界区内没有线程在执行,则只能让一个线程进入该临界区。
3.若线程未在临界区内执行,则不应阻止其他线程进入临界区。
本质上就是加锁。
2.2互斥锁的接口
2.2.1初始化mutex
1.静态初始化
把mutex定义到全局区
2.使用init函数
2.2.2销毁mutex
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
2.2.2mutex加锁和解锁
成功返回0,失败返回错误码
调用 pthread_ lock 时,可能会遇到以下情况:
当互斥量处于未锁定状态时,该函数会锁定互斥量,并返回成功。
如果在调用函数时,互斥量已被其他线程锁定,或者有其他线程也在申请互斥量但未能成功竞争到,那么pthread_mutex_lock调用将会阻塞(即执行流被挂起),直到互斥量被解锁。
改进之前的代码:
加上锁
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"
int t = 100;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
void *tick(void *arg)
{
while(1)
{
pthread_mutex_lock(&mtx);
if(t < 0)
{
pthread_mutex_unlock(&mtx);
break;
}
else
{
usleep(1000);
printf("线程%d卖: %d\n", pthread_self(), t);
t--;
pthread_mutex_unlock(&mtx);
}
}
return nullptr;
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1, nullptr, tick, nullptr);
pthread_create(&t2, nullptr, tick, nullptr);
pthread_create(&t3, nullptr, tick, nullptr);
pthread_create(&t4, nullptr, tick, nullptr);
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
return 0;
}
运行结果: