多线程(一)

  • 基础知识
    • 进程
    • 线程
      • 线程的优点
  • C语言多线程
    • 创建线程
    • 终止线程
    • 连接线程
    • 分离线程
  • 开启一个线程
    • 最基本的多线程实现
  • 开启两个线程

在串口助手编程中,-k命令下需要实现等待接收message的同时可以发送键入message。但是,键入message使用的fgets()函数如果得不到键入就会一直等待,无法继续接收message,考虑采用多线程实现有键入则发送,否则一直等待接收message。

基础知识

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

进程

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

  • 进程是一个可拥有资源的独立单位;
  • 进程是一个可以独立调度和分派的基本单位。

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

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

线程

线程:进程中的一个实体,是CPU调度和分派的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。线程在运行中呈现间断性。

线程建立之初,就是为了将进程的上述两个属性分开,线程构成了“CPU调度和分派的基本单位”,这样一个进程中可以有很多线程,操作系统对线程进行调度和分派,可以更好地实现进程的并打执行;同时同一个进程下的线程可以共享该进程的全部资源,可以满足同一个进程下不同线程对进程资源的访问。线程的出现,巧妙地将进程的两个属性分开,使得进程可以更好地处理并行执行的需求。

线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲,同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

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

一个进程可以一个或多个线程,各个线程共享程序的内存空间,即所在进程的内存空间(不包括栈),每条线程并行执行不同的任务。一个标准的线程由线程ID、当前指令助阵(PC)、寄存器和堆栈组成。进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。

进程是系统进行资源分配和调度的一个独立单位,线程是进程的一个实体,是CPU调度和分派的基本单位,线程只是一段程序的执行。

比如打开了一个软件,能从任务管理器,就能看到有该软件这样一个进程,这时候想跟别人聊个天,打开对话框,这就是运行一个线程;查看一下聊天的这个人的资料,这又运行了另外一个线程。

同一个进程下的线程是资源共享的,进程与进程之间都是独立的。

线程的优点

线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在Unix系统中,一个进程包含很多东西,包括可执行程序以及一大堆的诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,那么通信就需要在用户空间和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。

  • 避免拥塞
    单个线程中的程序,是按照顺序执行的,排在前面的程序如果发生异常卡住(阻塞),会影响到后面的程序执行。多线程就等于是异步调用,避免这个情况。

  • 避免CPU的空转
    这个比如一个网页,如果是单线程的话,服务器处理一条请求后,会等待下一个请求,这时候CPU处于一个闲置的状态。多线程能避免这个问题。

  • 提升效率

C语言多线程

多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。一般情况下,两种类型的多任务处理:基于进程和基于线程。

基于进程的多任务处理是程序的并发执行。
基于线程的多任务处理是同一程序的片段的并发执行。
多线程程序包含可以同时运行的两个或多个部分。这样的程序中的每个部分称为一个线程,每个线程定义了一个单独的执行路径。

本教程假设您使用的是 Linux 操作系统,我们要使用 POSIX 编写多线程 C++ 程序。POSIX Threads 或 Pthreads 提供的 API 可在多种类 Unix POSIX 系统上可用,比如 FreeBSD、NetBSD、GNU/Linux、Mac OS X 和 Solaris。

创建线程

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

int pthread_create(    pthread_t *restrict tidp,    const pthread_attr_t *restrict attr,    void *(*start_rtn)(void *),    void *restrict arg  );

线程创建函数pthread_create包含四个变量,分别为:

  1. 一个线程变量名,被创建线程的标识,存储线程ID,线程的句柄,可通过该变量操纵指向的线程
  2. 线程的属性指针,缺省为NULL即可
  3. 被创建线程执行的函数程序代码
  4. 参数3中函数程序代码的传入参数,不需要则为NULL

返回值:成功返回0,失败返回错误编号。

创建一个POSIX 线程实例:

pthread_create (thread, attr, start_routine, arg)
参数描述
thread指向线程标识符指针。
attr一个不透明的属性对象,可以被用来设置线程属性。您可以指定线程属性对象,也可以使用默认值 NULL。
start_routine线程运行函数起始地址,一旦线程被创建就会执行。
arg运行函数的参数。它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。

终止线程

pthread_exit(void *retval); //retval用于存放线程结束的退出状态

终止一个 POSIX 线程:

#include pthread_exit (status)

pthread_exit 用于显式地退出一个线程。通常情况下,pthread_exit() 函数是在线程完成工作后无需继续存在时被调用。

如果 main() 是在它所创建的线程之前结束,并通过 pthread_exit() 退出,那么其他线程将继续执行。否则,它们将在 main() 结束时自动被终止。

连接线程

pthread_create调用成功以后,新线程和老线程谁先执行,谁后执行用户是不知道的,这一块取决与操作系统对线程的调度,如果我们需要等待指定线程结束,需要使用pthread_join函数,这个函数实际上类似与多进程编程中的waitpid。
pthread_join()函数的功能是等待一个线程的结束,它是一个线程阻塞的函数。

pthread_join有两个参数:指定要等待的线程id;接收线程函数的返回值。
函数原型为:

int pthread_join(pthread_t thread,void**retval);

等待第一个参数的线程执行完成后,去执行retval指向的函数(起到线程同步的作用)。th是要等待结束的线程的标识;指针thread_return指向的位置存放的是终止线程的返回状态。例如假设 A 线程调用 pthread_join 试图去操作B线程,该函数将A线程阻塞,直到B线程退出,当B线程退出以后,A线程会收集B线程的返回码。

#include #include #include #define NUMBER_OF_THREADS 10 void* ptintf_hello_world(void* tid);int main(void){pthread_t threads[NUMBER_OF_THREADS];int status,i;for(i=0;i<NUMBER_OF_THREADS;i++){//循环创建10个现场printf("Main here. Creating thread %d\n",i);//创建线程,线程函数传入参数为istatus=pthread_create(&threads[i],NULL,ptintf_hello_world,(void*)i);if(status!=0){//线程创建不成功,打印错误信息printf("pthread_create returned error code %d\n",status);exit(-1);}}exit(0);}void* ptintf_hello_world(void* tid){printf("Hello world %d.\n",tid);//在线程函数中打印函数的参数pthread_exit(0);}

由于我们没有在主线程中等待我们创建出来的10个线程执行完毕,所以创建出来的子线程可能还没来得及执行,就因为主线程(main函数)执行完毕而终止整个进程,导致子线程没法运行。因此printf得到的“Hello world ”不是10个,其数量是无法预知的,其顺序也是无法预知的。此时我们就需要pthread_join()函数来等待线程执行完成。

#include #include #include #define NUMBER_OF_THREADS 10 void* ptintf_hello_world(void* tid);int main(void){pthread_t threads[NUMBER_OF_THREADS];int status,i;for(i=0;i<NUMBER_OF_THREADS;i++){//循环创建10个现场printf("Main here. Creating thread %d\n",i);//创建线程,线程函数传入参数为istatus=pthread_create(&threads[i],NULL,ptintf_hello_world,(void*)i);if(status!=0){//线程创建不成功,打印错误信息printf("pthread_create returned error code %d\n",status);exit(-1);}}for(i=0;i<NUMBER_OF_THREADS;i++){pthread_join(threads[i],NULL);}exit(0);}void* ptintf_hello_world(void* tid){printf("Hello world %d.\n",tid);//在线程函数中打印函数的参数pthread_exit(0);}

此时所有子线程都执行完毕,打印了对应的“Hello world ”,但是线程执行的顺序是不固定的,也就是说,我们无法预知打印的顺序。根据代码判断程序的输出就是不可行的,我们只知道输出的内容,但是不知道输出的顺序。

除非我们在每个子线程创建之后,一直等待其运行结束,然后才开始创建下一个子线程。即,将pthread_join()函数放到紧挨着pthread_create()函数的后面

#include #include #include #define NUMBER_OF_THREADS 10 void* ptintf_hello_world(void* tid);int main(void){pthread_t threads[NUMBER_OF_THREADS];int status,i;for(i=0;i<NUMBER_OF_THREADS;i++){//循环创建10个现场printf("Main here. Creating thread %d\n",i);//创建线程,线程函数传入参数为istatus=pthread_create(&threads[i],NULL,ptintf_hello_world,(void*)i);pthread_join(threads[i],NULL);if(status!=0){//线程创建不成功,打印错误信息printf("pthread_create returned error code %d\n",status);exit(-1);}}exit(0);}void* ptintf_hello_world(void* tid){printf("Hello world %d.\n",tid);//在线程函数中打印函数的参数pthread_exit(0);}

此时,我们实际通过pthread_join()函数将多线程的并行强制为顺序执行.此时打印的输出是有序的,因为pthread_join()函数将等待对应的线程结束,线程资源被收回,才继续执行下面的命令,创建另一个线程。实际相当于并行变为了串行。

除此之外,使用pthread_join()函数还可以利用其第二个参数,得到线程函数的返回值。

#include #include #include #define NUMBER_OF_THREADS 10 void* ptintf_hello_world(void* tid);int main(void){pthread_t threads[NUMBER_OF_THREADS];int status,i;for(i=0;i<NUMBER_OF_THREADS;i++){printf("Main here. Creating thread %d\n",i);status=pthread_create(&threads[i],NULL,ptintf_hello_world,(void*)i);//使用res得到线程函数的返回值 int** res=(int**)malloc(sizeof(int*));pthread_join(threads[i],(void**)res);//pthread_join函数以阻塞的方式等待指定的线程结束;如果线程已经结束,函数会立即返回,并且指定的线程必须是joinable的 printf("res[%d]:%d\n",i,**res);//打印线程函数的返回值 free(*res); //释放线程处理函数中使用malloc分配的内存空间 if(status!=0){printf("pthread_create returned error code %d\n",status);exit(-1);}} exit(0);}void* ptintf_hello_world(void* tid){printf("Hello world %d.\n",tid);int* a=(int*)malloc(sizeof(int));*a=(int)tid*(int)tid;return a;//线程函数的返回值 }

新建一个int**类型的变量res,用来接收线程处理函数的返回值,使用pthread_join()函数得到返回值之后,printf打印出来。与此同时,在线程处理函数中,我们将函数的传入参数tid进行平方运算,运算结果作为线程处理函数的返回值。

分离线程

pthread_join (threadid, status) pthread_detach (threadid)

pthread_join() 子程序阻碍调用程序,直到指定的 threadid 线程终止为止。当创建一个线程时,它的某个属性会定义它是否是可连接的(joinable)或可分离的(detached)。只有创建时定义为可连接的线程才可以被连接。如果线程创建时被定义为可分离的,则它永远也不能被连。pthread_join() 函数来等待线程的完成。

开启一个线程

最基本的多线程实现

#include#include#includevoid* func(void *args){printf("hello\n");return NULL;}int main(){pthread_t th;pthread_create(&th, NULL, func, NULL);pthread_join(th, NULL);return 0;}

主要分为三步:

  1. 声明一个线程变量th,类型为pthread_t
  2. 使用pthread_create函数创建,第一个参数是线程变量的地址,第三个参数是线程执行的函数;
  3. pthread_join函数等待;

注意:在编译时,pthread_create函数会报未定义引用的错误
原因:pthread库不是Linux系统默认的库,而是POSIX线程库,连接时需要使用库libpthread.a, 在使用pthread_create创建线程时,在编译中要加-lpthread参数。在Linux中将其作为一个库来使用,加上-lpthread以显式链接该库。在Ubuntu Linux命令行中编译时输入:gcc xxx.c -lpthread -o xxx.o

开启两个线程

#include#include#includevoid* func(void *args){int i;for(i=1; i<500; i++){printf("%d\n", i);}return NULL;}int main(){pthread_t th1;pthread_t th2;pthread_create(&th1, NULL, func, NULL);pthread_create(&th2, NULL, func, NULL);pthread_join(th1, NULL);pthread_join(th2, NULL);return 0;}

两个线程同时执行func函数,错序打印1~499
如果用到pthread_create函数的第4个参数,这个参数的传入会反应到func中的形参中去。

void* func(void *args){int i;char *name = (char*)args;for(i=1; i<500; i++){printf("%s:%d\n", name, i);}return NULL;}int main(){pthread_t th1;pthread_t th2;pthread_create(&th1, NULL, func, "th1");pthread_create(&th2, NULL, func, "th2");pthread_join(th1, NULL);pthread_join(th2, NULL);return 0;}

输出的结果,我们可以清晰地看出th1和th2的线程标记和交错运行。