Linux线程

概念

线程(Thread)也被叫做轻量级进程(Lightweight Process,LWP),它是程序执行流的最小单元。如果把进程看做成一个工厂的话,那么线程就是工厂里的工人,所以一个进程里能够包含多个线程,这些线程会共享进程的大部分资源,像内存空间、文件描述符等,不过每个线程都有自己独立的程序计数器、栈空间和寄存器组。线程能够并发执行,从而提升程序的性能与响应速度。

在 Linux 系统中,线程的实现依赖于内核调度器,内核会把线程当作独立的调度单元,按照调度算法来分配 CPU 时间片。线程之间的切换开销相对较小,因为它们共享进程的资源,不用像进程切换那样进行大量的上下文切换。

线程相关函数

线程的创建

主线程先退出,子线程会被强制结束

复制代码
#include <pthread.h>

int pthread_create( pthread_t *thread), //线程ID = 无符号长整型
                    const pthread_attr_t *attr, //线程属性
                    void *(*start_routine)(void *), //线程处理函数
                    void *arg); //线程处理函数
/*参数:
pthread:传出参数,线程创建成功之后,会被设置一个合适的值
attr:线程属性,可设置为NULL
start_routine:子线程的处理函数
arg: 处理函数的参数,void*类型,在传递前需要进行类型转换。在 start_routine 函数内部,需要将其转换回原来的类型才能使用。
返回值:
  成功:0
  错误:错误码 //perror不能使用该函数打印错误信息*/

单个线程退出

复制代码
#include <pthread.h>

void pthread_exit(void *retval);

/*参数说明

retval:这是一个 void * 类型的指针,用于传递线程的返回值,必须指向全局、堆,如果不需要返回任何值,可以将其设置为 NULL
返回值
该函数没有返回值,因为调用该函数的线程会立即终止执行。*/

阻塞等待线程退出

该函数阻塞等待线程退出,能够回收线程资源,避免资源泄漏;还可以获取线程的返回值,实现线程间的数据传递

复制代码
#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

/*参数:
pthread:要回收的子线程的ID
void **retval:这是一个指向指针的指针,用于获取目标线程的返回值,不关心返回值可设置为NULL
(void* ptr;
 pthread_join(pthid,&ptr);
 指向的内存和pthread_exit参数指向地址一致)

返回值
如果函数调用成功,返回 0。
如果调用失败,返回一个非零的错误码*/

线程分离

调用该函数之后不需要 pthread_join
子线程会自动回收自己的 PCB
由于分离线程的资源会自动回收,主线程无法得知其确切的结束时间,因此在设计程序时需要考虑这一点

复制代码
#include <pthread.h>

int pthread_detach(pthread_t thread);

/*参数
  pthread_t thread:需要被设置为分离状态的线程的线程ID。

  返回值
   若函数调用成功,返回 0。
   若调用失败,返回一个非零的错误码*/

取消线程

向指定的 thread 线程发送一个取消请求,请求该线程终止执行

注意:

线程需要有取消点

线程不会在任意位置响应取消请求,它需要在特定的 "取消点" 才能响应。取消点通常是一些系统调用或库函数,这些函数在执行过程中会检查是否有取消请求,并根据线程的取消状态和类型决定是否终止线程。
系统调用作为取消点

像 write、read、printf 这类系统调用或库函数在执行时会检查取消请求,所以在线程函数中进行这些操作可以让线程有机会响应取消请求

手动设置取消点

如果线程函数中没有自然的取消点(如系统调用),可以使用 pthread_testcancel 函数手动设置取消点。

pthread_testcancel();//设置取消点

复制代码
#include <pthread.h>

int pthread_cancel(pthread_t thread);

/*参数
  pthread_t thread:要取消的线程的线程ID。

  返回值
   若函数调用成功,返回 0。
   若调用失败,返回一个非零的错误码*/

比较两个线程ID是否相等

复制代码
#include <pthread.h>

int pthread_equal(pthread_t t1, pthread_t t2);

/*参数
pthread_t t1:要进行比较的第一个线程的线程 ID。
pthread_t t2:要进行比较的第二个线程的线程 ID。
返回值
如果 t1 和 t2 代表同一个线程,函数返回一个非零值。
如果 t1 和 t2 代表不同的线程,函数返回 0。*/

strerror函数

复制代码
#include <string.h>

char *strerror(int errnum);

/*参数
int errnum:错误码
返回值:返回错误信息的字符串*/

线程的分离属性

通过属性设置线程的分离

处于分离状态的线程在结束执行后,系统会自动回收其占用的资源,因此不能再使用 pthread_join 函数等待该线程结束。

在使用线程属性对象时,要确保在不再使用时调用 pthread_attr_destroy 函数进行销毁,避免资源泄漏。

复制代码
pthread_attr_t attr;//线程属性类型 

int pthread_attr_init(pthread_attr_t* attr);//对线程属性变量的初始化

//设置线程分离属性
int pthread_attr_setdetachstate(
pthread_attr_t* attr,
int detachstate
);

/*参数:
attr : 线程属性
detachstate:
PTHREAD_CREATE_DETACHED(分离)
PTHREAD_CREATE_JOINABLE(非分离)*/

//调用创建线程函数
pthread_create(); //传入初始化好的线程属性对象attr

//释放线程资源函数
int pthread_attr_destroy(pthread_attr_t* attr);

线程同步

当多个线程同时访问和修改共享资源时,可能会导致数据不一致或程序出现错误的结果。例如,两个线程同时对一个全局变量进行加一操作,如果没有适当的同步机制,可能会导致结果不准确。

线程同步是指多个线程在执行过程中,通过协调它们的执行顺序和访问共享资源的方式,以确保程序的正确性和稳定性。

线程同步的方法

**互斥锁(Mutex):**互斥锁是一种最基本的线程同步机制。它用于保护共享资源,确保在任何时刻只有一个线程能够访问该资源。线程在访问共享资源之前,需要先获取互斥锁,访问完成后释放互斥锁。例如,在 C 语言中使用 POSIX 线程库,可以通过 pthread_mutex_t 类型来定义互斥锁,使用 pthread_mutex_lock 函数获取锁,pthread_mutex_unlock 函数释放锁。

**信号量(Semaphore):**信号量用于控制对共享资源的访问数量。它维护一个计数器,线程在访问共享资源前需要先获取信号量,如果计数器大于零,则允许线程访问,并将计数器减一;如果计数器为零,则线程需要等待。信号量可以用于实现资源池、控制并发访问数量等场景。在 C 语言中,可以使用 sem_t 类型来定义信号量,通过 sem_wait 函数获取信号量,sem_post 函数释放信号量。

条件变量(Condition Variable): 条件变量用于在线程之间进行通信,它允许线程在满足特定条件时被唤醒。通常与互斥锁一起使用,线程在等待条件满足时会释放互斥锁并进入睡眠状态,当条件满足时,其他线程可以通过唤醒操作通知等待的线程。在 C 语言的 POSIX 线程库中,使用 pthread_cond_t 类型定义条件变量,pthread_cond_wait 函数用于等待条件变量,pthread_cond_signal 或 pthread_cond_broadcast 函数用于唤醒等待的线程。

互斥量(互斥锁)

互斥锁的使用步骤

创建互斥锁: pthread_mutex_t mutex;

初始化:pthread_mutex_init(&mutex,NULL); //mutex = 1

找到线程共同操作的共享数据
加锁: 操作共享资源之前加锁,pthread_mutex_lock(&mutex); //阻塞 :mutex = 0

pthread_mutex_trylock(&mutex); // 如果锁上锁直接返回,不阻塞

XXXXXX共享数据操作 //临界区 ,越小越好

解锁: pthread_mutex_unlock(&mutex); // mutex = 1

阻塞在锁上的线程会被唤醒

**销毁:**pthread_mutex_destory(&mutex);

相关函数

初始化互斥锁

复制代码
#include <pthread.h>

int pthread_mutex_init(
pthread_mutex_t *restrict mutex, 
const pthread_mutexattr_t *restrict attr);

/*参数
pthread_mutex_t *restrict mutex:指向要初始化的互斥锁对象的指针。
const pthread_mutexattr_t *restrict attr:指向互斥锁属性对象的指针,传入 NULL 表示使用默认属性。
返回值
如果函数调用成功,返回 0。
如果调用失败,返回一个非零的错误码*/

加锁(获取互斥锁)

没有被锁上,当前线程会将这把锁锁上

被锁上了:当前线程阻塞,锁被打开之后,线程解除阻塞

复制代码
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);

/*参数
pthread_mutex_t *mutex:指向互斥锁的指针
返回值
成功:如果函数调用成功获取到互斥锁,返回值为 0。
失败:若调用过程中出现错误,会返回一个非零的错误码*/

尝试加锁

没有锁上:当前线程会被这把锁加锁

如果锁上了:不会阻塞,返回

复制代码
#include <pthread.h>

pthread_mutex_trylock(pthread_mutex_t* mutex);

/*参数
pthread_mutex_t *mutex:指向互斥锁的指针
返回值
成功:如果函数调用成功获取到互斥锁,返回值为 0。
失败:若调用过程中出现错误,会返回一个非零的错误码*/

解锁(释放互斥锁)

复制代码
#include <pthread.h>

pthread_mutex_unlock(pthread_mutex_t* mutex);

/*参数
pthread_mutex_t *mutex:指向互斥锁的指针
返回值
成功:如果函数调用成功获取到互斥锁,返回值为 0。
失败:若调用过程中出现错误,会返回一个非零的错误码*/

销毁互斥锁

复制代码
#include <pthread.h>

pthread_mutex_destory(pthread_mutex_t* mutex );

/*参数
pthread_mutex_t *mutex:指向互斥锁的指针
返回值
成功:如果函数调用成功获取到互斥锁,返回值为 0。
失败:若调用过程中出现错误,会返回一个非零的错误码*/

死锁

1.自己锁自己

复制代码
for(int i = 0;i<MAX;i++)
{
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);//造成死锁
int crt = number;
crt++;
number = crt;
printf("thread A id = %ld,number = %d\n",pthread_self(),number);
pthread_mutex_unlock(&mutex);
usleep(10);
}

2.两个或多个线程(进程)因互相等待对方释放资源而陷入无限等待的状态


造成原因:
线程 1 对共享资源 A 加锁成功 -A 锁
线程 2 对共享资源 B 加锁成功 -B 锁
线程 1 访问共享资源 B ,对 B 锁加锁 - 线程 1 阻塞在 B 锁上
线程 2 访问共享资源 A ,对 A 锁加锁 - 线程 2 阻塞在 A 锁上
解决方法:
1.让线程按照一定的顺序去访问共享资源
2.在访问其他锁的时候,需要先将自己的锁解开
3.try_lock

读写锁

读写锁允许多个线程同时对共享资源进行读访问,因为读操作通常不会修改共享资源,所以多个读操作可以并发执行,不会产生数据竞争问题。但在进行写操作时,读写锁会保证在同一时刻只有一个线程能够对共享资源进行写访问,并且在写操作进行时,不允许其他线程进行读或写操作,以确保数据的一致性。

读共享 :多个线程可以同时持有读锁,并行地读取共享资源,提高了读操作的并发性能。
写独占 :在进行写操作时,写锁会独占资源,不允许其他线程进行读或写操作,保证了写操作的原子性和数据的一致性。
读写互斥:当有线程持有写锁时,其他线程不能获取读锁或写锁;当有线程持有读锁时,其他线程不能获取写锁,但可以获取读锁。

相关函数

复制代码
pthread_rwlock_t lock;//创建读写锁

初始化读写锁

复制代码
#include <pthread.h>

int pthread_rwlock_init(
pthread_rwlock_t *restrict rwlock, 
const pthread_rwlockattr_t *restrict attr);

/*参数:
rwlock:指向要初始化的读写锁对象的指针。
attr:指向读写锁属性对象的指针,若为 NULL,则使用默认属性。
返回值:成功返回 0,失败返回错误码。*/

获取读锁

复制代码
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

/*参数:rwlock 指向要获取读锁的读写锁对象的指针。
  返回值:成功返回 0,失败返回错误码*/

获取写锁

复制代码
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

/*参数:rwlock 指向要获取读锁的读写锁对象的指针。
  返回值:成功返回 0,失败返回错误码*/

释放读锁或写锁

复制代码
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

/*参数:rwlock 指向要获取读锁的读写锁对象的指针。
  返回值:成功返回 0,失败返回错误码*/

销毁读写锁

复制代码
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

/*参数:rwlock 指向要获取读锁的读写锁对象的指针。
  返回值:成功返回 0,失败返回错误码*/

条件变量

条件变量是线程间进行通信和同步的工具,它允许一个或多个线程等待某个条件变为真。当条件不满足时,线程可以进入等待状态;当其他线程改变了共享资源的状态并使条件满足时,这些线程可以通过条件变量唤醒等待的线程。
**条件变量通常与互斥锁一起使用。**互斥锁用于保护共享资源,确保同一时间只有一个线程可以访问该资源;条件变量则用于线程之间的通信,当某个线程发现条件不满足时,它会释放互斥锁并进入等待状态,直到其他线程发出信号表示条件已满足。当收到信号后,等待的线程会重新获取互斥锁并继续执行。

相关函数

复制代码
pthread_cond_t cond ;//创建条件变量

初始化条件变量

复制代码
#include <pthread.h>

int pthread_cond_init(
pthread_cond_t *restrict cond, 
const pthread_condattr_t *restrict attr);

/*参数:
cond:指向要初始化的条件变量对象的指针。
attr:指向条件变量属性对象的指针,若为 NULL,则使用默认属性。
返回值:成功返回 0,失败返回错误码。*/

等待条件变量

复制代码
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

/*参数:
cond:指向要等待的条件变量对象的指针。
mutex:指向与条件变量配合使用的互斥锁对象的指针。
返回值:成功返回 0,失败返回错误码。
该函数会原子性地释放互斥锁并使线程进入等待状态,直到收到条件变量的信号,然后重新获取互斥锁。*/

限时等待条件变量

复制代码
#include <pthread.h>
#include <time.h>

int pthread_cond_timedwait(
pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);

/*参数:
第三个参数指向一个 struct timespec 类型的结构体指针,用于指定等待的绝对时间

返回值
成功:如果线程在超时时间内被信号唤醒,函数返回 0。
超时:如果在指定的超时时间内没有收到信号,函数返回 ETIMEDOUT。
错误:如果出现其他错误,函数返回相应的错误码*/

唤醒一个等待线程

复制代码
int pthread_cond_signal(pthread_cond_t *cond);

/*参数:cond 指向要发送信号的条件变量对象的指针。
  返回值:成功返回 0,失败返回错误码。
  该函数会唤醒一个等待在该条件变量上的线程。*/

唤醒所有等待线程

复制代码
int pthread_cond_broadcast(pthread_cond_t *cond);

/*参数:cond 指向要发送信号的条件变量对象的指针。
  返回值:成功返回 0,失败返回错误码。
  该函数会唤醒所有等待在该条件变量上的线程。*/

销毁条件变量

复制代码
int pthread_cond_destroy(pthread_cond_t *cond);

/*参数:cond 指向要销毁的条件变量对象的指针。
返回值:成功返回 0,失败返回错误码*/

举例:

使用条件变量实现生产者,消费者模型

复制代码
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

typedef struct node
{
        int data;
        struct node *next;


}Node;

//create head node
Node *head = NULL;

//create mutex
pthread_mutex_t mutex;

//create cond
pthread_cond_t cond;

void *produce(void *arg)
{
        while(1)
        {

                //create node
                Node *pnew = (Node*)malloc(sizeof(Node));
                //init node
                pnew->data = rand()%1000;

                //lock 
                pthread_mutex_lock(&mutex);

                pnew->next = head;
                head = pnew;
                printf("produce: %ld,%d\n",pthread_self(),pnew->data);

                //unlock
                pthread_mutex_unlock(&mutex);
                pthread_cond_signal(&cond);

                sleep(rand()%3);
        }

        return NULL;
}

void* customer(void *arg)
{
        while(1)
        {
                //lock
                pthread_mutex_lock(&mutex);
                if(head == NULL)
                {
                        pthread_cond_wait(&cond,&mutex);
                }
                //delete head node
                Node* pdel = head;
                head = head->next;
                printf("customer: %ld,%d\n",pthread_self(),pdel->data);
                free(pdel);
                //unlock
                pthread_mutex_unlock(&mutex);
        }
        return NULL;
}
int main()
{
        pthread_mutex_init(&mutex,NULL);
        pthread_cond_init(&cond,NULL);
        pthread_t p1,p2;

        pthread_create(&p1,NULL,produce,NULL);
        pthread_create(&p2,NULL,customer,NULL);

        pthread_join(p1,NULL);
        pthread_join(p2,NULL);

        pthread_mutex_destroy(&mutex);
        pthread_cond_destroy(&cond);

 }

信号量

复制代码
sem_t sem;//创建信号量

相关函数

初始化信号量

复制代码
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

/*参数:
sem:指向要初始化的信号量对象的指针
pshared:0-线程同步
         1-进程同步
value:信号量的初始值,它代表了可用资源的初始数量。
返回值:若初始化成功,返回 0;若失败,返回 -1*/

等待信号量

执行 P 操作,即尝试获取信号量。如果信号量的值大于 0,则将信号量的值减 1 并继续执行;如果信号量的值为 0,则调用线程会被阻塞,直到信号量的值大于 0。

复制代码
#include <semaphore.h>

int sem_wait(sem_t *sem);

/*
参数:sem 指向要操作的信号量对象的指针。
返回值:若操作成功,返回 0;若失败,返回 -1*/

非阻塞等待信号量

复制代码
#include <semaphore.h>

int sem_trywait(sem_t *sem);

/*
参数:sem 指向要操作的信号量对象的指针。
返回值:若成功获取信号量,返回 0;若信号量不可用,返回 -1 */

限时等待信号量

复制代码
#include <semaphore.h>
#include <time.h>

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

/*参数:
sem:指向要操作的信号量对象的指针。
abs_timeout:指向 struct timespec 结构体的指针
返回值:若成功获取信号量,返回 0;若超时,返回 -1 */

释放信号量

复制代码
#include <semaphore.h>

int sem_post(sem_t *sem);

/*
参数:sem 指向要操作的信号量对象的指针。
返回值:若操作成功,返回 0;若失败,返回 -1*/

销毁信号量

复制代码
#include <semaphore.h>

int sem_destroy(sem_t *sem);

/*
参数:sem 指向要操作的信号量对象的指针。
返回值:若操作成功,返回 0;若失败,返回 -1*/
相关推荐
熬夜学编程的小王29 分钟前
【Linux篇】多线程编程中的互斥与同步:深入理解锁与条件变量的应用
linux·条件变量·线程同步·线程互斥
独行soc4 小时前
2025年渗透测试面试题总结-网络安全、Web安全、渗透测试笔试总结(一)(附回答)(题目+回答)
linux·运维·服务器·安全·web安全·面试·职场和发展
前进的程序员4 小时前
Linux 驱动开发步骤及 SPI 设备驱动移植示例
linux·运维·驱动开发
cocogogogo5 小时前
配置Jupyter Notebook环境及Token认证(Linux服务器)
linux·服务器·jupyter
木心6 小时前
Linux如何安装AppImage程序
linux
小跌—6 小时前
Linux:认识基础IO
linux·运维·数据库
小米里的大麦6 小时前
007 Linux 开发工具(上)—— vim、解放sudo、gc+
linux·vim
Eric.Lee20217 小时前
ubuntu 挂载硬盘
linux·运维·ubuntu
xinruoqianqiu7 小时前
shell脚本--2
linux·运维·开发语言·前端·c++·chrome