Linux系统编程之多线程【线程概述】

一、进程与线程
二、Linux上线程开发API概要
三、与线程自身相关API
四、与互斥锁相关API
五、与条件变量相关API
六、线程条件控制(Thread Condition Control)

一、进程与线程

进程(Process)和线程(Thread)的区别

进程(Process)和线程(Thread)都是计算机科学中关于执行程序的概念,它们之间的主要区别在于作用范围和资源共享。下面是用简单的语言解释它们的区别:

  1. 进程

    • 进程是独立的执行单元。每个进程都有自己的内存空间、文件句柄、系统资源等。
    • 进程之间相互隔离,一个进程的崩溃不会影响其他进程。
    • 进程通常较重,需要较多的系统资源,如内存和CPU时间。
    • 进程之间通信相对复杂,通常需要使用进程间通信(IPC)机制,如管道、套接字等。
  2. 线程

    • 线程是进程内的小任务单元,多个线程共享相同的进程内存和资源。
    • 线程之间相对轻量,它们共享进程的内存和上下文。
    • 线程通常更快地创建和销毁,因为它们共享进程资源。
    • 线程之间通信相对容易,因为它们共享相同的内存空间,可以通过共享变量等方式进行通信。

总之,进程和线程的主要区别在于它们的作用范围和资源共享方式。进程是独立的执行环境,相互隔离,而线程是在同一进程内运行的轻量任务单元,共享相同的资源。线程通常用于提高多核处理器上的并发性,而进程用于实现更强的隔离和资源分配。

进程和线程各自具有一些优点和缺点

进程和线程各自具有一些优点和缺点,它们适用于不同的情况和应用。以下是进程和线程的主要优点和缺点:

进程的优点:

  1. 隔离性:进程是独立的执行单元,进程之间互相隔离。如果一个进程崩溃,不会影响其他进程的稳定性,因此系统更加稳定。

  2. 资源独立性:每个进程有自己独立的内存空间、文件句柄和系统资源。这使得进程之间的资源冲突更少。

  3. 并行性:不同进程可以并行执行,尤其是在多核处理器上。这可以提高系统的性能。

  4. 强通信隔离:进程之间的通信通常需要显式的进程间通信机制,这可以确保数据的完整性和安全性。

进程的缺点:

  1. 资源消耗:每个进程都占用独立的内存空间和系统资源,因此进程通常较重,占用更多的系统资源。

  2. 创建销毁开销:创建和销毁进程需要较多的时间和开销,因此进程管理开销较大。

  3. 通信复杂性:进程之间的通信较复杂,通常需要使用IPC机制,如管道、套接字等。

线程的优点:

  1. 资源共享:线程在同一进程内共享相同的内存空间和资源,因此线程之间的资源共享更加容易。

  2. 轻量级:线程通常更轻量,创建和销毁开销较小。多个线程可以在同一进程内并行执行。

  3. 速度:线程之间的切换速度较快,因为它们共享相同的地址空间。

  4. 通信简单:线程之间通信相对简单,因为它们可以直接访问共享内存。

线程的缺点:

  1. 隔离性差:由于线程共享内存,因此线程之间的隔离较差。如果一个线程访问了无效的内存地址,可能会影响其他线程的稳定性。

  2. 并发性问题:线程之间的并发性可能导致竞争条件和死锁等问题,需要小心处理。

  3. 复杂性:线程的并发性问题和共享资源管理可能使编程和调试更加复杂。

综合而言,进程和线程各自有其优势和限制,选择哪个取决于具体的应用需求。通常,线程适用于需要轻量级和快速切换的并发操作,而进程适用于需要高度隔离和稳定性的应用。在实际应用中,可能会同时使用进程和线程来实现特定的功能。

对比

典型的UNIX/Linux进程可以看成只有一个控制线程:一个进程在同一时刻只做一件事情。有了多个控制线程后,在程序设计时可以把进程设计成在同一时刻做不止一件事,每个线程各自处理独立的任务。

进程是程序执行时的一个实例,是担当分配系统资源(CPU时间、内存等)的基本单位。在面向线程设计的系统中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程包含了表示进程内执行环境必须的信息,其中包括进程中表示线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno常量以及线程私有数据。进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本、程序的全局内存和堆内存、栈以及文件描述符。在Unix和类Unix操作系统中线程也被称为轻量级进程(lightweight processes),但轻量级进程更多指的是内核线程(kernel thread),而把用户线程(user thread)称为线程。

“进程——资源分配的最小单位,线程——程序执行的最小单位”

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

从函数调用上来说,进程创建使用fork()操作;线程创建使用clone()操作。Richard Stevens大师这样说过:

  • fork is expensive. Memory is copied from the parent to the child, all descriptors are duplicated in the child, and so on. Current implementations use a technique called copy-on-write, which avoids a copy of the parent’s data space to the child until the child needs its own copy. But, regardless of this optimization,fork is expensive.

  • IPC is required to pass information between the parent and child after the fork. Passing information from the parent to the child before the fork is easy, since the child starts with a copy of the parent’s data space and with a copy of all the parent’s descriptors. But, returning information from the child to the parent takes more work.

Threads help with both problems. Threads are sometimes called lightweight processes since a thread is “lighter weight” than a process. That is, thread creation can be 10–100 times faster than process creation.

All threads within a process share the same global memory. This makes the sharing of information easy between the threads, but along with this simplicity comes the problem of synchronization.

使用线程的理由

进程与线程的区别,其实这些区别也就是我们使用线程的理由。总的来说就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。

使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

  • 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
  • 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  • 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

二、Linux上线程开发API概要

多线程开发在 Linux 平台上已经有成熟的 pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。详细请见下表:
在这里插入图片描述

三、与线程自身相关API

1. 线程创建

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
 返回:若成功返回0,否则返回错误编号
  • 用于创建一个新线程。
  • 参数包括线程标识符、线程属性、线程执行的函数,以及传递给函数的参数。

当pthread_create成功返回时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于定制各种不同的线程属性,暂可以把它设置为NULL,以创建默认属性的线程。

新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。

2. 线程退出

单个线程可以通过以下三种方式退出,在不终止整个进程的情况下停止它的控制流:

1)线程只是从启动例程中返回,返回值是线程的退出码。

2)线程可以被同一进程中的其他线程取消。

3)线程调用pthread_exit:

#include <pthread.h>
void pthread_exit(void *value_ptr);
  • 用于线程退出并返回一个值。
  • 可以在线程内部调用,将一个指针作为返回值传递给等待该线程的其他线程。

rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函数访问到这个指针。

3. 线程等待

#include <pthread.h>
int pthread_join(pthread_t thread, void **restrict retval);
 返回:若成功返回0,否则返回错误编号
  • 用于等待指定线程结束。
  • 参数包括要等待的线程的标识符和一个指向存储线程返回值的指针。

调用这个函数的线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。如果例程只是从它的启动例程返回i,rval_ptr将包含返回码。如果线程被取消,由rval_ptr指定的内存单元就置为PTHREAD_CANCELED。

可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL。

如果对线程的返回值不感兴趣,可以把rval_ptr置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程终止,但并不获得线程的终止状态。

4. 线程脱离

一个线程或者是可汇合(joinable,默认值),或者是脱离的(detached)。当一个可汇合的线程终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。脱离的线程却像守护进程,当它们终止时,所有相关的资源都被释放,我们不能等待它们终止。如果一个线程需要知道另一线程什么时候终止,那就最好保持第二个线程的可汇合状态。

pthread_detach函数把指定的线程转变为脱离状态。

#include <pthread.h>
int pthread_detach(pthread_t thread);
 返回:若成功返回0,否则返回错误编号

本函数通常由想让自己脱离的线程使用,就如以下语句:

pthread_detach(pthread_self());

5. 线程ID获取及比较

#include <pthread.h>
pthread_t pthread_self(void);
 返回:调用线程的ID

对于线程ID比较,为了可移植操作,我们不能简单地把线程ID当作整数来处理,因为不同系统对线程ID的定义可能不一样。我们应该要用下边的函数:

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
 返回:若相等则返回非0值,否则返回0

对于多线程程序来说,我们往往需要对这些多线程进行同步。同步(synchronization)是指在一定的时间内只允许某一个线程访问某个资源。而在此时间内,不允许其它的线程访问该资源。我们可以通过互斥锁(mutex),条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。在这里,我们暂不介绍读写锁。

四、与互斥锁相关API

互斥量(mutex)从本质上来说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为可运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去等待它重新变为可用。在这种方式下,每次只有一个线程可以向前运行。

在设计时需要规定所有的线程必须遵守相同的数据访问规则。只有这样,互斥机制才能正常工作。操作系统并不会做数据访问的串行化。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其它的线程在使用共享资源前都获取了锁,也还是会出现数据不一致的问题。

互斥变量用pthread_mutex_t数据类型表示。在使用互斥变量前必须对它进行初始化,可以把它置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态地分配互斥量(例如通过调用malloc函数),那么在释放内存前需要调用pthread_mutex_destroy。

1. 创建及销毁互斥锁

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
 返回:若成功返回0,否则返回错误编号
  • 用于初始化互斥锁。
  • 参数包括指向互斥锁对象和可选的互斥锁属性对象。
  • 用于销毁互斥锁。
  • 释放与互斥锁相关的资源。

要用默认的属性初始化互斥量,只需把attr设置为NULL。

2. 加锁及解锁

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
 返回:若成功返回0,否则返回错误编号
  • 用于锁定互斥锁。
  • 如果互斥锁已被其他线程锁定,则线程将阻塞,直到互斥锁可用。
  • 用于解锁互斥锁。
  • 它允许其他线程访问被保护的临界区。

如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,而返回EBUSY。

这些函数用于创建、销毁和操作互斥锁。下面是对每个函数的简要说明:

  1. pthread_mutex_init:用于初始化互斥锁。它接受一个指向互斥锁对象(pthread_mutex_t类型)和一个可选的互斥锁属性对象(pthread_mutexattr_t类型)作为参数。通常,可以将属性参数设置为NULL以使用默认属性。返回0表示成功,非零值表示失败。

  2. pthread_mutex_destroy:用于销毁互斥锁。它接受一个指向互斥锁对象的指针作为参数,用于释放与互斥锁相关的资源。返回0表示成功,非零值表示失败。

  3. pthread_mutex_lock:用于锁定互斥锁。如果互斥锁已被其他线程锁定,则调用线程将阻塞,直到互斥锁可用为止。这是用于保护临界区的常见操作。

  4. pthread_mutex_trylock:与pthread_mutex_lock类似,但它是非阻塞的。如果互斥锁已被其他线程锁定,它会立即返回并返回一个错误码,而不是等待。可以使用这个函数来尝试锁定互斥锁,如果失败,可以执行其他操作或等待一段时间后再次尝试。

  5. pthread_mutex_unlock:用于解锁互斥锁,使其可供其他线程使用。通常在保护临界区后使用此函数。

这些函数用于实现多线程中的互斥和同步,以确保线程安全。

五、与条件变量相关API

线程条件变量的API通常是由POSIX线程库(pthread)提供的,用于实现线程之间的条件协作。以下是常见的线程条件变量相关函数:

  1. 初始化条件变量:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
  • cond:指向要初始化的条件变量的指针。
  • attr:条件变量的属性,通常设置为NULL以使用默认属性。
  • 返回:成功返回0,失败返回错误码。
  1. 销毁条件变量:
int pthread_cond_destroy(pthread_cond_t *cond);
  • cond:指向要销毁的条件变量的指针。
  • 返回:成功返回0,失败返回错误码。
  1. 等待条件变量满足,同时释放互斥锁并阻塞当前线程。
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
  • 等待条件变量满足,同时释放互斥锁并阻塞当前线程。
  • cond:指向条件变量的指针。
  • mutex:指向互斥锁的指针,必须与pthread_cond_signalpthread_cond_broadcast使用的互斥锁相同。
  • 返回:成功返回0,失败返回错误码。
  1. 在指定的时间内等待条件变量满足,同时释放互斥锁并阻塞当前线程。
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
  • cond:指向条件变量的指针。
  • mutex:指向互斥锁的指针,必须与pthread_cond_signalpthread_cond_broadcast使用的互斥锁相同。
  • abstime:指定的等待时间,如果超时仍未满足条件,将返回。
  • 返回:成功返回0,失败返回错误码。
  1. 发送条件满足的信号给等待的线程中的一个线程,以唤醒其中一个线程。
int pthread_cond_signal(pthread_cond_t *cond);
  • cond:指向条件变量的指针。
  • 返回:成功返回0,失败返回错误码。
  1. 发送条件满足的信号给等待的线程中的所有线程,以唤醒所有等待的线程。
int pthread_cond_broadcast(pthread_cond_t *cond);
  • cond:指向条件变量的指针。
  • 返回:成功返回0,失败返回错误码。

这些函数是用于创建、销毁条件变量、等待条件满足、发出信号等操作的常见API。条件变量通常与互斥锁一起使用,以确保线程在等待条件变量时能够释放互斥锁,允许其他线程访问共享资源。条件变量是实现线程同步和协作的重要工具。

六、线程条件控制(Thread Condition Control)

线程条件控制(Thread Condition Control)是多线程编程中的一种重要机制,用于线程之间的协作和同步。条件控制提供了一种方式,允许一个线程等待某个条件的发生,而其他线程可以在满足条件时通知等待线程。它通常与互斥锁结合使用,以确保线程在访问共享资源之前等待适当的条件。

在C/C++中,条件控制通常是使用以下两个相关的数据结构和函数实现的:

  1. 条件变量(Condition Variable):条件变量是一个用于线程之间通信的数据结构。它允许一个线程等待某个条件的发生,而其他线程可以在满足条件时发出通知。条件变量通常与互斥锁一起使用,以确保在检查条件和等待条件之间的操作是原子的。

    • 创建条件变量:使用pthread_cond_init函数初始化条件变量。
    • 等待条件:使用pthread_cond_wait函数使线程等待条件的发生。
    • 发出通知:使用pthread_cond_signalpthread_cond_broadcast函数通知等待的线程条件已发生。
    • 销毁条件变量:使用pthread_cond_destroy函数销毁条件变量。
  2. 互斥锁(Mutex):互斥锁用于保护共享资源,以确保在检查条件和等待条件之间的操作是原子的。通常,线程在等待条件时会释放互斥锁,以允许其他线程访问共享资源。

    • 创建互斥锁:使用pthread_mutex_init函数初始化互斥锁。
    • 锁定互斥锁:使用pthread_mutex_lock函数锁定互斥锁。
    • 解锁互斥锁:使用pthread_mutex_unlock函数解锁互斥锁。
    • 销毁互斥锁:使用pthread_mutex_destroy函数销毁互斥锁。

基本的条件控制流程如下:

  1. 线程A获取互斥锁并检查条件。
  2. 如果条件不满足,线程A释放互斥锁并等待条件变量。
  3. 线程B或其他线程满足了条件后,发出通知。
  4. 等待的线程A被唤醒,再次获取互斥锁,检查条件是否满足。
  5. 如果条件满足,线程A继续执行。

这个机制允许线程在需要等待某些条件的发生时挂起,而不是无休止地忙等。条件控制在并发编程中用于实现各种同步和协作场景,如生产者-消费者问题、读写锁、任务队列等。
当处理多线程程序时,条件控制机制是一种重要的工具,用于协调线程之间的操作。
下面是一个使用条件控制的示例,并附有详细的注释,以帮助理解:

静态初始化、生产者消费者

#include <stdio.h>
#include <pthread.h>

int data = 0; // 全局变量
	
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 初始化互斥锁
pthread_cond_t condition = PTHREAD_COND_INITIALIZER; // 初始化条件变量

void *producer(void *arg) 
{
    for (int i = 0; i < 5; i++) {
        pthread_mutex_lock(&mutex); // 锁定互斥锁
        data = i; // 生产数据
        printf("Producer: Produced %d\n", data);
        pthread_cond_signal(&condition); // 发出条件满足的信号
        pthread_mutex_unlock(&mutex); // 解锁互斥锁
    }
    pthread_exit(NULL);
}

void *consumer(void *arg) 
{
    for (int i = 0; i < 5; i++) {
        pthread_mutex_lock(&mutex); // 锁定互斥锁
        while (data < i) {
            pthread_cond_wait(&condition, &mutex); // 等待条件满足
        }
        printf("Consumer: Consumed %d\n", data);
        pthread_mutex_unlock(&mutex); // 解锁互斥锁
    }
    pthread_exit(NULL);
}

int main() 
{
    pthread_t producer_thread, consumer_thread;

    // 创建生产者和消费者线程
    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);

    // 等待线程结束
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&condition);

    return 0;
}

这个示例演示了一个简单的生产者-消费者问题,其中生产者线程生产数据,而消费者线程消费数据。互斥锁用于保护共享的data变量,而条件变量用于在数据可用时通知消费者线程。注释提供了对每个关键步骤的解释,以帮助理解条件控制的工作原理。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咖喱年糕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值