目录
一、创建线程
pthread_create
是 POSIX 线程(pthreads)库中的一个函数,用于创建一个新的线程。它是多线程编程中常用的函数之一,允许程序并发执行多个任务。
函数原型:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);参数说明:
thread
: 指向pthread_t
类型的指针,用于存储新创建线程的唯一线程 ID。
attr
: 指向pthread_attr_t
类型的指针,用于设置线程的属性(如栈大小、调度策略等)。如果为NULL
,则使用默认属性。
start_routine
: 线程启动时执行的函数指针。该函数的签名必须是void* func(void*)
,即接受一个void*
参数并返回一个void*
值。
arg
: 传递给start_routine
函数的参数,类型为void*
。返回值:
如果成功创建线程,返回
0
。如果失败,返回一个非零的错误码。
作用
pthread_create
的主要作用是创建一个新的线程,并让该线程从指定的入口函数开始执行。新线程与调用线程(主线程)并发运行,共享进程的地址空间和资源。注意:
1、编译时候记得连接pthread库,-lpthread
2、主线程和子线程是分开执行的,当子线程结束后并不会在继续执行主线程的内容
示例代码:创建一个子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
// 3. 线程函数定义
// 参数:void* arg,表示传递给线程的参数
// 返回值:void*,表示线程的返回值
void *thread_func(void *arg) {
// 无限循环,线程会一直运行
while (1) {
// 打印线程运行信息,arg 是传递给线程的字符串参数
printf("thread running.... %s\n", (char *)arg);
// 线程休眠 1 秒,避免占用过多 CPU 资源
sleep(1);
}
// 线程函数返回 NULL,表示线程正常结束
return NULL;
}
// 主函数
int main(int argc, char *argv[]) {
// 1. 定义一个 pthread_t 类型的变量,用于存储线程 ID
pthread_t thread;
// 2. 创建一个新线程
// 参数:
// - &thread:指向线程 ID 的指针,用于存储新线程的 ID
// - NULL:线程属性,设置为 NULL 表示使用默认属性
// - thread_func:线程函数,指定线程要执行的函数
// - "hello":传递给线程函数的参数,这里是一个字符串
int err = pthread_create(&thread, NULL, thread_func, "hello");
// 4. 检查线程创建是否成功
// pthread_create 返回 0 表示成功,非 0 表示错误
if (err != 0)
{
// 如果创建失败,打印错误信息
// strerror(err) 将错误码转换为可读的错误信息
fprintf(stderr, "pthread_create: %s\n", strerror(err));
// 返回 -1,表示程序异常退出
return -1;
}
// 5. 主线程的无限循环
while (1) {
// 打印主线程的运行信息
printf("main.....\n");
// 主线程休眠 1 秒,避免占用过多 CPU 资源
sleep(1);
}
// 程序正常结束,返回 0
return 0;
}
程序运行流程:
a) 主线程启动,调用 pthread_create 创建一个新线程。
b) 新线程执行 thread_func,不断打印 "thread running.... hello",然后休眠 1 秒。
c) 主线程进入无限循环,不断打印 "main.....",然后休眠 1 秒。
d) 程序会一直并行运行,直到手动终止。
二、线程退出和等待
2.1 线程退出
pthread_exit
用于显式地终止当前线程。与return
语句不同,pthread_exit
可以在线程的任何地方调用,并且可以传递一个返回值给其他线程(例如通过pthread_join
获取)。
函数原型:
#include <pthread.h> void pthread_exit(void *retval);参数说明:
retval
: 线程的返回值,类型为void*
。其他线程可以通过pthread_join
获取这个值。如果不需要返回值,可以传入NULL
。作用:
终止当前线程的执行。
释放线程占用的资源(如栈空间)。
将返回值传递给其他线程(通过
pthread_join
)。与
return
的区别:
return
: 只能在线程函数的末尾调用,返回的值会被隐式传递给pthread_join
。
pthread_exit
: 可以在线程的任何地方调用,显式终止线程并返回一个值。
2.2 等待线程退出并回收资源
pthread_join
用于等待指定的线程终止,并获取该线程的返回值。它是多线程编程中常用的同步机制之一,确保主线程(或其他线程)能够等待子线程完成任务后再继续执行。
函数原型:
#include <pthread.h> int pthread_join(pthread_t thread, void **retval);参数说明:
thread
: 需要等待的线程的线程 ID(pthread_t
类型)。
retval
: 指向指针的指针,用于存储目标线程的返回值。如果不需要返回值,可以传入NULL
。返回值:
如果成功,返回
0
。如果失败,返回一个非零的错误码(例如,目标线程已经处于分离状态)。
作用:
等待线程结束:调用
pthread_join
的线程会阻塞,直到目标线程终止。获取线程返回值:目标线程的返回值可以通过
retval
参数获取。回收线程资源:线程终止后,其占用的资源(如栈空间)会被释放。
它是一种线程同步机制,确保调用线程(如主线程)在子线程完成之前不会继续执行。
示例代码:线程退出和回收线程资源的演示
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
// 3. 线程函数定义
// 参数:void* arg,表示传递给线程的参数
// 返回值:void*,表示线程的返回值
void *thread_func(void *arg)
{
// 线程运行五次
int i = 5;
while (i--)
{
// 打印线程运行信息,arg 是传递给线程的字符串参数
printf("thread running.... %s\n", (char *)arg);
// 线程休眠 1 秒,避免占用过多 CPU 资源
sleep(1);
}
// 线程退出
pthread_exit("线程退出了"); // 可以通过 pthread_join 获取 “线程退出了”
}
// 主函数
int main(int argc, char *argv[])
{
// 1.存储线程 ID
pthread_t thread;
// 2. 创建一个新线程
// 参数:
// - &thread:指向线程 ID 的指针,用于存储新线程的 ID
// - NULL:线程属性,设置为 NULL 表示使用默认属性
// - thread_func:线程函数,指定线程要执行的函数
// - "hello":传递给线程函数的参数,这里是一个字符串
int err = pthread_create(&thread, NULL, thread_func, "hello");
// 4. 检查线程创建是否成功
// pthread_create 返回 0 表示成功,非 0 表示错误
if (err != 0)
{
// 如果创建失败,打印错误信息
// strerror(err) 将错误码转换为可读的错误信息
fprintf(stderr, "pthread_create: %s\n", strerror(err));
// 返回 -1,表示程序异常退出
return -1;
}
// 5. 主线程的打印
printf("main.....\n");
// 6. 等待线程退出,并回收资源(阻塞)
void *retval = NULL;
err = pthread_join(thread,&retval); // retval 获取的就是 pthread_exit("线程退出了"); 的 线程退出了
if(err != 0)
{
fprintf(stderr,"pthread_join:%s\n",strerror(err));
return -1;
}
// 打印一下线程回收信息
printf("retval = %s\n",(char *)retval);
return 0;
}
三、线程取消
pthread_cancel
用于请求取消(终止)指定的线程。它允许一个线程向另一个线程发送取消请求,但目标线程是否立即终止取决于其取消状态和类型。
函数原型:
#include <pthread.h> int pthread_cancel(pthread_t thread);参数说明:
thread
: 需要取消的线程的线程 ID(pthread_t
类型)。返回值:
如果成功,返回
0
。如果失败,返回一个非零的错误码。
作用:
向目标线程发送取消请求。
目标线程是否立即终止取决于其取消状态和类型(见下文)(先不用关心)。
线程的取消状态和类型:
线程的取消行为由以下两个属性决定:
取消状态:
PTHREAD_CANCEL_ENABLE
: 线程可以接收取消请求(默认状态)。
PTHREAD_CANCEL_DISABLE
: 线程忽略取消请求。取消类型:
PTHREAD_CANCEL_DEFERRED
: 取消请求被延迟,直到线程到达取消点(如sleep
、read
、write
等函数)(默认类型)。
PTHREAD_CANCEL_ASYNCHRONOUS
: 线程可以在任何时候被取消(立即取消)。可以使用
pthread_setcancelstate
和pthread_setcanceltype
来设置线程的取消状态和类型。
示例代码:演示一下线程退出
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
// 3. 线程函数定义
void *thread_func(void *arg)
{
// 线程运行五次
int i = 5;
while (i--) {
printf("thread running.... %s\n", (char *)arg);
sleep(1);
}
// 线程退出
pthread_exit("线程退出了");
}
// 主函数
int main(int argc, char *argv[])
{
// 1.存储线程 ID
pthread_t thread;
// 2. 创建一个新线程
int err = pthread_create(&thread, NULL, thread_func, "hello");
// 4. 检查线程创建是否成功
if (err != 0)
{
fprintf(stderr, "pthread_create: %s\n", strerror(err));
return -1;
}
// 5. 主线程的打印
printf("main.....\n");
sleep(2);
// 6. 取消线程
err = pthread_cancel(thread);
if(err != 0)
{
fprintf(stderr,"pthread_join:%s\n",strerror(err));
return -1;
}
// 7. 等待线程退出,并回收资源
void *retval = NULL;
err = pthread_join(thread,NULL); // 线程中程退出非正常结束,所以不会有返回值,这里不要打印会报错
if(err != 0)
{
fprintf(stderr,"pthread_join:%s\n",strerror(err));
return -1;
}
// 打印一下线程回收信息
printf("retval = %s\n",(char *)retval);
return 0;
}
创建的子线程原计划是每秒打印一次,共打印五次,可是在两秒后被取消了,所以只打印了两次
四、线程分离
pthread_detach
用于将线程设置为分离状态。分离状态的线程在终止时会自动释放其资源,而不需要其他线程调用pthread_join
来回收资源。
函数原型:
#include <pthread.h> int pthread_detach(pthread_t thread);参数说明:
thread
: 需要设置为分离状态的线程的线程 ID(pthread_t
类型)。返回值:
如果成功,返回
0
。如果失败,返回一个非零的错误码。
作用:
将线程设置为分离状态。
分离状态的线程在终止时会自动释放资源(如栈空间)。
分离状态的线程不能被其他线程调用
pthread_join
等待。
分离状态的特点
自动释放资源:
分离的线程在终止后,系统会自动回收其资源(如栈空间)。
不需要其他线程通过
pthread_join
显式等待它。无法获取返回值:
分离的线程无法通过
pthread_join
获取其退出状态(返回值)。如果需要返回值,必须使用非分离状态的线程。
适合后台任务:
分离的线程适合用于后台任务,如日志记录、定时任务等。
主线程不需要等待这些线程完成。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
// 3. 线程函数定义
void *thread_func(void *arg)
{
// 无限循环,线程会一直运行
while (1) {
printf("thread running.... %s\n", (char *)arg);
sleep(1);
}
return NULL;
}
// 主函数
int main(int argc, char *argv[])
{
// 1.存储线程 ID
pthread_t thread;
// 2. 创建一个新线程
int err = pthread_create(&thread, NULL, thread_func, "hello");
// 4. 检查线程创建是否成功
if (err != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(err));
return -1;
}
// 5.线程分离
err = pthread_detach(thread);
if(err != 0)
{
fprintf(stderr,"pthread_detach:%s\n",strerror(err));
return -1;
}
// 6. 主线程
// 打印主线程的运行信息
printf("main.....\n");
sleep(6);
return 0;
}
主线程并不会等到子线程,当主线程结束后,子线程也被迫挂掉
五、线程间通信
5.1 信号量
通过全局变量,多线程之间共享同一个进程的地址空间,共享的资源叫临界资源
线程通信机制-信号量(同步)
同步:
1. 指多个任务(线程)按照约定的顺序相互配合完成一件事情,
2. 同步机制是由“信号量”决定线程是继续运行还是阻塞等待
1、线程间同步 — P / V 操作
信号量代表某一类资源,其值表示系统中该资源的数量(信号量就是一个表示有几个资源可用的整数)
信号量是受保护的变量,只能通过三种操作来访问
(1)初始化:给信号量设置初始值
头文件
#include <semaphhore.h>
函数原型
int sem_init(sem_t *sem,int pshared,unsigned int value);
参数
Sem : 获取信号量
pshared :0
Value : 初始化信号量的值
返回值
成功 : 0
失败 : -1,设置error
(2)P操作【申请资源】:
函数原型
int sem_wait(sem_t *sem);
参数
sem : 信号量
返回值
成功 : 0
失败 : -1,设置error
当资源数量 > 0 时,申请一次 => 资源数量 -1
当资源数量 = 0 时,申请不到资源,P操作阻塞直到申请到资源为止
(3)V操作【释放资源】:资源数量 + 1
函数原型
int sem_post(sem_t *sem);
参数
sem : 信号量
返回值
成功 : 0
失败 : -1,设置error
同步(信号量):按照先后顺序共同完成一件事
示例代码1:单个信号量,实现主线程输入,子线程打印
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
// 全局变量
char buf[1024];
// 信号量
sem_t sem;
// 4. 线程函数
void *thread_func(void *arg)
{
// 等待输入
// 申请资源 => 执行后 资源数量 -1
int ret = sem_wait(&sem); // 没有资源就等待(阻塞)
if (ret < 0)
{
perror("sem_wait");
// 退出线程
pthread_exit(NULL);
}
buf[strcspn(buf, "\n")] = '\0'; // 去掉换行符
printf("buf = %s\n", buf);
// 退出线程
pthread_exit(NULL);
}
int main()
{
// 1. 线程ID
pthread_t thread;
// 2.初始化信号量 0
int ret = sem_init(&sem, 0, 0);
if (ret < 0)
{
perror("sem_init");
return -1;
}
// 3.创建线程
int err = pthread_create(&thread, NULL, thread_func, NULL);
if (err != 0)
{
fprintf(stderr, "pthread_create:%s\n", strerror(err));
return -1;
}
// 5.从键盘输入数据到 buf
fgets(buf, sizeof(buf), stdin);
// 6.V操作 => 资源数量 + 1 [释放资源,信号量sem=1,上面子线程才申请得到]
ret = sem_post(&sem);
if (ret < 0)
{
perror("sem_post");
return -1;
}
// 7. 等待线程退出,并回收资源
err = pthread_join(thread, NULL);
if (err != 0)
{
fprintf(stderr, "pthread_join:%s\n", strerror(err));
return -1;
}
// 8.销毁信号量
sem_destroy(&sem);
return 0;
}
注意:输入之后才会打印,否则一直阻塞等待
5.2 生产者/消费者
生产者:产生数据的
消费者:使用数据的
先生产后消费
生产者 消费者 问题
目标:生产一次消费一次
实现:用两个信号量,一个代表生产者、一个代表消费者
操作细节: 1:初始化 生产者sem1=1(可以立即生产 P操作),消费者sem2=0
2:信号量的工作原理
2.1 生产者逻辑
资源申请:
生产者调用 sem_wait(&sem1);
如果 sem1 = 1,则可以继续执行(继续生产),sem1 - 1 => sem1 = 0
如果 sem1 = 0,则生产者阻塞,等待资源
产生资源:
例如给 count + 1
释放资源: 生产者调用 sem_post(&sem2);
将 sem2 的值 从 0 增加到 1 ,表示消费者可以消费
2.2 消费者逻辑
资源申请:
生产者调用 sem_wait(&sem2);
如果 sem2= 1,则可以继续执行(继续消费),sem2 - 1 => sem2 = 0
如果 sem2 = 0,则生产者阻塞,等待资源
产生资源:
例如打印 count 的值
释放资源: 生产者调用 sem_post(&sem2);
将 sem1 的值 从 0 增加到 1 ,表示生产者可以生产
示例代码1:两个信号量,生产一次消费一次
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
// 定义两个信号量
sem_t sem_Prod; // 生产者信号量
sem_t sem_cons; // 消费者信号量
// 定义全局变量 count
int count = 0;
// 生产者线程函数
void *producer(void *arg)
{
while (1)
{
// 1.P操作 > 生产者申请自己的信号量 [ sem_Prod - 1 ]
// sem_Prod>0 可以继续执行,并且sem1会减1,为0则阻塞
sem_wait(&sem_Prod);
// 2.生产资源 > count+1
count++;
printf("生产者: 生产资源 > count = %d\n", count);
// 3.V操作 > 释放消费者的信号量 [ sem_cons + 1 使消费者能消费(能P操作)]
sem_post(&sem_cons);
// 4.模拟生产耗时
sleep(1);
}
return NULL;
}
// 消费者线程函数
void *consumer(void *arg)
{
while (1)
{
// 1.P操作 > 生产者申请自己的信号量 [ sem_cons - 1 ]
// sem_cons>0 可以继续执行,并且sem1会减1,为0则阻塞
sem_wait(&sem_cons);
// 2.消费资源 > 打印 count 的值
printf("消费者: 消费资源 > count = %d\n", count);
// 3.V操作 > 释放生产者的信号量 [ sem_Prod + 1 使生产者能生产(能P操作)]
sem_post(&sem_Prod);
// 模拟消费耗时
sleep(1);
}
return NULL;
}
int main()
{
pthread_t prod_thread, cons_thread;
// 1.初始化信号量
sem_init(&sem_Prod, 0, 1); // 生产者信号量初始值为 1
sem_init(&sem_cons, 0, 0); // 消费者信号量初始值为 0
// 2.创建生产者和消费者线程
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);
// 3.等待线程结束(实际上不会结束,因为线程是无限循环)
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);
// 4.销毁信号量
sem_destroy(&sem_Prod);
sem_destroy(&sem_cons);
return 0;
}
这样就达到了顺序执行,解决了线程强占CPU时间片的问题,但是有更好的方法,接着往下看😎
5.3 互斥锁
1,异步(互斥):你用我不能用,我用你不能用,只能有一个访问临界资源(共享的资源叫临界资源,例如全局变量)
2,任何时刻最多只能有一个线程访问该资源
3,线程必须先获得“互斥锁”才能访问临界资源(共享资源),访问完资源后释放该锁 。如果无法获得锁,线程会阻塞直到获得得锁为止
头文件
#include <pthread.h>
第一步:初始化锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
参数
Mutex : 互斥锁
mutexattr : 属性,一般为 NULL
返回值
总是 0
第二步:申请锁
int pthread_mutex_lock(pthread_mutex_t *mutex)
第三步:释放锁
int pthread_mutex_unlock(pthread_mutex_t *mutex)
示例代码:并不会打印任何内容
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
// 全局变量(临界值)
int value = 0;
int count1,count2;
// 锁
pthread_mutex_t mutex;
// 线程函数
void *thread_func(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex); // 加锁(申请锁)
if(count1 != count2)
printf("count1 = %d, count2 = %d\n",count1,count2);
pthread_mutex_unlock(&mutex); // 解锁(释放锁)
}
// 退出线程
pthread_exit(NULL);
}
int main(int argc,char *argv[])
{
// 1.初始化锁
pthread_mutex_init(&mutex,NULL);
// 2.创建线程
pthread_t thread;
int err = pthread_create(&thread,NULL,thread_func,NULL);
if(err != 0 )
{
fprintf(stderr,"pthread_create:%s\n",strerror(err));
return -1;
}
printf("count1 和 count2 一直相等,所以线程不会打印任何内容...\n");
while(1)
{
value++;
pthread_mutex_lock(&mutex); // 加锁(申请锁)
count1 = value;
count2 = value;
pthread_mutex_unlock(&mutex); // 解锁(释放锁)
}
return 0;
}
总结
1、没有加锁之前,这样实际非常不安全,两边同步执行,很有可能会打印不一样的数据,因为他们没有先后顺序,像主线程 count1 刚+1,子线程就执行了,一直在抢占CPU资源
2、加锁后不会打印任何内容,因为顺序执行 count1 和 count2 一直相等
3、锁是用来保护临界资源(共享资源)的,锁的数量是看资源的数量,而不是看线程的数量,同时只能有一个线程访问共享资源4、当线程申请不到锁时,通常会阻塞在申请锁的位置,直到锁可用为止。
5、这种行为是同步机制(如互斥锁、信号量等)的默认行为,目的是确保线程之间的正确同步。