linux操作系统:线程,令复杂的项目并行执行

为什么要有线程

其实,对于任何一个进程来讲,即使我们没有主动去创建线程,进程也是默认会有一个主线程的

  • 线程是负责执行二进制指令的,它会根据项目执行计划书(二进制文件)一行一行的执行下去。
  • 进程要比线程管的宽多了,除了执行指令之外,内存、文件系统等等都要它来管。

所以,进程相当于一个项目,而线程是为了完成项目需求,而建立的一个个开发任务

  • 默认情况下,你可以创建一个大的任务,就是完成某某功能,然后交给一个人让它从头做到尾,这就是主线程
  • 但是有时候,任务是可以拆解的,如果相关性没有非常大的前后关联关系,就可以并行执行。

那为什么我们不成立多个项目组(进程)实现并发开发呢?

  • 第一,立项设计的部门比较多,可能需要多个独立的会议室之类的
  • 第二,会有沟通问题

也就是说,使用进程并行执行有两个问题:

  • 第一,创建进程占用资源太多
  • 第二,进程之间的通信需要数据在不同的内存空间传来传去,无法共享。

所以多个线程比多个进程要好。

另外,除了希望任务能够并行执行,有的时候,你作为项目管理人员,肯定需要管控风险,因此还会预留一部分人作为应急小分队,来处理紧急的事情。

比如,主线程正在一行一行执行二进制命令,突然收到一个通知,要做一点小事情,应该停下主线程来做吗?太耽误事情了,应该创建一个单独的线程,单独处理这些事情。

在这里插入图片描述

如何创建线程

假如说,现在我们有 N 个非常大的视频需要下载,一个个下载需要的时间太长了。按照刚才的思路,我们可以拆分成 N 个任务,分给 N 个线程各自去下载。

  • 我们知道,进程的执行是需要项目执行计划书的,那线程是一个项目小组,这个小组也应该有自己的项目执行计划书,也就是一个函数。我们将要执行的子任务放在这个函数里面,比如上面的下载任务。

  • 这个函数参数是 void 类型的指针,用于接收任何类型的参数。我们就可以将要下载的文件的文件名通过这个指针传给它。

  • 当然,这里我们不是真的下载这个文件,而仅仅打印日志,并生成一个一百以内的随机数,作为下载时间返回。这样,每个子任务干活的同时在喊:“我正在下载,终于下载完了,用了多少时间"

  • 一个运行中的线程可以调用 pthread_exit 退出线程。这个函数可以传入一个参数转换为 (void *) 类型。这是线程退出的返回值。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
 
#define NUM_OF_TASKS 5
 
void *downloadfile(void *filename)
{
   printf("I am downloading the file %s!\n", (char *)filename);
   sleep(10);
   long downloadtime = rand()%100;
   printf("I finish downloading the file within %d minutes!\n", downloadtime);
   pthread_exit((void *)downloadtime);
}
 
int main(int argc, char *argv[])
{
   char files[NUM_OF_TASKS][20]={"file1.avi","file2.rmvb","file3.mp4","file4.wmv","file5.flv"};
   pthread_t threads[NUM_OF_TASKS];
   int rc;
   int t;
   int downloadtime;
 
   pthread_attr_t thread_attr;
   pthread_attr_init(&thread_attr);
   pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_JOINABLE);
 
   for(t=0;t<NUM_OF_TASKS;t++){
     printf("creating thread %d, please help me to download %s\n", t, files[t]);
     rc = pthread_create(&threads[t], &thread_attr, downloadfile, (void *)files[t]);
     if (rc){
       printf("ERROR; return code from pthread_create() is %d\n", rc);
       exit(-1);
     }
   }
 
   pthread_attr_destroy(&thread_attr);
 
   for(t=0;t<NUM_OF_TASKS;t++){
     pthread_join(threads[t],(void**)&downloadtime);
     printf("Thread %d downloads the file %s in %d minutes.\n",t,files[t],downloadtime);
   }
 
   pthread_exit(NULL);
}

接下来,我们来看主线程

  • 在这里面,列了五个文件名。接下来声明了一个数组,里面有五个 pthread_t 类型的线程对象
  • 接下来,声明一个线程属性 pthread_attr_t。我们通过 pthread_attr_init 初始化这个属性,并且设置属性 PTHREAD_CREATE_JOINABLE。这表示将来主线程程等待这个线程的结束,并获取退出时的状态。
  • 接下来是一个循环。对于每一个文件和每一个线程,可以调用 pthread_create 创建线程。一共有四个参数,第一个参数是线程对象,第二个参数是线程的属性,第三个参数是线程运行函数,第四个参数是线程运行函数的参数。主线程就是通过第四个参数,将自己的任务派给子线程。
  • 任务分配完毕,每个线程下载一个文件,接下来主线程要做的事情就是等待这些子任务完成。当一个线程退出的时候,就会发送信号给其他所有同进程的线程。有一个线程使用 pthread_join 获取这个线程退出的返回值。线程的返回值通过 pthread_join 传给主线程,这样子线程就将自己下载文件所耗费的时间,告诉给主线程。

好了,程序写完了,开始编译。多线程程序要依赖于 libpthread.so。

gcc download.c -lpthread

编译好了,执行一下,就能得到下面的结果。

# ./a.out
creating thread 0, please help me to download file1.avi
creating thread 1, please help me to download file2.rmvb
I am downloading the file file1.avi!
creating thread 2, please help me to download file3.mp4
I am downloading the file file2.rmvb!
creating thread 3, please help me to download file4.wmv
I am downloading the file file3.mp4!
creating thread 4, please help me to download file5.flv
I am downloading the file file4.wmv!
I am downloading the file file5.flv!
I finish downloading the file within 83 minutes!
I finish downloading the file within 77 minutes!
I finish downloading the file within 86 minutes!
I finish downloading the file within 15 minutes!
I finish downloading the file within 93 minutes!
Thread 0 downloads the file file1.avi in 83 minutes.
Thread 1 downloads the file file2.rmvb in 86 minutes.
Thread 2 downloads the file file3.mp4 in 77 minutes.
Thread 3 downloads the file file4.wmv in 93 minutes.
Thread 4 downloads the file file5.flv in 15 minutes.

总结:一个普通线程的创建和运行过程如下

在这里插入图片描述

线程的数据

线程可以将项目并行运行起来,加快进度,但是也带来负面影响,过程并行起来了,那数据呢?

我们把线程访问的数据分为三类:
(1)第一类是线程栈上的本地数据

  • 因为线程任务本质是一个函数,所以每个线程都有自己的栈空间
  • 栈的大小可以通过命令ultimt -a查看,默认情况下线程栈大小8192(8MB)。我们可以通过ulimit -s修改
  • 对于线程栈,可以通过下面函数修改其大小
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
  • 主线程在内核中有一个独立的栈空间,其他线程栈也拥有自己独立的栈空间。为了避免线程之间的占空间踩踏,线程栈之间还会有小块区域,用来隔离保护各自的栈空间。一旦另一个线程踏入到这个隔离区,就会引发段错误

(2)第二类数据就是在整个进程里共享的全局数据

  • 比如全局变量,虽然在不同进程中是隔离的,但是在同一个进程中是共享的。
  • 如果是同一个全局变量,两个线程同时修改,那肯定会有问题。这时就需要某些保护机制比如锁来限制不要同时被修改
  • 那线程能不能像进程一样,也有自己的私有数据呢?如果想声明一个线程级别,而非进程级别的全局变量,有没有什么办法呢?虽然咱们都是一个大组,分成小组,也应该有点隐私。

(3)这就是第三类数据,线程私有数据(Thread Specific Data),可以通过以下函数创建:

int pthread_key_create(pthread_key_t *key, void (*destructor)(void*))
  • 可以看到,创建一个 key,伴随着一个析构函数。
  • key 一旦被创建,所有线程都可以访问它,但各线程可根据自己的需要往 key 中填入不同的值,这就相当于提供了一个同名而不同值的全局变量。
  • 我们可以通过下面的函数设置 key 对应的 value。
int pthread_setspecific(pthread_key_t key, const void *value)

  • 我们还可以通过下面的函数获取 key 对应的 value。
void *pthread_getspecific(pthread_key_t key)
  • 而等到线程退出的时候,就会调用析构函数释放value

在这里插入图片描述

数据的保护

接下来,我们来看共享的数据保护问题。

我们先来看一种方式,Mutex,全称 Mutual Exclusion,中文叫互斥。顾名思义,有你没我,有我没你。它的模式就是在共享数据访问的时候,去申请加把锁,谁先拿到锁,谁就拿到了访问权限,其他人就只好在门外等着,等这个人访问结束,把锁打开,其他人再去争夺,还是遵循谁先拿到谁访问。

  • 使用 Mutex,首先要使用 pthread_mutex_init 函数初始化这个 mutex,初始化后,就可以用它来保护共享变量了。
  • pthread_mutex_lock() 就是去抢那把锁的函数,如果抢到了,就可以执行下一行程序,对共享变量进行访;如果没抢到,就被阻塞在那里等待。
  • 如果不想被阻塞,可以使用 pthread_mutex_trylock 去抢那把锁,如果抢到了,就可以执行下一行程序,对共享变量进行访问;如果没抢到,不会被阻塞,而是返回一个错误码。
  • 当共享数据访问结束了,别忘了使用 pthread_mutex_unlock 释放锁,让给其他人使用,最终调用 pthread_mutex_destroy 销毁掉这把锁。

在使用 Mutex 的时候,有个问题是如果使用 pthread_mutex_lock(),那就需要一直在那里等着。如果是 pthread_mutex_trylock(),就可以不用等着,去干点儿别的,但是我怎么知道什么时候回来再试一下,是不是轮到我了呢?能不能在轮到我的时候,通知我一下呢?

  • 这其实就是条件变量,也就是说如果没事儿,就让大家歇着,有事儿了就去通知,别让人家没事儿就来问问,浪费大家的时间

  • 但是当它接到了通知,来操作共享资源的时候,还是需要抢共享锁,因为可能很多人都收到了通知,都来访问了,所以条件变量和互斥锁是配合使用的。

总结

写多线程的程序是有套路的,下面总结了一些套路:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值