Linux系统编程(第七章)笔记

线程

   线程是指在单个进程内,多路并行执行的创建和管理单元。

二进制程序、进程和线程

   二进制程序是指保存在存储介质上的程序,以给定操作系统和计算机体系结构可访问的格式编译生成,可以运行但尚未开始。
   进程是操作系统对运行的二进制程序的抽象,包括:加载的二进制程序、虚拟内存、内核资源如打开的文件、关联的用户等。
   线程是进程内的执行单元,具体包括:虚拟处理器、堆栈、程序状态。

多线程

   多线程机制提供的六大好处:

  • 编程抽象
       把工作切分为多个模块,并为每个分块分配一个执行单元。
  • 并发性
       对于有多个处理器的计算机,线程提供了一种实现“真正并发”的高效方式。
  • 提高响应能力
       即使在单处理器的计算机上,在多线程中,有一个线程在忙,仍然会有其他线程接替新的任务。
  • I/O阻塞
       在多线程的进程中,单个线程可能会因I/O等待而阻塞,而其他线程可以继续执行。
  • 上下文切换
       在同一个进程中,从一个线程切换到另一个线程的代价要显著低于进程间的上下文切换。
  • 内存保存
       线程提供了一种可以共享内存,并同时利用多个执行单元的高效方式。
进程与线程

   线程的一大性能优势在于同一个进程内的线程之间的上下文切换代价很低。在Linux中,进程间切换代价并不高,而进程内切换的成本接近于0:接近进入和推出内核的代价。进程的代价不高,但是线程的代价更低。计算机体系结构对进程切换有影响,而线程不存在这个问题,因为进程切换设计把一个虚拟地址空间切换到另一个虚拟地址空间。对与线程而言不需要。
   多线程带来的低延迟和高I/O吞吐也可以通过I/O复用,非阻塞I/O和异步I/O来实现。
   Linux中的线程模型就是“1:1线程模型”(也称为内核级线程模型):每个内核线程直接转换成用户空间的线程。
   “N:1线程模型“(用户级线程模型):一个保护N个线程的进程只会映射到一个内核进程–即N:1。其缺点在于由于支持线程的内核实体只有一个,该模型无法利用多处理器。因此无法提供真正的并行性。在现代的操作系统中,这个缺点很严重,尤其是在Linux上,减少上下文切换代价带来的好处微乎其微,因为Linux支持非常低成本的上下文切换。

线程模式
“每个连接对应一个线程”

   在该模式中,每个工作单元被分配给一个线程,而该线程在该工作单元执行期间不会被分配给其他工作单元。

“事件驱动”

   在“每个连接对应一个线程”模式中大部分工作负荷时在等待,我们把这些等待操作从线程中剥离出来。转而通过发送异步I/O请求和使用I/O多路复用来管理服务器中的控制流。在这种模式下,请求处理转换成一系列异步I/O请求及其关联的回调函数。这些回调函数可能会通过I/O多路复用方式来等待,完成该操作的进程称为“事件循环”。当返回I/O请求时,事件循环会向等待的线程发送回调。

竞争

   一般而言,竞争条件是指由两个或多个线程对共享资源的非同步访问而导致错误的程序行为。共享资源可以是以下任意一种:系统硬件、内核资源或内存中的数据。后者是最常见的,称为数据竞争(data race)。竞争所发生的窗口——需要同步的代码区——称为“临界区”。竞争可以通过对临界区的同步线程访问来消除。

同步

   竞争的最根本的源头在于临界区是个窗口,在这个窗口内,正确的程序行为要求线程不要交叉执行。为了阻止竞争条件,程序员需要在这个窗口内执行同步访问操作,确保对临界区以互斥的方式访问。
   在计算机科学中,如果一个操作(或一组操作)不可分割,我们就称该操作是原子性的(atomic),不能和其他操作交叉。对于系统的其他部分而言,原子操作看起来是瞬间发生的,而这正是临界区的问题:这些区域不是不可分割,也不是瞬间发生的,它们不是原子的。

互斥

   实现临界区原子性访问的技术有很多种:从单一指令解决方案到大块的代码段。最常见的技术是锁(lock),锁机制可以保证临界区的互斥,使得对临界区的操作具备原子性。由于锁支持互斥,在Pthreads(以及其他地方)中称之为“互斥(mutexes)”。
   计算机的“锁”和现实世界的锁的工作机制类似:假设房间是个临界区。如果没有锁,人们(线程)就可以在房间里(临界区)随意来来去去。在特定情况下,同一时刻房间里可以有多个人。因此,我们给房间安上门并锁上门。我们给这扇门发个钥匙。当有人(线程)来到门前时,他们发现钥匙在外面,就拿钥匙开门,进到房间里,然后从里面锁上门。不会再有其他人进来。他们可以在房间内做自己的事情,而不会被打扰。没有其他人会同时占用该房间,它是个互斥的资源。当这个人不需要房间时,打开门出去,把钥匙留在外面。可能会有下一个人进来,并锁上门,这样不断重复。
   锁在线程机制下的工作方式很类似。程序员定义锁,并确保在进入临界区之前获取该锁。锁的实现机制确保一次只能持有一个锁。如果有另一个线程使用锁,新的线程在继续之前必须等待。如果不再在临界区,就释放锁,让等待线程(如果有的话)持有锁并继续执行。

死锁

   死锁是指两个线程都在等待另一个线程结束,因此两个线程都不能结束。在互斥场景下,两个线程都在等待对方持有的互斥对象。另一个场景是当某个线程被阻塞了,等待自己已经持有的互斥体。调试死锁往往很需要技巧,因为程序本身并没有崩溃。相反,它只是不再向前执行,因为越来越多的线程都在等待锁,而这一天却永远也不会来。
   避免死锁很重要,要想持续、安全地做到这一点,唯有从一开始的设计中就为多线程程序设计好锁的机制。互斥体应该和数据关联,而不是和代码关联,从而有清晰的数据层。

Pthreads

   Linux内核只为线程的支持提供了底层原语,比如clone()系统调用。POSIX线程,或简称为Pthreads。Pthreads是UNIX系统上C和C++语言的主要线程解决方案。
   Pthreads标准是一堆文字描述。在Linux中,该标准的实现是通过glibc提供的,即Linux的C库。随着时间推移,glibc提供了两个不同的Pthreads实现机制:LinuxThreads和NPTL。
   LinuxThreads是Linux原始的Pthread实现,提供1:1的线程机制。它的第一版被包含在glibc的2.0版本中,虽然只是作为外部库提供。LinuxThreads在设计上是为了内核设计的,它提供非常少的线程支持:和创建一个新的线程的clone()系统调用不同。LinuxThreads通过已有的UNIX接口实现了POSIX线程机制。举个例子,LinuxThreads通过信号实现线程到线程的通信机制(参见第10章)。由于缺乏对Pthreads的内核支持,LinuxThreads需要“管理员”线程来协调各种操作,当线程数很大时会导致可扩展性很差,而且对于POSIX标准的兼容也不太完善。
   本地POSIX线程库(NPTL)比LinuxThreads要优越,依然是标准的LinuxPthread实现机制。NPTL是在Linux 2.6和glibc2.3中引入的。类似于LinuxThreads,NPTL基于clone()系统调用和内核模型提供了1:1的线程模式,线程和其他任何进程没有太大区别,除了它们共享特性资源。和LinuxThreads不同,NPTL突出了内核2.6新增的额外内核接口,包括用于线程同步的futex()系统调用,以及exit_group()系统调用,用于终止进程中的所有线程,内核支持线程本地存储(TLS)模式。NPTL解决了LinuxThreads的非一致性问题,极大提升了线程的兼容性,支持在单个进程中创建几千个线程,而且不会变慢。

Pthread API
pthread_create()

   虽然Pthreads是由glibc提供的,但它在独立库libpthread中,因此需要显式链接。

gcc -Wall -Werror =pthread beard.c -o beard

   Pthreads提供了函数pthread_create()来定义和启动新的线程:

#include<pthread.h>

int pthread_create(pthread_t *thread,
                   const pthread_attr_t *arr,
                   void *(*start_routine)(void *),
                   void *arg);

   调用成功时,会创建新的线程,开始执行start_routine提供的函数,可以给该函数传递一个参数arg。函数会保存线程ID,用于表示新的线程。

  • thread指向的pthread_t结构体中,保存了线程ID。
  • attr指向的pthread_attr_t对象是用于改变新创建线程的默认线程属性。绝大多数pthread_create()调用会传递NULL给attr,采用默认属性。线程属性支持程序改变线程的各个方面,比如栈大小、调度参数以及初始分离状态。
  • start_routine是线程执行函数,接受void指针作为参数,返回值也是一个void指针。arg可以给该函数传递一个参数
       线程ID类似进程ID,但是进程ID(PID)是由Linux内核分配的,而TID是由Pthread库分配的。线程可以在运行是通过pthread_self()函数来获取自己的TID,可以用pthread_equal()来比较线程ID。
       线程终止的情况:
  • 如果线程在启动时返回,该线程就结束。这和main()函数结束有点类似;
  • 如果线程调用了pthread_exit()函数,它就会终止,这和调用exit()返回类似
  • 如果线程是被另一个线程通过pthread_cancel()函数取消,它就会终止。这和通过kill()发送SIGKILL信号类似。
       进程终止的情况:
  • 进程从main()函数中返回;
  • 进程通过exit()函数终止;
  • 进程通过execve()函数执行新的二进制镜像;
pthread_join()

   join线程支持等待一个线程阻塞,等待另一个线程终止:

#include<pthread.h>

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

   成功调用时,调用线程会被阻塞,直到由thread指定的线程终止。一旦线程终止,调用线程就会醒来,如果retval值不为NULL,被等待线程传递给pthread_exit()函数的值或其运行函数退出时的返回值会被放到retval中。

pthread_detach()

   默认情况下,线程是创建成可join的。但是,线程也可以detach(分离),使得线程不可join。因为线程在被join之前占有的系统资源不会被释放,正如进程消耗系统资源那样,直到其父进程调用wait(),不想join的线程应该调用pthread_detach进行detach。

#include<pthread.h>

int pthread_detach(pthread_t thread);
pthread_mutex_t

   加锁操作:

#include<pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);

   解锁操作:

#include<pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值