文章目录
线程以及linux中原生线程库
线程概念
什么是线程:是进程内的一个执行分支,是一个新的执行流。线程的执行粒度要比进程要细。
-
linux中线程如何理解
linux实现方案,在Linux中,线程在进程"内部"执行,线程在进程的地址空间内运行
任何执行流都要有资源
cpu只有调度执行流的概念不关心是进程还是线程
linux没有真正意义上的线程,而是用"进程"的数据结构模拟的线程
pcb也就是一个执行流
linux中的执行流,是一个轻量级进程。
-
重新定义线程和进程
线程是操作系统调度的基本单位
进程是承担系统资源的基本实体
进程内部包含线程,线程是进程内部的执行流资源
线程目前分配资源,本质就是分配地址空间范围
线程相对于进程的优势:
创建和释放更加轻量化
切换更加轻量化
线程切换的页表和地址空间都不需要切换。
线程在执行就是进程在执行。
pthread库
内核中没有线程的概念, 只有轻量级进程,不会直接给我们提供轻量级进程的系统调用
所以有了用户层的第三方库,pthread线程库,在应用层对轻量级进程接口进行封装。为用户直接提供线程的接口
Linux中编写多线程代码需要使用第三方库 pthread
1.pthread_create
pthread_create
函数是 POSIX 线程 (pthreads) 库的一部分,通常用于在类 Unix 操作系统环境中创建和管理线程。线程允许程序在同一进程内并发地执行多个任务。
以下是 pthread_create
的工作原理、参数以及一个示例:
函数原型
c
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
参数
-
*pthread_t thread : 指向
pthread_t
类型变量的指针,该变量将在成功创建线程后存储线程 ID。 -
*const pthread_attr_t attr : 指向
pthread_attr_t
结构的指针,该结构指定新线程的属性。如果此参数为NULL
,则使用默认属性。大部分情况不考虑。 -
void *(*start_routine)(void *) : 指向新线程将要执行的函数的指针。该函数应接受一个
void *
类型的参数并返回一个void *
类型的值。 -
*void arg : 传递给启动例程的单个参数。如果不需要参数,可以传递
NULL
。
返回值
- 成功时,
pthread_create
返回 0。 - 失败时,返回一个错误号码,指示错误原因。
示例:
以下是一个简单的示例,演示如何使用 pthread_create
:
c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
// 定义线程要运行的函数
void *print_message(void *ptr) {
char *message = (char *)ptr;
printf("%s\n", message);
return NULL;
}
int main() {
pthread_t thread1, thread2;
const char *message1 = "来自线程1的问候";
const char *message2 = "来自线程2的问候";
// 创建第一个线程
if (pthread_create(&thread1, NULL, print_message, (void *)message1)) {
fprintf(stderr, "创建线程1时出错\n");
return 1;
}
// 创建第二个线程
if (pthread_create(&thread2, NULL, print_message, (void *)message2)) {
fprintf(stderr, "创建线程2时出错\n");
return 1;
}
// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
在这个示例中,我们定义了一个名为 print_message
的函数,线程在创建后将运行这个函数。我们创建了两个线程,每个线程打印一条消息。pthread_join
用于等待线程完成,以确保主程序在所有线程完成之前不会退出。
线程的标识符LWP
ps -aL查看所有的轻量级进程
LWP轻量级进程的标识符。
PID = LWP是主线程
LWP是调度的基本单位
任何一个线程被杀掉进程都会被杀掉
当一个线程崩溃了,进程也就退出了。
一旦一个线程把自己的函数执行完了就退出了
线程是linux中线程库所维护的一个概念,所以线程库需要负责维护独立的栈结构。
线程库注定咬维护多个线程结构。栈在线程中私有的。所以这就是用户级的线程。
线程的tid:线程tId是在用户层维护的,LWP是操作系统层面 ,每一个线程库级别的tcb的起始地址叫做线程的tid
堆空间也是被所有线程共享的
线程栈:
每一个线程在运行的时候一定要有自己的栈结构
主线程用主结构的栈结构即可
线程和线程之间没有秘密,线程上的栈上的数据,也是可以被其他线程看到并且访问的
___thread定义一个单独属于自己的线程全局变量,这是一个编译选项,编译的时候会给每个线程的局部存储开辟一份。
线程的局部存储只能够用来定义内置类型,不能用来修饰自定义类型
除了主线程,所有其他线程的独立栈,都在共享区,具体来讲是在pthread库中,tid指向用户tcb中。
主线程或者其他线程想要访问一个线程内的数据是可以访问到的,因为他们都在同一块地址空间内
线程栈中的数据其他线程是可以看到的
全局变量是被所有线程同时看到并且访问的
虽然一个线程栈是独立的一人一份的,但是其他的线程还是可以访问到,但是我们禁止这样操作。所以我们强调独立而不是私有。
假如我们想要给每一个线程开辟一个全局变量,我们需要用到线程的局部存储,线程局部存储在每个线程的独立栈结构中。
线程的局部存储:定义在共享区,只能定义内置类型
c++
__thread int num = 100;
//局部存储只能定义内置变量
线程控制
线程等待
一个线程被创建出来了,哪个线程先运行?我们不清楚
但是最后退出的就是主线程。防止新线程造成内存泄露。创建新线程的退出结果。
main thread等待的时候,默认是阻塞等待的。
pthread_join()函数:
pthread_t thread
:要等待的线程的ID。void **retval
:指向存储线程返回值的指针。如果不需要返回值,可以传递NULL
。
c++
1. **等待其他线程结束**:当调用 `pthread_join()` 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行¹. 这对于管理多个线程的资源使用和确保代码的正确执行顺序非常重要².
2. **对线程的资源进行回收**:如果一个线程是非分离的(默认情况下创建的线程都是非分离),并且没有对该线程使用 `pthread_join()` 的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了"僵尸线程"¹.
因此,`pthread_join()` 既用于等待其他线程的终止,也用于确保线程资源的回收。如果想获取某个线程执行结束时返回的数据,也可以使用 `pthread_join()`
保证新线程先退出,主线程后退出。
线程不用考虑异常,线程一旦异常了,进程就直接退出了。
线程分离:
c++
int pthread_detach(pthread_t thread)
线程分离是一种设置,当线程被标记为分离状态后,线程结束时,其资源会被系统自动回收,而不再需要在其他线程中使用 pthread_join()
这样,你无需显式等待线程结束,系统会自动处理资源释放。如果你不关心线程的返回值,或者不想让线程继续执行,可以选择将线程设置为分离状态。
c
#include <stdio.h>
#include <pthread.h>
void* ThreadEntry(void* arg) {
// 线程执行的代码
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, ThreadEntry, NULL);
pthread_detach(tid); // 将线程设置为分离线程
// 主线程的其他操作
return 0;
}
终止线程:
c
pthread_exit(void*(100));
return (void*)100
这两种写法是等价的,都可以让进程退出。
线程取消:
cpp
int pthread_cancel(pthread_t thread)
如果一个线程是被取消的,那么这个线程的函数的返回值就是-1,就是PTHREAD_CANCELE
线程的互斥
思考一个问题:对一个全局变量进行多线程并发--/++操作是否安全?
c
/ 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
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, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}
当我们代码在执行到if判断以后可能会被操作系统切换到另一个线程中,这时的ticket的值就会改变。
第一步我们要将内存的数据mov到cpu中,第二步在cpu中进行--操作,第三步将结果mov到内存中
在这三步中的任何一个操作时,线程都会被切换。
在线程切换时,这个线程的上下文将会保存起来。
cpu内的两种运算,一种是逻辑运算,一种是算术运算。
我们的并发操作对100操作不是原子的。
怎么解决这个问题:
对于共享数据的访问,要保证任何时候只有一个执行流访问------互斥
锁的使用:
c
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex); //加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
c
void * getTicke(void* args)
{
while(true)
{
pthread_mutex_lock(td->lock);//申请锁成功才能往后执行,不成功,阻塞等待
if(tickets > 0)
{
......
pthread_mutex_unlock(td->lock);
}
else
{
pthread_mutex_unlock(td->lock);
break;
}
}
return nullptr;
}
加锁的本质是用时间来换空间
加锁的表现:线程对于临界区代码串行执行
加锁的原则:尽量的要保证临界区代码越少越好。
纯互斥环境,如果锁分配不够合理,容易导致其他线程的饥饿。不是说只要有互斥,必有饥饿
适合纯互斥的场景, 就用互斥
锁本身就是共享资源:所以申请锁和释放锁的过程本身就是原子的
线程在任何时候都可以被切换,就算再临界区内也可以线程切换,
只有我是持有锁被切换的,其他线程也不能进入临界区访问临界资源
对于其他线程来讲,一个线程要么没有锁,要么释放锁 。当前进程访问临界区的过程,对于其他线程是原子的。