一、基础知识

  • 计算机的核心是CPU,承担了所有的计算任务。
  • 操作系统是计算机的管理者,负责任务的调度、资源的分配和管理,统领整个计算机硬件。
  • 应用程序则是具有某种功能的程序,程序是运行于操作系统之上的。

进程:

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的。进程是一种抽象的概念,没有统一的标准定义。

进程由程序、数据集合和进程控制块三部分组成:

  • 程序:描述进程要完成的功能,是控制进程执行的指令集;
  • 数据集合:程序在执行时所需要的数据和工作区;
  • 程序控制块:(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。

线程:

线程的一些好处:(个人理解,保留质疑!)

在进程为任务调度的最小单位时,但进程遇到堵塞时,操作系统会切换其它的进程进行处理。但由于进程不仅是调度的基本单位,同时还是资源分配的独立单位,所以对进程进行切换时,开销会比较大。为了减小切换时的开销,将任务调度的最小单位这个责任交给了线程,进程依然是资源分配的单位。

线程的基本理解:

  • 是程序执行中一个单一的顺序控制流程
  • 是程序执行流的最小单元
  • 是处理器调度和分派的基本单位

一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间,不包括栈)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。

二、线程的创建

在C语言中,使用pthread_create函数创建一个线程。该函数定义在头文件pthread.h中,函数原型为:

int pthread_create(

    pthread_t *restrict tidp,

    const pthread_attr_t *restrict attr,

    void *(*start_rtn)(void *),

    void *restrict arg

  );

介绍:

  • 参数1:存储线程ID,线程的句柄,可通过该变量操纵指向的线程;
  • 参数2:线程的属性,默认且一般是NULL;
  • 参数3:一个函数用于给新创建的线程去执行;
  • 参数4:参数3中的函数的传入参数。不需要则为NULL;
  • 返回值:成功返回0,失败则返回错误编号;

另一个比较重要的函数:pthread_join()

  • 函数原型:int pthread_join(pthread_t thread,void**retval);
  • 功能:等待第一个参数的线程执行完成后,去执行retval指向的函数(起到线程同步的作用)

先开始我们C语言多线程编程的第一个小程序吧!

演示代码:

#include<stdio.h>

#include<stdlib.h>

#include<pthread.h>

void* Print(char* str)

{

printf("%s ",str);

}

int main()

{

pthread_t thread1,thread2;

pthread_create(&thread1,NULL,(void*)&Print,"Hello");

pthread_create(&thread2,NULL,(void*)&Print,"World");

return 0;

}

!在编译时,pthread_create函数会报未定义引用的错误:

在解决报错后,得到了可执行文件。但在运行时,却看不到任何输入。Why?这里涉及到条件竞争的概念了,使用pthread_create函数创建了两个线程,两个线程创建后,并不影响主线程的执行,所以这里就存在了三个线程的竞争关系了。可见,似乎主线程执行return 0;先于另外两个线程的打印函数。主线程的退出会导致创建的线程退出,所以我们看不见它们的输出。

那么,为了使return 0语句慢点执行,可以采用sleep()函数进行延迟。

可以看到有打印了输出,但有World Hello和Hello World两种情况,也是因为竞争的原因。

三、线程同步与互斥锁机制

在遇到条件竞争的问题中,上面采用sleep()函数进行延迟似乎也能解决问题。但实则不然,采用sleep()的弊端很是明显:

  • 不能判断延迟的时间长度,加上每次执行都会有所改变,更加不可控。
  • 会使程序执行卡顿,缺乏紧凑。

最适当的解决方法是采用锁机制。

互斥锁机制:

通过访问时对共享资源加锁的方法,防止多个线程同时访问共享资源。锁有两种状态:未上锁和已上锁。在访问共享资源时,进行上锁,在访问结束后,进行解锁。若在访问时,共享资源已被其它线程锁住了,则进入堵塞状态等待该线程释放锁再继续下一步的执行。这种锁我们称为互斥锁。

通过锁机制,前面的代码不难进行改变,这里将不进行描述。下面将介绍一下生产者消费者模型,为了进一步演示锁机制。

互斥锁相关函数介绍:

1、pthread_mutex_init :初始化一个互斥锁。

函数原型:int pthread_mutex_init(pthread_mutex_t*mutex,constpthread_mutexattr_t*attr);

2、pthread_mutex_lock:若所访问的资源未上锁,则进行lock,否则进入堵塞状态。

函数原型:intpthread_mutex_lock(pthread_mutex_t*mutex);

3、pthread_mutex_unlock:对互斥锁进行解锁。

函数原型:intpthread_mutex_unlock(pthread_mutex_t*mutex);

4、pthread_mutex_destroy:销毁一个互斥锁。

函数原型:intpthread_mutex_destroy(pthread_mutex_t*mutex);

生产者消费者模型:

生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中生成产品,消费者从存储空间中取走产品。当存储空间为空时,消费者阻塞;当存储空间满时,生产者阻塞。(下面代码中存储空间为1)

演示代码test02.c:

#include<stdio.h>

#include<pthread.h>

#include<stdlib.h>

int buf = 0;

pthread_mutex_t mut;

void producer()

{

while(1)

{

pthread_mutex_lock(&mut);

if(buf == 0)

{

buf = 1;

printf("produced an item.\n");

sleep(1);

}

pthread_mutex_unlock(&mut);

}

}

void consumer()

{

while(1)

{

pthread_mutex_lock(&mut);

if(buf == 1)

{

buf = 0;

printf("consumed an item.\n");

sleep(1);

}

pthread_mutex_unlock(&mut);

}

}

int main(void)

{

pthread_t thread1,thread2;

pthread_mutex_init(&mut,NULL);

pthread_create(&thread1,NULL,&producer,NULL);

consumer(&buf);

pthread_mutex_destroy(&mut);

return 0;

}

生产者消费者模型演示代码

执行结果:

从执行结果可以看出,运行顺序井然有序。生产后必是消费,消费完后必是生产。由于互斥锁机制的存在,生产者和消费者不会同时对共享资源进行访问。

四、信号量机制

上面了解到的互斥锁有两种状态:资源为0和1的状态。当我们所拥有的资源大于1时,可以采用信号量机制。在信号量机制中,我们有n个资源(n>0)。在访问资源时,若n>=1,则可以访问,同时信号量-1,否则堵塞等待直到n>=1。其实互斥锁可以看出信号量的一种特殊情况(n=1)。

信号量相关函数的介绍:

头文件:semaphore.h

1、sem_init函数:初始化一个信号量。

函数原型:int sem_init(sem_t* sem, int pshared, unsigned int value);

参数:

  • sem:指定了要初始化的信号量的地址;
  • pshared:如果其值为0,就表示信号量是当前进程的局部信号量,否则信号量就可以在多个进程间共享;
  • value:指定了信号量的初始值;

返回值:成功=>0 , 失败=> -1;

2、 sem_post函数:信号量的值加1,如果加1后值大于0:等待信号量的值变为大于0的进程或线程被唤醒。

函数原型:int sem_post(sem_t* sem);

返回值:成功=>0 , 失败=> -1;

3、sem_wait函数:信号量的减1操作。如果当前信号量的值大于0,则可继续执行。如果当前信号量的值等于0,则会堵塞,直到信号量的值大于0.

函数原型:int sem_wait(sem_t* sem);

返回值:成功=>0 , 失败=> -1;

4、sem_destroy函数:销毁一个信号量。

函数原型:int sem_destroy(sem_t* sem);

返回值:成功=>0 , 失败=> -1;

5、sem_getvalue函数:获取信号量中的值。

函数原型:int sem_getvalue(sem_t* sem, int* sval);

获取信号量的值,并放在&sval上。

#include<stdio.h>

#include<stdlib.h>

#include<pthread.h>

#include<semaphore.h>

#include<unistd.h>

sem_t npro; //还可以生产多少

sem_t ncon; //还可以消费多少

//producer function

void* producer(void* arg)

{

while(1)

{

int num;

sem_wait(&npro); //先判断是否可以生产

sem_post(&ncon); //生产一个,可消费数+1

sem_getvalue(&ncon,&num);

printf("produce one,now have %d items.\n",num);

sleep(0.7);

}

}

//consumer function

void consumer(void* arg)

{

while(1)

{

int num;

sem_wait(&ncon); //判断是否可以消费

sem_post(&npro); //消费一个,可生产数+1

sem_getvalue(&ncon,&num);

printf("consume one,now have %d items.\n",num);

sleep(1);

}

}

int main(void)

{

pthread_t thread1,thread2;

//init semaphore

sem_init(&npro,0,5); //设最大容量为5

sem_init(&ncon,0,0);

pthread_create(&thread1,NULL,&producer,NULL);

consumer(NULL);

return 0;

}

生产者消费者模型(信号量机制)

运行结果:

同样也可以解决条件竞争问题,而且使用范围更广了。

五、小结

  • 进程和线程的基础知识
  • 线程的创建以及线程存在的条件竞争问题

条件竞争的解决:

    • pthread_join()函数
    • 互斥锁机制
    • 信号量机制

以上内容若有不妥,麻烦提出(抱拳~)

参考文章:

tolele
2022-04-02

C语言 之 多线程编程的更多相关文章

  1. Rust语言的多线程编程

    我写这篇短文的时候,正值Rust1.0发布不久,严格来说这是一门兼具C语言的执行效率和Java的开发效率的强大语言,它的所有权机制竟然让你无法写出线程不安全的代码,它是一门可以用来写操作系统的系统级语 ...

  2. linux下c语言的多线程编程

    我们在写linux的服务的时候,经常会用到linux的多线程技术以提高程序性能 多线程的一些小知识: 一个应用程序可以启动若干个线程. 线程(Lightweight Process,LWP),是程序执 ...

  3. C# 语言的多线程编程,完全是本科OS里的知识

    基本知识,无参数Thread和带参数的Thread Thread类的参数就是参数指针,可以传入一个无参的函数. 如果要传入带参数的函数,先new一个ParameterizedThreadStart委托 ...

  4. linux下C语言多线程编程实例

    用一个实例.来学习linux下C语言多线程编程实例. 代码目的:通过创建两个线程来实现对一个数的递加.代码: //包含的头文件 #include <pthread.h> #include ...

  5. C语言中的多线程编程

    很久很久以前,我对C语言的了解并不是很多,我最早听说多线程编程是用Java,其实C语言也有多线程编程,而且更为简单.方便.强大.下面就让我们简单领略一下Unix C语言环境下的多线程编程吧! 下面先看 ...

  6. C语言多线程编程

    HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUT ...

  7. C语言使用pthread多线程编程(windows系统)二

    我们进行多线程编程,可以有多种选择,可以使用WindowsAPI,如果你在使用GTK,也可以使用GTK实现了的线程库,如果你想让你的程序有更多的移植性你最好是选择POSIX中的Pthread函数库,我 ...

  8. Linux C语言多线程编程实例解析

    Linux系统下的多线程遵循POSIX线程接口,称为 pthread.编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a.顺便说一下,Linux ...

  9. windows多线程编程星球(一)

    以前在学校的时候,多线程这一部分是属于那种充满好奇但是又感觉很难掌握的部分.原因嘛我觉得是这玩意儿和编程语言无关,主要和操作系统的有关,所以这部分内容主要出现在讲原理的操作系统书的某一章,看完原理是懂 ...

随机推荐

  1. tp5 (自写) 实现redis消息队列 + 排行榜

    1:小皮开启redis, 控制器按Ctrl 点击new Redis 进入 redis.php 进行封装 //向队列添加数据 // LPUSH key value1 [value2] //将一个或多个值 ...

  2. tensorflow源码解析之framework-node

    目录 什么是node node_def 关系图 涉及的文件 迭代记录 1. 什么是node TF中的计算图由节点组成,每个节点包含了一个操作,表示这个节点的作用,比如,如果一个节点的作用是做矩阵乘法, ...

  3. 华为交换机配置ACL详细步骤

    ACL 介绍 #2000-2999普通ACL,根据源IP过滤 #3000-3999高级ACL,根据源目的端口和源目的地址等过滤 #4000-4999二层ACL,根据源目的MAC等过滤 配置举例: 拒绝 ...

  4. CF498B题解

    咋黑色啊,这不是看到数据范围就去想 \(O(nT)\) 的做法吗? 然后仔细想想最靠谱的就是 DP. 设 \(dp[n][T]\) 表示听完第 \(n\) 首歌,总共听了 \(T\) 秒. 很明显有 ...

  5. 1.1 STL基本概念

    文章目录 1 STL概述 1.1 STL基本概念 1.2 STL 六大组件 1.3 STL优点 2.1 容器 2.2 算法 2.3 迭代器 2.4 示例 1 STL概述 STL是StandardTem ...

  6. 互联网前沿技术——01 找不到模块“lodash”

    检查安装 node --version 修改 安装:npm install 启动:grunt server 如果报错: 找不到模块"lodash" https://www.soin ...

  7. Intellij IDEA远程debug线上项目记录

    远程调试,特别是当你在本地开发的时候,你需要调试服务器上的程序时,远程调试就显得非常有用. JAVA 支持调试功能,本身提供了一个简单的调试工具JDB,支持设置断点及线程级的调试同时,不同的JVM通过 ...

  8. 【Linux】apt软件管理和远程登录

    镜像下载.域名解析.时间同步请点击 阿里云开源镜像站 1. apt 介绍 apt 是 Advanced Packaging Tool 的简称,是一款安装包管理工具.在 Ubuntu 下,可以使用 ap ...

  9. 4月18日 python学习总结 异常处理、网络编程

    一. 异常 1.什么是异常 异常是错误发生的信号,程序一旦出错,如果程序中还没有相应的处理机制 那么该错误就会产生一个异常抛出来,程序的运行也随之终止 2.一个异常分为三部分: 1.异常的追踪信息 2 ...

  10. Redis数据库的初步认识(二)-C/C++连接redis数据库

    1用C语言连接数据库,首先要安装c语言的数据库 在目录/redis- 4.0.1/deps下面执行sudo make/make install命令 在执行完之后可能执行ldconfig命令来更新连接符 ...