Linux系统编程@多线程编程(一)

多线程编程

涉及操作系统原理概念
  时间片
  进程状态

  上下文: 对进程来说,就是进程的执行环境,具体就是各个变量和数据,包括所有的寄存器变量、打开的文件、内存信息等。

  进程的写时复制:由于一般 fork后面都接着exec,所以,现在的 fork都在用写时复制的技术,顾名思意,就是,数据段,堆,栈,一开始并不复制,由父,子进程共享,并将这些内存设置为只读。直到父,子进程一方尝试写这些区域,则内核才为需要修改的那片内存拷贝副本。这样做可以提高 fork的效率。

  线程函数的可重入性:所谓“重入”,常见的情况是,程序执行到某个函数foo()时,收到信号,于是暂停目前正在执行的函数,转到信号处理函数,而这个信号处理函数的执行过程中,又恰恰也会进入到刚刚执行的函数foo(),这样便发生了所谓的重入。此时如果foo()能够正确的运行,而且处理完成后,之前暂停的foo()也能够正确运行,则说明它是可重入的。

  要确保函数可重入,需满足一下几个条件:

  1、不在函数内部使用静态或全局数据 
  2、不返回静态或全局数据,所有数据都由函数的调用者提供。 
  3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
  4、不调用不可重入函数。

********************************************************************

线程与进程的对比

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。

线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。

进程和线程的关系:

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。 

(3)处理机分给线程,即真正在处理机上运行的是线程。

(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。线程是指进程内的一个执行单元,也是进程内的可调度实体.

进程与线程的区别:

(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位

(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行

(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.

(4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

线程相对于进程的优势

(调度、分配:更少的生成删除时间
 进程/线程之间的切换开销(挂起正在运行的进程或线程,恢复以前挂起的进程或线程):线程切换更快,不用恢复用户地址空间
  通信机制:通信更有效(共享地址空间、不需要调用内核传递信息)

  并发性
  编码之间的原理:创建过程的复杂度,及对程序的控制力度)

 (1)易于调度。

 (2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。

 (3)开销少。创建线程比创建进程要快,所需开销很少。。

 (4)利于充分发挥多处理器的功能。通过创建多线程进程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。

进程与线程的状态对比

进程状态

 

状态:运行、阻塞、挂起阻塞、就绪、挂起就绪

状态之间的转换:

  准备就绪的进程,被CPU调度执行,变成运行态;

     运行中的进程,进行I/O请求或者不能得到所请求的资源,变成阻塞态;

     运行中的进程,进程执行完毕(或时间片已到),变成就绪态;

     将阻塞态的进程挂起,变成挂起阻塞态,当导致进程阻塞的I/O操作在用户重启进程前完成(称之为唤醒),挂起阻塞态变成挂起就绪态,当用户在I/O操作结束之前重启进程,挂起阻塞态变成阻塞态;

     将就绪(或运行)中的进程挂起,变成挂起就绪态,当该进程恢复之后,挂起就绪态变成就绪态;

线程状态

     

同步和互斥的区别:

        当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。

      所谓同步,是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

        所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

********************************************************************

线程的操作(根据生命周期的图示操作)
线程的创建

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
// compile and link with -pthread  
  thread: 指向线程标识符的指针
  attr: 设置线程的属性
  start_routine: 线程运行程序的起始地址
  arg: 运行程序的参数

  void* arg : 无法通过该参数传输大量数据

 解决方法:

  为每个线程定义一个结构,结构中包括同一线程函数所需的所有数据(易于实现多线程复用同一线程函数);

等待指定线程的结束

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

当一个进程创建的时候,一个主线程将被创建
主线程持有进程信息,主线程与新创建进程间没有显示的父子关系
函数返回时,被等线程的资源被回收(与进程不同)

一个线程不能被多个线程等待:如果这样的话第一个收到信号的线程成功返回,其他的出错ESRCH;

注意线程等待
线程的结束

#include <pthread.h>
void pthread_exit(void *retval);

线程的原子操作(例如银行取钱系统的操作)
进程中没有原子操作

线程原子操作:要么全部执行,要么全部不执行

  异步可删除(初始化时,线程默认是异步可删除的):线程可以在任一点被删除;

  同步可删除:设置可删除点,删除请求被放进队列,等线程完成一定的任务后再响应。

  不可删除:任何删除请求都被忽略

包含线程的程序的GCC编译
在编译参数中加入 -pthread

线程id跟进程id

进程id是可移植的,是unsigned int 类型.
线程id是一个非可移植性的类型,也就是说,在这个系统中,可能是unsigned int类型,在别的系统可能是long,double,或者甚至就是个结构体。
所以,为了准确起见,打印的时候,表示为十六进制的话,也就是:
printf("threadId is %x ",threadId[i]); 如我们的系统获得,pthread_t是个结构体时,我们可以把这个值直接认为是十六进制的地址值.

线程应用实例
web服务器
单线程服务器(只有一个主线程)


多线程服务器(一个主线程生成多个工作线程)


一个主线程对应一个端口,故程序一般只占有一个主线程。
线程池服务器(一个主线程创建一个线程池,从线程池中取出线程用于处理用户请求)

进程的动态性
进程、信号的生命周期


线程的时间片
课堂练习:
新线程与主线程共享内存,主线程等待停到i=0的状态,四个新线程都创建完成了
主线程可能还停留在i=0的状态。然后新线程打印出四个0.
解决方法:设置新线程不共享内存,每个都有独立的内存空间。 

原文地址:https://www.cnblogs.com/kwseeker-bolgs/p/4515262.html