概念
线程(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*/