Bootstrap

C语言 | 多线程:一篇搞定所有难点

目录

一、创建线程

二、线程退出和等待

2.1 线程退出

2.2 等待线程退出并回收资源 

三、线程取消

四、线程分离

五、线程间通信

5.1 信号量

5.2 生产者/消费者 

5.3 互斥锁

总结


一、创建线程

pthread_create 是 POSIX 线程(pthreads)库中的一个函数,用于创建一个新的线程。它是多线程编程中常用的函数之一,允许程序并发执行多个任务。


函数原型:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

参数说明:

  1. thread: 指向 pthread_t 类型的指针,用于存储新创建线程的唯一线程 ID

  2. attr: 指向 pthread_attr_t 类型的指针,用于设置线程的属性(如栈大小、调度策略等)。如果为 NULL,则使用默认属性

  3. start_routine: 线程启动时执行的函数指针。该函数的签名必须是 void* func(void*),即接受一个 void* 参数并返回一个 void* 值。

  4. 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

作用:

  1. 终止当前线程的执行。

  2. 释放线程占用的资源(如栈空间)。

  3. 将返回值传递给其他线程(通过 pthread_join)。

与 return 的区别:

  • return: 只能在线程函数的末尾调用,返回的值会被隐式传递给 pthread_join

  • pthread_exit: 可以在线程的任何地方调用,显式终止线程并返回一个值。


2.2 等待线程退出并回收资源 

pthread_join 用于等待指定的线程终止,并获取该线程的返回值。它是多线程编程中常用的同步机制之一,确保主线程(或其他线程)能够等待子线程完成任务后再继续执行。


函数原型:

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

参数说明:

  1. thread: 需要等待的线程的线程 ID(pthread_t 类型)。

  2. retval: 指向指针的指针,用于存储目标线程的返回值。如果不需要返回值,可以传入 NULL

返回值:

  • 如果成功,返回 0

  • 如果失败,返回一个非零的错误码(例如,目标线程已经处于分离状态)。

作用:

  1. 等待线程结束调用 pthread_join 的线程会阻塞,直到目标线程终止。

  2. 获取线程返回值:目标线程的返回值可以通过 retval 参数获取。

  3. 回收线程资源:线程终止后,其占用的资源(如栈空间)会被释放。

  4. 它是一种线程同步机制,确保调用线程(如主线程)在子线程完成之前不会继续执行。


示例代码:线程退出和回收线程资源的演示

#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

  • 如果失败,返回一个非零的错误码。

作用:

  1. 向目标线程发送取消请求。

  2. 目标线程是否立即终止取决于其取消状态和类型(见下文)(先不用关心)。


线程的取消状态和类型:

        线程的取消行为由以下两个属性决定:

  1. 取消状态

    • PTHREAD_CANCEL_ENABLE: 线程可以接收取消请求(默认状态)。

    • PTHREAD_CANCEL_DISABLE: 线程忽略取消请求。

  2. 取消类型

    • PTHREAD_CANCEL_DEFERRED: 取消请求被延迟,直到线程到达取消点(如 sleepreadwrite 等函数)(默认类型)。

    • 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

  • 如果失败,返回一个非零的错误码。

作用:

  1. 将线程设置为分离状态。

  2. 分离状态的线程在终止时会自动释放资源(如栈空间)。

  3. 分离状态的线程不能被其他线程调用 pthread_join 等待。


分离状态的特点

  1. 自动释放资源

    1. 分离的线程在终止后,系统会自动回收其资源(如栈空间)。

    2. 不需要其他线程通过 pthread_join 显式等待它。

  2. 无法获取返回值

    1. 分离的线程无法通过 pthread_join 获取其退出状态(返回值)。

    2. 如果需要返回值,必须使用非分离状态的线程。

  3. 适合后台任务:

    1. 分离的线程适合用于后台任务,如日志记录、定时任务等。

    2. 主线程不需要等待这些线程完成。

示例代码:

#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、这种行为是同步机制(如互斥锁、信号量等)的默认行为,目的是确保线程之间的正确同步。

 

;