一.线程
今天我们在Linux中学习线程。
不过我们需要注意的是Linux中并不存在真正的线程,而是称为轻量级进程。
其实线程也是在进程中抽取出的一部分。
但是我们的window是真正有线程的,拥有自己对于线程的管理方法。
不过我们主要学习的是Linux对于windows对于的线程操作先不过于讨论。
为们提一下Linux自己的系统创建线程的方法以及直接创建线程的方法(用户调用方法)为用户级别。
1.clone
clone() 是 Linux 系统中一个功能强大的系统调用,用于创建新的进程,它比 fork() 提供了更精细的控制,允许父进程和子进程有选择地共享内存空间 、文件描述符表 、信号处理器表等执行上下文
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
| 参数 | 类型 | 解析 |
|---|---|---|
fn |
int (*fn)(void *) |
子进程创建后执行的函数。当其返回时,子进程终止,返回值作为退出状态码-1-8。 |
stack |
void * |
子进程的栈指针 。由于父子进程可能共享内存,必须为子进程分配独立的栈空间。在大多数架构(如x86)上栈向下增长,因此该指针通常指向分配内存区域的最高地址 -1。 |
flags |
int |
核心参数,是一个位掩码,用于控制共享行为并指定子进程退出时发送给父进程的信号(低字节)。 |
arg |
void * |
传递给 fn 函数的参数。 |
parent_tid tls child_tid |
多种 | 可选的高级参数,用于线程库实现。例如,用于在特定内存位置存储线程ID-1。 |
调用成功时,clone() 在两个进程中各返回一次,返回值不同-1:
-
在父进程中 :返回新创建子进程的进程ID (PID)。
-
在子进程中 :返回 0。
调用失败时,父进程返回 -1 ,并设置 errno 以指示错误(如无足够内存 ENOMEM 或权限不足 EPERM)
2.pthread_create
#include<pthread>
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
在将这个方法之前我们先讲一下pthread库。
---pthread:为用户级别的库,Linux系统自带的,原生线程库。但是之后被glibc收录。
| 参数 | 类型 | 解析 |
|---|---|---|
thread |
pthread_t * |
输出参数 。调用成功时,新创建的线程ID会被存储到 thread 指向的内存位置,后续的线程操作函数(如 pthread_join)会使用这个ID来引用该线程。 |
attr |
const pthread_attr_t * |
输入参数 。用于设置新线程的属性(如是否分离、栈大小、调度策略等)。如果传入 NULL,则使用默认属性创建线程-10。 |
start_routine |
void *(*)(void *) |
函数指针 。新线程创建后将开始执行的函数入口地址。该线程函数的格式是固定的:接收一个 void* 类型的参数,并返回一个 void* 类型的值-6-7。 |
arg |
void * |
输入参数 。任意类型的指针,作为参数传递给 start_routine 函数。通过这个 void*,你可以传递任意复杂的数据给新线程。 |
pthread_create 的返回值设计遵循 libc 库函数的惯例:
-
成功 :返回 0 ,并且
thread指向的内存会被设置为新线程的ID。 -
失败 :返回一个非零的错误码 ,直接指示错误原因,此时
thread指向的内容是未定义的。
3.thead
在 C 语言中,线程(thread) 的操作通常依赖于 POSIX 线程库(pthread),因为 C 语言标准库本身并不包含多线程支持
void *thread_function(void *arg);
-
参数 :
void *arg------ 一个通用指针,可以指向任意类型的数据(传地址)。 -
返回值 :
void *------ 一个通用指针,可以返回任意类型数据的地址。
本质是封装了pthread库。
有上面的知识我们就可以创建出一个简单的多线程的程序:
cpp
#include<iostream>
#include<thread>
#include<string>
#include<unistd.h>
void *routine(void *args)
{
std::string name=static_cast<const char*>(args);
while(true)
{
std::cout<<"我是新线程,我的名字是:"<<name<<std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
tid=pthread_create(&tid,nullptr,routine,(void*)"thread_1");
//最后一个参数的类型不要受局限,可以封装一个类或者一个数组,这样就可以实现多参数的传递
while(true)
{
std::cout<<"This is main"<<std::endl;
sleep(1);
}
return 0;
}
传入一个类版本:
cpp
class ThreadData{
public:
ThreadData(int a,int b,std::string name):_name(name),_a(a),_b(b){}
std::string Name()
{
return _name;
}
~ThreadData()
{
}
private:
std::string _name;
int _a;
int _b;
};
void *routine(void *args)
{
ThreadData* mage=static_cast<ThreadData*>(args);
while(true)
{
std::cout<<"我是新线程,我的名字是:"<<mage->Name()<<std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
ThreadData td(1,2,"thread_1");
tid=pthread_create(&tid,nullptr,routine,(void*)&td);
int n=pthread_join(tid,nullptr);
if(n!=0)
{
std::cout<<"joid error"<<n<<","<<strerror(n)<<std::endl;
}
std::cout<<"join success"<<n<<std::endl;
}
4.pthread_join
当我们创建出一个线程时为了防止产生向进程那样的僵尸进程我们需要对线程进行等待。
pthread_join 的作用:
等待指定线程结束,并获取其返回值。
-
主线程(或其他线程)调用
pthread_join后会阻塞,直到目标线程执行完毕 -
可以回收目标线程的资源
-
可以获取目标线程函数的返回值等同ret=routine(xxx)
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
| 参数 | 说明 |
|---|---|
thread |
要等待的线程 ID(由 pthread_create 创建时返回) |
retval |
二级指针,用于接收线程函数的返回值(不需要可传 NULL) |
| 返回值 | 含义 |
|---|---|
0 |
成功 |
ESRCH |
找不到指定的线程 |
EINVAL |
线程不可加入(如已分离) |
EDEADLK |
死锁检测(如线程等待自己)-= |
5.pthread_exit
经过线程的等待,那么我们想要退出某一线程的时候应该怎么做呢?
pthread_exit 是 POSIX 多线程编程中用于终止调用线程的核心函数
-
头文件 :
#include <pthread.h> -
函数原型 :
void pthread_exit(void *retval); -
库链接 :编译时需加
-lpthread -
返回值 :该函数调用后不返回,直接终止线程
📝 核心行为
-
返回值传递 :
retval指针指向线程退出状态,可由其他线程通过pthread_join获取 -
资源清理:自动执行清理函数和线程私有数据的析构函数
-
主线程特殊处 :主线程调用
pthread_exit仅终止自身,进程等待其他线程结束后才退出;而return或exit()会终止整个进程
不过一般来说与return等同。
6.pthread_cancel
pthread_cancel 是 POSIX 线程中用于向指定线程发送取消请求的函数,目标线程可以选择响应或忽略该请求。
-
头文件 :
#include <pthread.h> -
函数原型 :
int pthread_cancel(pthread_t thread); -
返回值:成功返回 0,失败返回错误码(如 ESRCH 线程不存在)
-
库链接 :编译时需加
-lpthread
取消线程是主线程进行调用的。
当取消线程时,等待拿到的返回结果是-1.,他的本质是PTHREAD_CANCELED这个宏。
二.线程分离
线程被等待的状态:
1.joined:线程需要被join(默认)
2.detach:线程分离(主线程不需要等待新进程)类似与分家。
1.pthread_detach
pthread_detach 用于将线程标记为分离状态 ,表示线程的资源在终止时由系统自动回收,无需其他线程调用 pthread_join 来等待和回收。
-
头文件 :
#include <pthread.h> -
函数原型 :
int pthread_detach(pthread_t thread); -
参数 :
thread- 要分离的线程 ID -
返回值:成功返回 0,失败返回错误码
-
ESRCH:线程不存在 -
EINVAL:线程已经是分离状态或已被其他线程 join
-
-
库链接 :编译时需加
-lpthread
可以主线程进行分离,也可以子线程进行分离。
2.线程的局部存储
在同一进程中的线程中的数据是共享的,但是这样就使得我们的数据没有足够的安全性。那么我们有没有办法使线程有自己的局部全局变量呢?
下面给大家介绍一下
(1)__thread
__thread 是 GCC/Clang 等编译器支持的 线程局部存储(Thread-Local Storage, TLS) 关键字。
它修饰的全局或静态变量每个线程拥有一份独立副本,一个线程修改自身副本不影响其他线程。
cpp
#include <stdio.h>
#include <pthread.h>
__thread int tls_var = 0; // 每个线程独立
void* thread_func(void* arg) {
tls_var = (int)(long)arg;
printf("Thread %ld: tls_var = %d\n", (long)arg, tls_var);
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, thread_func, (void*)1);
pthread_create(&t2, NULL, thread_func, (void*)2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
三.线程互斥
1.线程互斥的概念
互斥量(Mutex) = Mutual Exclusion(互斥)
-
就像一把锁:一次只有一个线程能获得锁
-
其他线程必须等待锁被释放
• 共享资源
• 临界资源:多线程执行流被保护的共享的资源就叫做临界资源
• 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
• 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
• 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
2.原子与非原子
拿一个我们的a++举例。
我们的编译器实际上是没有计算功能的,我们所有的计算工作全都依靠CPU来运行。
这就使得我们的a++需要经过:
- 内存数据搬入CPU寄存器
- CPU内部++
- 写回内存
这3个步骤才能完成目标。这就使得我们在执行该程序时分为了3步:
执行前,执行中,执行后。使得这串代码并不具有原子性
cpp
int ticket = 100;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 ) {
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id , ticket);
ticket--;
} else {
break;
}
}
}
int main(void )
{
pthread_t t1 , t2 , t3 , t4;
pthread_create(&t1 , NULL , route , (void*)"thread 1 ");
pthread_create(&t2 , NULL , route , (void*)"thread 2 ");
pthread_create(&t3 , NULL , route , (void*)"thread 3 ");
pthread_create(&t4 , NULL , route , (void*)"thread 4 ");
pthread_join(t1 , NULL);
pthread_join(t2 , NULL);
pthread_join(t3 , NULL);
pthread_join(t4 , NULL);
}
这样的话这段码经过轮番调度就会导致ticket负数的出现,同理if比较也是这样的。
对于对全局变量进行访问和修改的就是临界区。对于这个问题我们需要对临界区代码进行加锁和解锁。
3.加锁和解锁
#include <pthread.h>
// 静态初始化(推荐)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;(全局)
// 或动态初始化
pthread_mutex_t mutex; (局部)
pthread_mutex_init(&mutex, NULL); // NULL 表示默认属性
// 加锁
pthread_mutex_lock(&mutex);
// 访问共享资源(临界区)
shared_variable++;
// 解锁
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
cpp
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int ticket = 100;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 ) {
pthread_mutex_lock(&mutex);
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id , ticket);
ticket--;
pthread_mutex_unlock(&mutex);
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main(void )
{
pthread_t t1 , t2 , t3 , t4;
pthread_create(&t1 , NULL , route , (void*)"thread 1 ");
pthread_create(&t2 , NULL , route , (void*)"thread 2 ");
pthread_create(&t3 , NULL , route , (void*)"thread 3 ");
pthread_create(&t4 , NULL , route , (void*)"thread 4 ");
pthread_join(t1 , NULL);
pthread_join(t2 , NULL);
pthread_join(t3 , NULL);
pthread_join(t4 , NULL);
}
-
pthread_mutex_t:互斥量类型 -
pthread_mutex_lock():阻塞式加锁 -
pthread_mutex_unlock():解锁 -
配对使用:每个 lock 必须有对应的 unlock
-
保护临界区:只锁必要的代码,不要锁整个函数
-
避免死锁:注意加锁顺序,使用 RAII 管理锁
下面是需要注意的几个问题:
- 锁本身是全局的,那么锁也是共享资源那么谁来保证锁的安全?
加锁和解锁操作被设计为原子的 - 如果申请锁的时候,锁已经被别人拿走了,怎么办?
其他线程要阻塞等待
加锁之后运行效率会变低,原因是当加锁之后CPU变为串行。当我调度下去的时候我会把锁带下去,其他线程无法进来。
四.线程同步
条件变量
• 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
• 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
同步概念与竞态条件
• 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
• 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解
1.ptread_cond_操作
pthread_cond_ 系列函数用于操作条件变量(Condition Variable) 。它是一种线程同步机制,允许线程以无竞争的方式等待某个条件成立。
| 函数 | 作用 |
|---|---|
pthread_cond_init |
初始化一个条件变量。 |
pthread_cond_destroy |
销毁一个条件变量。 |
pthread_cond_wait |
等待 条件变量。调用时会原子性地解锁互斥量 并阻塞线程,直到被唤醒。被唤醒后,它会重新锁定互斥量再返回。 |
pthread_cond_timedwait |
限时等待版本。如果在指定时间内未被唤醒,则返回超时错误 (ETIMEDOUT)。 |
pthread_cond_signal |
唤醒至少一个(通常是一个)正在等待该条件变量的线程。 |
pthread_cond_broadcast |
唤醒所有正在等待该条件变量的线程。 |
等待:
cpp
pthread_mutex_lock(&mutex);
// 必须用while循环,而非if。原因:
// 1. 防止"虚假唤醒"(spurious wakeups)[citation:3][citation:7]
// 2. 防止在被唤醒后、重新加锁前,条件又被其他线程改变
while (条件不满足) {
// 此函数会原子性地:1. 解锁mutex 2. 进入等待状态
// 被唤醒后,它会重新锁定mutex再返回
pthread_cond_wait(&cond, &mutex);
}
// 现在条件满足了,可以安全地操作共享资源了
// ...
pthread_mutex_unlock(&mutex);
唤醒:
cpp
pthread_mutex_lock(&mutex);
// 修改共享变量,这可能使得等待方的条件变为真
// ...
// 发送信号(或广播)
pthread_cond_signal(&cond); // 或者 pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);
-
pthread_cond_signal: 唤醒等待队列中的至少一个线程。它更高效,适用于只需要一个线程来处理新任务的场景,比如"生产者-消费者"队列。 -
pthread_cond_broadcast: 唤醒所有正在等待的线程。适用于多个线程都在等待同一个条件改变,并且它们都应该去竞争处理的场景,比如"线程池"中所有工作线程都在等待新任务
