C语言实现多线程

简介: C语言实现多线程

一.多线程

1.什么是线程

要了解线程,首先需要知道进程。一个进程指的是一个正在执行的应用程序。线程对应的英文名称为“thread”,它的功能是执行应用程序中的某个具体任务,比如一段程序、一个函数等。

线程和进程之间的关系,类似于工厂和工人之间的关系,进程好比是工厂,线程就如同工厂中的工人。一个工厂可以容纳多个工人,工厂负责为所有工人提供必要的资源(电力、产品原料、食堂、厕所等),所有工人共享这些资源,每个工人负责完成一项具体的任务,他们相互配合,共同保证整个工厂的平稳运行。

每个进程执行前,操作系统都会为其分配所需的资源,包括要执行的程序代码、数据、内存空间、文件资源等。一个进程至少包含 1 个线程,可以包含多个线程,所有线程共享进程的资源,各个线程也可以拥有属于自己的私有资源。

进程仅负责为各个线程提供所需的资源,真正执行任务的是线程,而不是进程。

2.什么是多线程

所谓多线程,即一个进程中拥有多(≥2)个线程,线程之间相互协作、共同执行一个应用程序。

当进程中仅包含 1 个执行程序指令的线程时,该线程又称“主线程”,这样的进程称为“单线程进程”。

二.多线程的相关函数

1.pthread_create() 函数

该函数用来创建线程,pthread_create() 函数声明在<pthread.h>头文件中,或者说我们接下来使用的多线程相关函数都声明在<pthread.h>头文件中。

该函数的详细使用方法可以通过CSDN技能树、菜鸟教程等地方学习,这里主要介绍学习时比较难以理解的地方。

该函数的第一个参数 pthread_t *thread:传递一个 pthread_t 类型的指针变量,也可以直接传递某个 pthread_t 类型变量的地址。**pthread_t 是一种用于表示线程的数据类型,每一个 pthread_t 类型的变量都可以表示一个线程。**例如 int 是一种表示整数的数据类型,每个 int 类型的变量都可以表示一个整数,它们都是数据类型的一种。

该函数的第三个参数是正在创建的该线程需要执行的函数,需要注意的是这里是以函数指针的方式指明新建线程需要执行的函数,该函数的形参和返回值都必须为 void* 类型。void* 类型又称空指针类型,表明指针所指数据的类型是未知的(如果不理解,类比于结构体指针一样理解)。使用此类型指针时,我们通常需要先对其进行强制类型转换,然后才能正常访问指针指向的数据。

如果成功创建线程,pthread_create() 函数返回数字 0,反之返回非零值。各个非零值都对应着不同的宏,指明创建失败的原因,这里可以自己去了解。

2.pthread_exit()函数

该函数用来终止线程执行。多线程程序中,终止线程执行的方式本来有 3 种,分别是:

  • 线程执行完成后,自行终止;
  • 线程执行过程中遇到了 pthread_exit() 或者 return,也会终止执行;
  • 线程执行过程中,接收到其它线程发送的“终止执行”的信号,然后终止执行。

第一种的理解就是什么也不管,线程执行完会自己终止;第二种就是本部分要用的pthread_exit()函数,return也好理解,返回即终止;第三种方法,本来要使用pthread_cancel()函数,但在使用这个函数时会出现其他一系列的问题,解决起来非常麻烦,所以除非特殊情况,我们一般使用第二种方式。


补充:pthread_exit()和return的区别:首先,return 语句和 pthread_exit() 函数的含义不同,return 的含义是返回,它不仅可以用于线程执行的函数,普通函数也可以使用;pthread_exit() 函数的含义是线程退出,它专门用于结束某个线程的执行。实际使用中,我们终止子线程一般都使用pthread_exit()函数,不建议使用return。

3.pthread_join() 函数

该函数用来获取某个线程执行结束时返回的数据,使用也比较简单,学习一下就会使用,这里不解释。

但需要注意的有一点:一个线程执行结束的返回值只能由一个 pthread_join() 函数获取,当有多个线程调用 pthread_join() 函数获取同一个线程的执行结果时,哪个线程最先执行 pthread_join() 函数,执行结果就由那个线程获得,其它线程的 pthread_join() 函数都将执行失败。

三.线程同步

1.缘由

多线程程序中各个线程除了可以使用自己的私有资源(局部变量、函数形参等)外,还可以共享全局变量、静态变量、堆内存、打开的文件等资源。我们通常将“多个线程同时访问某一公共资源”的现象称为“线程间产生了资源竞争”或者“线程间抢夺公共资源”,线程间竞争资源往往会导致程序的运行结果出现异常,我们常常采用同步机制来解决这种问题。

2.实现方法

实现线程同步的常用方法有 4 种,分别称为互斥锁信号量条件变量读写锁

互斥锁(Mutex)又称互斥量或者互斥体,是最简单也最有效地一种线程同步机制。互斥锁的用法和实际生活中的锁非常类似,当一个线程访问公共资源时,会及时地“锁上”该资源,阻止其它线程访问;访问结束后再进行“解锁”操作,将该资源让给其它线程访问。


信号量又称“信号灯”,主要用于控制同时访问公共资源的线程数量,当线程数量控制在 ≤1 时,该信号量又称二元信号量,功能和互斥锁非常类似;当线程数量控制在 N(≥2)个时,该信号量又称多元信号量,指的是同一时刻最多只能有 N 个线程访问该资源。


条件变量的功能类似于实际生活中的门,门有“打开”和“关闭”两种状态,分别对应条件变量的“成立”状态和“不成立”状态。当条件变量“不成立”时,任何线程都无法访问资源,只能等待条件变量成立;一旦条件变量成立,所有等待的线程都会恢复执行,访问目标资源。为了防止各个线程竞争资源,条件变量总是和互斥锁搭配使用。


多线程程序中,如果大多数线程都是对公共资源执行读取操作,仅有少量的线程对公共资源进行修改,这种情况下可以使用读写锁解决线程同步问题。

这里我们使用最简单的也是最常用的方法:互斥锁。

3.互斥锁的用法

3.1互斥锁的初始化

POSIX 标准规定,用 pthread_mutex_t 类型的变量来表示一个互斥锁,该类型以结构体的形式定义在<pthread.h>头文件中。

初始化 pthread_mutex_t 变量的方式有两种,分别为:

//1、使用特定的宏
pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;
//2、调用初始化的函数
pthread_mutex_t myMutex;
pthread_mutex_init(&myMutex , NULL);

3.2互斥锁的“加锁”和“解锁”

对于互斥锁的“加锁”和“解锁”操作,常用的函数有以下 3 种:

int pthread_mutex_lock(pthread_mutex_t* mutex);   //实现加锁
int pthread_mutex_trylock(pthread_mutex_t* mutex);  //实现加锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);   //实现解锁

参数 mutex 表示我们要操控的互斥锁。函数执行成功时返回数字 0,否则返回非零数。

四.线程死锁

实现线程同步的 4 种方法,分别是互斥锁、信号量、条件变量和读写锁。很多初学者在使用这些方法的过程中,经常会发生“线程一直被阻塞”的情况,我们习惯将这种情况称为“死锁”。线程死锁指的是线程需要使用的公共资源一直被其它线程占用,导致该线程一直处于“阻塞”状态,无法继续执行。

使用互斥锁、信号量、条件变量和读写锁实现线程同步时,要注意以下几点:

  • 占用互斥锁的线程,执行完成前必须及时解锁;
  • 通过 sem_wait() 函数占用信号量资源的线程,执行完成前必须调用 sem_post() 函数及时释放;
  • 当线程因 pthread_cond_wait() 函数被阻塞时,一定要保证有其它线程唤醒此线程;
  • 无论线程占用的是读锁还是写锁,都必须及时解锁。

注意,函数中可以设置多种结束执行的路径,但无论线程选择哪个路径结束执行,都要保证能够将占用的资源释放掉。

避免线程死锁也有许多方法,比如最经典的银行家算法,后面会写一篇博客单独用代码实现这一算法。

银行家算法

五.代码演示

5.1线程的基本结构

#include <stdio.h>
#include <pthread.h>
//定义线程要执行的函数,arg 为接收线程传递过来的数据
void *Thread1(void *arg)                        
{
    printf("CSDN@终究还是散了\n");
    return "Thread1成功执行";
}
//定义线程要执行的函数,arg 为接收线程传递过来的数据
void* Thread2(void* arg)
{
    printf("博客园@挽留岁月挽留你\n");
    return "Thread2成功执行";
}
int main()
{
    int res;
    pthread_t mythread1, mythread2;
    void* thread_result;
    /*创建线程
    &mythread:要创建的线程
    NULL:不修改新建线程的任何属性
    ThreadFun:新建线程要执行的任务
    NULL:不传递给 ThreadFun() 函数任何参数
    返回值 res 为 0 表示线程创建成功,反之则创建失败。
    */
    res = pthread_create(&mythread1, NULL, Thread1, NULL);
    if (res != 0) {
        printf("线程创建失败");
        return 0;
    }
    res = pthread_create(&mythread2, NULL, Thread2, NULL);
    if (res != 0) {
        printf("线程创建失败");
        return 0;
    }
    /*
    等待指定线程执行完毕
    mtThread:指定等待的线程
    &thead_result:接收 ThreadFun() 函数的返回值,或者接收 pthread_exit() 函数指定的值
    返回值 res 为 0 表示函数执行成功,反之则执行失败。
    */
    res = pthread_join(mythread1, &thread_result);
    //输出线程执行完毕后返回的数据
    printf("%s\n", (char*)thread_result);
    res = pthread_join(mythread2, &thread_result);
    printf("%s\n", (char*)thread_result);
    printf("主线程执行完毕");
    return 0;
}

5.2线程同步:卖票问题

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
int ticket_sum = 10;
//创建互斥锁
pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;
//模拟售票员卖票
void *sell_ticket(void *arg) {
    //输出当前执行函数的线程 ID
    printf("当前线程ID:%u\n", pthread_self());
    int i;
    int islock = 0;
    for (i = 0; i < 10; i++)
    {
        //当前线程“加锁”
        islock = pthread_mutex_lock(&myMutex);
        //如果“加锁”成功,执行如下代码
        if (islock == 0) {
            //如果票数 >0 ,开始卖票
            if (ticket_sum > 0)
            {
                sleep(1);
                printf("%u 卖第 %d 张票\n", pthread_self(), 10 - ticket_sum + 1);
                ticket_sum--;
            }
            //当前线程模拟完卖票过程,执行“解锁”操作
            pthread_mutex_unlock(&myMutex);
        }
    }
    return 0;
}
int main() {
    int flag;
    int i;
    void *ans;
    //创建 4 个线程,模拟 4 个售票员
    pthread_t tids[4]={1,2,3,4};
    for (i = 0; i < 4; i++)
    {
        flag = pthread_create(&tids[i], NULL, &sell_ticket, NULL);
        if (flag != 0) {
            printf("线程创建失败!");
            return 0;
        }
    }
    sleep(10);   //等待 4 个线程执行完成
    for (i = 0; i < 4; i++)
    {
        //阻塞主线程,确认 4 个线程执行完成
        flag = pthread_join(tids[i], &ans);
        if (flag != 0) {
            printf("tid=%d 等待失败!", tids[i]);
            return 0;
        }
    }
    return 0;
}

六.运行结果

c1c37408ae634e7d2ee95239c71cdb50.jpg

07051c903bbf9e7bc4d80e16c1e80c43.jpg

相关文章
|
4月前
|
安全 Java C语言
C语言线程解池解读和实现01
C语言线程解池解读和实现01
|
3月前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。
|
3月前
|
存储 Ubuntu Linux
C语言 多线程编程(1) 初识线程和条件变量
本文档详细介绍了多线程的概念、相关命令及线程的操作方法。首先解释了线程的定义及其与进程的关系,接着对比了线程与进程的区别。随后介绍了如何在 Linux 系统中使用 `pidstat`、`top` 和 `ps` 命令查看线程信息。文档还探讨了多进程和多线程模式各自的优缺点及适用场景,并详细讲解了如何使用 POSIX 线程库创建、退出、等待和取消线程。此外,还介绍了线程分离的概念和方法,并提供了多个示例代码帮助理解。最后,深入探讨了线程间的通讯机制、互斥锁和条件变量的使用,通过具体示例展示了如何实现生产者与消费者的同步模型。
|
3月前
|
C语言
C语言 网络编程(九)并发的UDP服务端 以线程完成功能
这是一个基于UDP协议的客户端和服务端程序,其中服务端采用多线程并发处理客户端请求。客户端通过UDP向服务端发送登录请求,并根据登录结果与服务端的新子线程进行后续交互。服务端在主线程中接收客户端请求并创建新线程处理登录验证及后续通信,子线程创建新的套接字并与客户端进行数据交换。该程序展示了如何利用线程和UDP实现简单的并发服务器架构。
|
4月前
|
C语言
【C语言】线程同步
【C语言】线程同步
40 3
|
4月前
|
C语言
【C语言】多线程服务器
【C语言】多线程服务器
35 0
|
4月前
|
程序员 C语言
【C语言】多线程
【C语言】多线程
29 0
|
5月前
|
调度 C语言
深入浅出:C语言线程以及线程锁
线程锁的基本思想是,只有一个线程能持有锁,其他试图获取锁的线程将被阻塞,直到锁被释放。这样,锁就确保了在任何时刻,只有一个线程能够访问临界区(即需要保护的代码段或数据),从而保证了数据的完整性和一致性。 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含一个或多个线程,而每个线程都有自己的指令指针和寄存器状态,它们共享进程的资源,如内存空间、文件句柄和网络连接等。 线程锁的概念
184 1
|
6月前
|
存储 Linux C语言
c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。
|
7月前
|
安全 Linux 编译器
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(下)
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)
50 0
下一篇
无影云桌面