LInux高级编程 - 线程(Threads)

2023-05-16

http://www.91linux.com/html/article/program/cpp/20090105/15374.html

ALP Chapter 4 线程(Threads)

  • 线程可以简单理解成为进程的下级。一个系统可以有多个进程,一个进程内部可以有多个线程。
  • 回想上一章讲过的新进程的创建。先是fork,相当于拷贝了一个新的进程,然后调用exec,我们便有了两个毫不相关的进程。线程不一样,当创建一个新的线程时,它和原来的线程是完全共享内存的。如果该线程修改了一个全局变量,则其他所有的线程读到的该变量的值都是修改后的。如果该线程调用了exec,很不幸的,它的所有其他线程“兄弟”都会被终结。
4.1 线程的创建
  • 每个进程都有一个进程id,类似的,每个线程也有一个线程id,在C/C++里面,表示线程id的数据类型是pthread_t。
  • 线程函数(thread function)是一个类型为void* (*) (void*)的函数,线程被创建之后,就执行该函数内的代码。当该函数返回时,线程即结束。\
  • 创建线程的函数时pthread_create,这个函数有以下四个参数:
    • 一个指向pthread_t变量的指针,新创建的线程的id将保存在这里
    • 一个指向线程属性(thread attribute)的指针
    • 一个指向线程函数的指针void* (*) (void*)
    • 一个类型为void*的指针,指向传递给线程函数的参数
  • wait用来等待一个进程的结束,类似的作用于线程的函数名字叫做pthread_join。它有两个参数,等待的线程id(pthread_t)以及该线程的返回值(void*)。
  • pthread_self()返回当前的线程id,pthread_equal()可以用来比较两个线程id。这两个函数相当有用,它们通常在调用pthread_join之前被调用,因为一个线程如果pthread_join自己的话很显然是会产生死锁的。(事实上也是如此,pthread_join会返回EDEADLCK的错误)
  • 创建线程属性(Process attribute)的步骤:
    • 创建一个pthread_attr_t类型的变量
    • 调用pthread_attr_init,参数为上一步创建的变量的指针
    • 修改pthread_attr_t,使之包含我们需要的属性
    • 将pthread_attr_t传入pthread_create
    • 完成之后调用pthread_attr_destroy,参数为第一步创建的变量的指针。
  • 对于大部分的程序(所谓的“大部分的”之外的那些就是一些实时程序),我们用的上的线程属性就是它的分离状态(detach state)。
    • 一个线程可以是可加入的(joinable thread)或者是可分离的(detached thread)。可加入的线程在结束后,如果没有别的线程对其使用pthread_join,则它会挂起并等待,直到有线程调用 pthread_join,它的资源才会被释放。可分离的线程结束后,它的资源可被立即回收,别的线程无法通过pthread_join来保证和它的同步,或者获得它的返回值。
    • 我们可以通过函数pthread_attr_setdetachstate来设置一个线程的分离状态。
    • 即使一个线程创建的时候是可加入的,我们也可以调用pthread_detach来把它设为可分离的。设为可分离的线程不能再重新设为可加入的。
4.2 线程的扼杀(Thread Cancellation)
  • 一个线程结束自己的方法有两种:从线程函数返回,或者调用pthread_exit,可惜这两种都不是我们这里要讨论的对象。除此之外,线程还可以要求另一个线程中止,称为扼杀(canceling)一个线程(这就是为什么我不把cancellation翻译成取消的原因)。
  • 扼杀一个线程只要调用pthread_cancel即可。如果那个线程是可加入的,你可以先加入之,杀掉,然后释放它的资源(这里的资源指的是线程属性)
  • 随便杀人是不对的,随便杀线程也是不对的。如果一个线程自己申请了一些资源,然后莫名其妙被杀了,那些资源就不能释放了。我们不能阻止别人的杀人行为,但可以增强自己的防御能力,线程也可以增强自己的防御能力,它可以把自己声明为以下三种状态之一:
    • 异步可杀的(asynchronously cancelable):线程可以在任何时间被杀。
    • 同步可杀的(synchronously cancelable):线程可以被杀,但它不会马上咽气,它会坚持继续做自己的事情(例如电视中的说一大堆无用而煽情的对白),然后在到达某一个特定的位置之后(比如在说到凶手名字之前那一霎那),再死掉。我们称这些特定的位置为扼杀点(cancellation points)。
    • 无敌的(uncancelable):免疫一切暗杀魔法,反正就是杀不掉。
    • 线程创建后的默认状态是同步可杀的。
  • 我们可以使用pthread_setcanceltype来将线程的扼杀状态设为上面三种的一种。对于扼杀点的创建,函数 pthread_testcancel可以创建,这个函数除了创建扼杀点之外不做任何事情。另外,一些其他函数的内部实现可能会创建扼杀点,所以调用这些函数也会相当于创建了一个扼杀点。可以参考pthread_cancel的man page来查看这些函数的列表。
  • 线程保护自己的更为强大的一种手段是pthread_setcancelstate函数,该函数可以禁止/激活扼杀模式。在禁止扼杀模式的情况下,我们可以放心的写一些critical section的代码。需要注意的是,在离开critical section后,应立即恢复到原来的扼杀模式。
4.3 线程副本数据(Thread-Specific Data)
  • 我们知道,对于全局变量,所有线程是共享的。对于局部变量,各个线程是独享的。介于这两者之间的是线程副本变量(thread-specific variable),每个线程都能够访问这些变量,不同的是每个线程都有各自唯一的一份拷贝。
  • 线程副本变量的读取/修改只能通过特定的函数接口来实现
    • 每个变量都必须有一个对应的key,key的类型是pthread_key_t,可以通过pthread_key_create来创建。 pthread_key_create的第一个参数就是一个pthread_key_t的指针,第二个参数是一个清理函数。该函数会在线程结束的时候被调用(即使该线程是被扼杀的)。
    • 有了key之后就可以通过pthread_setspecific和pthread_getspecific来set/get线程副本变量了。
  • 线程副本数据的清理函数非常有用,用户甚至会专门把一些变量声明为副本数据来使得他们的清理函数可以自动被调用。事实上这样做是多余的,因为我们有一套独立的清理函数机制:
    • 清理函数和前面的一样,是void * (*) (void*)类型的
    • 用pthread_clearnup_push函数来注册清理函数,该函数的第二个参数将会做为清理函数的参数
    • 用pthread_clearnup_pop来注销已注册的清理函数,该函数有一个参数,如果该参数不为0,则在注销清理函数之前,会先调用该清理函数。
  • 在C++中,大家通常习惯于使用析构函数(destructor)来释放资源,因为普遍的观点是析构函数总是会被调用。其实这是错误的观点,当一个线程调用了pthread_exit,C++并不保证所有在栈(Stack)上的变量的析构函数都会被调用!一个聪明的办法是,我们不直接调用pthread_exit,而是抛出一个异常,在该异常的处理函数里面调用pthread_exit

4.4 同步(Synchronization)和关键部分(Critical Sections)

  • 这两个概念出现的地方实在是太多太多了,看得眼皮都起茧了。原理这里就不说了,我们还是集中看看linux是怎么实现这两个概念的吧。
  • Mutex的使用
    • 类型为pthread_mutex_t
    • 初始化的时候调用pthread_mutex_init函数,第一个参数为pthread_mutex_t *,第二个参数为mutex的attribute的指针,类似于thread的初始化,我们可以直接指定NULL。
    • 如果觉得pthread_mutex_init太繁琐,并且你只想使用默认属性的mutex,则把mutex赋值为PTHREAD_MUTEX_INITIALIZER就可以了。
    • 对应的加锁和解锁的函数为pthread_mutex_lock和pthread_mutex_unlock,参数都是pthread_mutex_t *
  • Mutex的类型
    • fast mutex(缺省):如果被同一个线程lock一次以上,就会有deadlock
    • recursive mutex:可以被同一个线程多次lock,但是记住必须调用同样次数的pthread_mutex_unlock
    • error-checking mutex:如果同一个线程由于进行了一次以上的lock而造成死锁的话,那么第二次以及第二此之后的lock会返回EDEADLK的错误值
    • 指定Mutex类型的代码:
      pthread_mutexattr_t attr;
      pthread_mutex_t mutex;
      pthread_mutexattr_init (&attr);
      // recursive mutex则使用PTHREAD_MUTEX_RECURSIVE_NP
      pthread_mutexattr_setkind_np (&attr, PTHREAD_MUTEX_ERRORCHECK_NP);
      pthread_mutex_init (&mutex, &attr);
      pthread_mutexattr_destroy (&attr);
  • block版本的lock函数pthread_mutex_lock无疑是低效的,非block版本的lock函数是pthread_mutex_trylock
  • Semaphore的使用
    • Semaphore的类型是sem_t
    • 初始化的函数是sem_init,第一个参数是sem_t的指针,第二个参数是0(非0代表该semaphore可以被多个进程共享,而目前的GNU不支持这个feature),第三个参数是sem_t的初始值
    • 操作函数分别是sem_wait和sem_post(有些书上也把post叫做signal)
  • 条件变量(Condition Variables)
    • 条件变量也是一种实现同步的机制,两个相关的操作是wait和signal。
    • 条件变量没有计数器,也不占内存。因此wait必须在signal之前,如果signal的时候没有正在wait的thread,则signal丢失。
    • 每个条件变量通常都和一个mutex联合使用
  • 条件变量的使用
    • 条件变量的类型是pthread_cond_t
    • 初始化的函数是pthread_cond_init,第一个参数是pthread_cond_t的指针,第二个参数是条件变量的属性的指针。
    • 操作函数分别是pthread_cond_wait和pthread_cond_signal。这里需要注意的是 pthread_cond_wait需要的第二个参数是一个mutex,并且在调用wait之前该mutex必须是被lock的,wait函数会自动 unlock该mutex,并且在wait成功后自动继续lock该mutex。

4.5 GNU/Linux中线程的实现

  • 这里之所以要特别提出实现问题是因为GNU/Linux对POSIX线程的实现不同于其他Unix平台。
    • 当我们调用pthread_create来创建线程的时候,系统创建的是一个新的进程,并且在该进程内创建我们需要的线程。
    • 这个新创建的进程和用fork创建出来的进程不同,他和原来的进程是共享内存的,而不是像fork出来的那样是做了一份copy的。
    • 其实在第一次调用pthread_create的时候,系统还会创建一个进程,叫做管理进程(manager thread),这牵涉到GNU/Linux的内部实现问题,我们就不展开了。
    • 因此,如果我们有一个程序./pthread-pid,该程序只是调用pthread_create来创建一个新的线程,并且使用无限循环来保证主线程和新的线程都不结束。执行该程序后,我们使用ps来查看,就会发现三个./pthread-pid进程,一个是我们执行的,一个是 pthread_create创建出来的,另一个就是管理进程。
  • Signal的处理
    • Signal发送的单位是进程,恰好GNU/Linux里面线程也是用进程实现的,因此在向一个多线程的程序发送Signal的时候,不会出现无法识别该由哪个线程来负责接受Signal的问题。
    • 在一个多线程程序中,线程之间也可以发送Signal,方法是pthread_kill函数。第一个参数是线程ID,第二个参数是signal number
  • 我们前面提到pthread_create会创建一个和当前进程共享内存的新进程,而fork会创建一个获得当前进程copy的进程。那么pthread_create是怎么做到的?
    • 其实有一个函数是pthread_create和fork的统一形式,该函数叫clone,clone可以指定在新的进程和旧的进程之间有哪些东西是共享的,哪些东西是copy的。
    • 这里提出clone只是为了满足大家的好奇心罢了,一般来说,在我们的程序中应该尽量避免使用这个函数。

4.6 进程Vs. 线程

  • 这是一个永恒的话题,新手们总是弄不清楚在一个需要多任务并发的程序中应该使用进程还是线程。这里列出了他们之间的区别:
    • 一个程序中的所有线程必须执行同一个可执行文件。一个新的子进程可以去执行和父进程不同的可执行文件(fork接exec是通常的做法)。
    • 在线程中,一个老鼠屎坏了一锅汤,如果一个线程出现了问题,其他所有的线程都会收到影响,因为他们共享内存。而进程彼此间独立,不存在这样的问题。
    • 创建一个进程比创建一个线程的代价要大,因为需要额外的内存拷贝的操作。虽然在现在绝大多数操作系统中,都实现了copy-on-write的技术,但进程的创建还是要比线程更费时一些。
    • 如果一个多任务的程序的各个任务是紧耦合的,或者说几乎相同的,线程将是更好的选择。如果各个任务直接没什么紧密的联系,那使用进程会更恰当一些。
    • 在线程之间共享数据是非常容易的,因为他们本身就是内存共享的。而在进程之共享数据要麻烦的多,还要使用一种叫做IPC的机制(我们下章会详细描述),但是进程之间的数据共享对于死锁等同步问题具有更强的抵抗能力。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

LInux高级编程 - 线程(Threads) 的相关文章

  • 如何将 elf 解释器(ld-linux.so.2/ld-2.17.so)构建为静态库?

    如果我的问题不准确 我深表歉意 因为我没有太多 Linux 相关经验 我目前正在构建一个 Linux 从头开始 主要遵循 linuxfromscratch org 版本的指南 7 3 我遇到了以下问题 当我构建可执行文件时 获取一个称为 E
  • 了解 Linux oom-killer 日志

    我的应用程序被 oom killer 杀死了 它是在实时 USB 上运行的 Ubuntu 11 10 无需交换 PC 具有 1 Gig 的 RAM 唯一运行的应用程序 除了所有内置的 Ubuntu 东西 是我的程序 flasherav 请注
  • 套接字发送调用被阻塞很长时间

    我每 10 秒在套接字上发送 2 个字节的应用程序数据 阻塞 但发送调用在下面的最后一个实例中被阻塞超过 40 秒 2012 06 13 12 02 46 653417 信息 发送前 2012 06 13 12 02 46 653457 信
  • 亚马逊 Linux - 安装 openjdk-debuginfo?

    我试图使用jstack在 ec2 实例上amazon linux 所以我安装了openjdk devel包裹 sudo yum install java 1 7 0 openjdk devel x86 64 但是 jstack 引发了异常j
  • 如何回忆上一个 bash 命令的参数?

    Bash 有没有办法回忆上一个命令的参数 我通常这样做vi file c其次是gcc file c Bash 有没有办法回忆上一个命令的参数 您可以使用 or 调用上一个命令的最后一个参数 Also Alt can be used to r
  • 隐式声明“gets”

    据我所知 隐式声明 通常意味着该函数必须在调用之前放置在程序的顶部 或者我需要声明原型 然而 gets应该在stdio h文件 我已包含 有没有什么办法解决这一问题 include
  • 如果输入被重定向则执行操作

    我想知道如果我的输入被重定向 我应该如何在 C 程序中执行操作 例如 假设我有已编译的程序 prog 并且我将输入 input txt 重定向到它 我这样做 prog lt input txt 我如何在代码中检测到这一点 一般来说 您无法判
  • 为什么无论 -rdynamic 如何,backtrace 都不包含 Objective-C 符号?

    Update 我正在 Linux 上使用 GNU 运行时 问题是not发生在带有 Apple 运行时的 MacOS 上 更新2 我在 MacOS 上编译了 GNU 运行时并用它构建了示例 该错误确实not发生在带有 GNU 运行时的 Mac
  • Linux下单个目录下文件过多会怎样?

    如果一个目录中有大约 1 000 000 个单独的文件 大部分大小为 100k 其中没有其他目录和文件 是否会以任何其他可能的方式降低效率或产生缺点 ARG MAX 会对此提出异议 例如 rm rf 在目录中时 会说 参数太多 想要执行某种
  • 为什么使用signalfd无法捕获SIGSEGV?

    我的系统是ubuntu 12 04 我将示例修改为man 2 signalfd 并添加sigaddset mask SIGSEGV 在示例中 但我无法得到输出SIGSEGV被生成 这是一个错误吗glibc 源代码片段如下 sigemptys
  • Linux >2.6.33:可以使用 sendfile() 来实现更快的“猫”吗?

    必须将大量大文件连接成一个更大的单个文件 我们目前使用 cat file1 file2 output file but are wondering whether it could be done faster than with that
  • C++ Linux GCC 应用程序中的 GUID

    我有很多服务器运行这个 Linux 应用程序 我希望他们能够生成一个碰撞概率较低的 GUID 我确信我可以从 dev urandom 中提取 128 个字节 这可能没问题 但是有没有一种简单易用的方法来生成与 Win32 更等效的 GUID
  • 安装 JDK 时出错:keytool 命令需要已安装的 proc fs (/proc)。 Linux 的 Windows 子系统

    我尝试在 Linux 的 Windows 子系统 Ubuntu 14 04 上安装 Oracle JDK 1 7 但出现以下错误 the keytool command requires a mounted proc fs proc Jav
  • 我可以在 Ubuntu 上使用 Homebrew 吗?

    我只是尝试使用 Homebrew 和 Linuxbrew 在我的 Ubuntu 服务器上安装软件包 但都失败了 这就是我尝试安装它们的方法 sudo apt get install build essential curl git m4 r
  • 用于 e NetworkManager VPN 连接的 dbus 信号处理程序

    我需要开发一些在建立 VPN 连接时执行的 python 代码 VPN 由 NetworkManager 控制 我试图弄清楚如何为此使用 NM DBUS 事件 使用 dbus monitor system 我能够识别连接信号 signal
  • 在 MacOS 上构建需要 net461 的 dotnet SDK 项目的最简单方法

    我有一个 dotnet SDK sln and a build proj with
  • 删除 Python 中某些操作的 root 权限

    在我的 Python 脚本中 我执行了一些需要 root 权限的操作 我还创建并写入文件 我不想由 root 独占所有 而是由运行我的脚本的用户独占所有 通常 我使用以下命令运行脚本sudo 有办法做到上述吗 您可以使用以下方式在 uid
  • PHP 日志文件颜色

    我正在编写一个 PHP 日志文件类 但我想为写入文件的行添加颜色 我遇到的问题是颜色也会改变终端的颜色 我想要实现的是仅更改写入日志文件的行的颜色 class logClass extends Singleton private funct
  • 使用 hcitool 扫描低功耗蓝牙?

    当我运行此命令时 BLE 设备扫描仅持续 5 秒 sudo timeout 5s hcitool i hci0 lescan 输出显示在终端屏幕中 但是 当我将输出重定向到文件以保存广告设备的地址时 每次运行该命令时 我都会发现该文件是空的
  • php56 - CentOS - Remi 仓库

    我刚刚在测试盒上安装了 php 5 6 正常的 cli php 解释器似乎不存在 gt php v bash php command not found gt php56 v PHP 5 6 13 cli built Sep 3 2015

随机推荐