LINUX-线程
创建指定n个进程
1. 信号与Core文件
- 信号:信号是操作系统中用于进程间通信或通知进程某个事件(如错误、外部中断等)的机制。当进程接收到无法处理的信号时,可能会生成core dump文件。
- Core文件:当进程因为接收到某些信号(如SIGSEGV,即段错误)而异常终止时,如果系统配置允许(通常通过
ulimit -c
设置),则会在进程的当前工作目录下生成一个core文件。这个文件包含了进程终止时的内存、寄存器状态、堆栈信息等,可用于后续的调试分析。
2. 进程切换与线程切换
- 进程切换:涉及CPU寄存器状态的保存与恢复、页表的切换(因为每个进程有自己的虚拟地址空间)以及可能的缓存刷新等操作。进程切换的开销相对较大。
- 线程切换:在同一进程内的不同线程之间切换时,因为所有线程共享同一进程的虚拟地址空间,所以不需要进行页表的切换。线程切换主要涉及CPU寄存器(如PC指针、栈指针等)的保存与恢复,开销相对较小。
3. 线程的定义与分类
进程是内存资源分配的基本单位,cpu调度是以线程为单位;
- 线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程上下文主要包括PC指针、栈指针等。
- 分类:
- 用户级线程:由用户态的线程库(如POSIX线程库pthread)来管理。线程切换不需要内核参与,效率高但缺乏系统级别的保护。
- 内核级线程:由操作系统内核直接支持和管理。线程切换需要内核参与,但可以获得系统级别的保护和调度。
- Linux中的实现:Linux实际上是将进程控制块(PCB)和线程控制块合并,通过
task_struct
结构来统一管理进程和线程。
4. 线程资源
1. 线程并发执行
- 并发执行:线程是进程内部的一个实体,是CPU调度和分派的基本单位。线程可以并发执行,这意味着它们可以在同一个进程的上下文中几乎同时运行。虽然实际上在任意时刻CPU只能执行一个线程,但由于CPU的快速切换,从用户的角度看多个线程似乎是在同时运行。
2. 多线程共享内存地址空间
- 共享内存地址空间:这是线程与进程之间最重要的区别之一。线程共享其所属进程的内存空间(包括代码段、数据段、堆等),这意味着线程之间的通信非常高效,因为它们可以直接访问相同的数据而无需进行数据的复制。
2.1 数据段共享
- 数据段共享:数据段(全局变量和静态变量)对同一进程中的所有线程都是可见的。因此,如果多个线程试图同时修改同一全局变量,就可能会出现竞态条件和数据不一致的问题。
3. 多线程共享堆空间
- 堆空间共享:堆是动态内存分配的区域,线程可以使用
malloc
、new
等函数在堆上分配内存。这些内存对同一进程内的所有线程都是可见的,但线程在访问和修改堆上的数据时同样需要注意同步和互斥,以避免数据竞争。3.1 主线程和子线程使用同一个数值的地址
- 共享地址:主线程和子线程(或任何两个线程)可以通过指针或引用(在C++中)共享同一块内存区域。这意味着一个线程对内存内容的修改会立即对另一个线程可见。这既是优点也是缺点,因为它简化了线程间的通信,但也增加了同步和互斥的复杂性。
进程能创建多少线程取决于栈区的大小
- 栈区大小限制:虽然线程的创建不直接受限于栈区的大小,但每个线程都有自己的栈空间(尽管在某些系统上这些栈可以共享或动态增长)。如果系统为每个线程分配的栈空间固定且较大,那么可以创建的线程数就会受到限制。此外,如果线程使用大量的栈空间(例如,通过深度递归),那么能够创建的线程数也会减少。
- 共享资源:包括进程的虚拟地址空间、全局变量、文件描述符等。
- 私有资源:包括线程自己的栈空间、寄存器状态等。
多线程共享一个整数,把数据地址,用指针传递给子线程
这是多线程间共享数据的常见方式。如上例所示,主线程通过传递整数的地址(即指针)给子线程,使得子线程能够访问和修改这个整数。
多线程的栈区是相对独立的
每个线程都有自己的栈空间,用于存储局部变量、函数调用的返回地址等。这些栈空间是线程私有的,互不干扰。
一个线程可以通过地址去访问另一个线程的栈区
实际上,一个线程不应该直接访问另一个线程的栈区,除非是通过合法且受控的方式(如通过传递的指针)。直接访问其他线程的栈可能会导致未定义行为,因为栈的布局和内容在线程之间是不可预测的,且可能受到操作系统调度的影响。
5. 线程相关的函数操作
启动进程就创建了主线程,栈区从main开始压栈;
创建线程:
- pthread_create()
- 功能:创建一个新线程。
- 原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
- 参数:
thread
:指向pthread_t类型的指针,用于存储新线程的标识符。attr
:指向线程属性对象的指针,通常设为NULL以使用默认属性。start_routine
:新线程将执行的函数指针。线程启动函数arg
:传递给start_routine
函数的参数。
- 返回值:成功时返回0,失败时返回错误码。
-
查看线程ID:
pthread_self
函数返回调用线程的线程ID。 - proc文件夹查看进程号;
- 线程终止:主线程的终止通常会导致整个进程的终止,除非进程中有其他线程在运行且被设置为在主线程退出后继续执行。
线程终止
线程可以通过多种方式终止:
- main 函数返回:主线程(通常是启动程序时自动创建的线程)通过返回退出程序,这会导致程序中的所有线程都终止。
- exit 或 _exit/_Exit:这些函数会立即终止整个程序,包括所有线程。但是,
_exit
和_Exit
不会清理 C 标准库分配的资源(如打开的文件、动态分配的内存等),而exit
会。- abort:这个函数会导致程序异常终止,并产生核心转储(如果启用了该选项)。它会终止所有线程。
- 收到信号:线程可以接收到如 SIGKILL 或 SIGTERM 等信号而终止。
- 子线程只终止自己:子线程通过从其启动函数中返回来终止自己。这是子线程优雅退出的标准方式。
-
pthread_exit()
- 功能:终止调用线程。
- 原型:
void pthread_exit(void *retval);
- 参数:
retval
是一个指向返回值的指针,其他线程可以通过pthread_join()
获取这个值。 - 注意事项:线程一旦调用
pthread_exit()
或返回其启动函数,就会终止。
- pthread_join()
- 功能:等待指定的线程终止。
- 原型:
int pthread_join(pthread_t thread, void **retval);
- 参数:
thread
:要等待的线程的标识符。retval
:用于存储终止线程的返回值的指针的地址。如果不需要,可以设为NULL。
- 返回值:成功时返回0,失败时返回错误码。
多线程和信号不能同时使用,多线程会共享注册信号的信息;
pthread_detach
pthread_detach
函数用于将线程设置为“分离”状态(detached state)。当一个线程被分离后,它不能通过pthread_join
等待其终止。当分离的线程终止时,其所有资源会自动被释放回系统,而无需其他线程对其进行显式的同步操作。这意味着,一旦线程开始执行,你就不能再通过pthread_join
来等待它完成,并且你无法获取它的退出状态。函数原型:
int pthread_detach(pthread_t thread);
- 参数:
thread
是要设置为分离状态的线程的标识符。- 返回值: 成功时返回 0;失败时返回错误码。
pthread_join
pthread_join
函数用于等待指定的线程终止。调用pthread_join
的线程(通常是主线程或其他线程)将被阻塞,直到指定的线程调用pthread_exit
或从它的启动例程返回。通过pthread_join
,调用线程可以获取被等待线程的退出状态,并回收它所占用的系统资源。函数原型:
int pthread_join(pthread_t thread, void **retval);
- 参数:
thread
是要等待的线程的标识符。retval
是一个指向 void 指针的指针,用于存储被等待线程的返回值。如果不需要返回值,可以将其设置为 NULL。- 返回值: 成功时返回 0;失败时返回错误码。
使用场景
- 使用
pthread_detach
的场景:
- 当你不需要等待线程完成,也不关心它的返回值时。
- 当你想要线程独立运行,并在其完成后自动释放资源时。
- 当你创建了大量短期运行的线程,且不希望管理它们的生命周期时。
- 使用
pthread_join
的场景:
- 当你需要等待线程完成其任务,并获取其结果时。
- 当你需要同步多个线程的执行顺序时。
- 当你需要确保线程使用的资源在线程终止后被正确回收时。
注意事项
- 默认情况下,线程是“可连接的”(joinable),即可以通过
pthread_join
等待其终止。- 一旦线程被分离,就不能再将其设置为可连接状态,也不能再对其调用
pthread_join
。- 如果尝试对一个已经终止的线程调用
pthread_join
,调用将成功并返回线程的退出状态。但是,如果线程尚未终止,调用线程将被阻塞,直到目标线程终止。- 调用
pthread_detach
或pthread_join
多次(对同一个线程)的行为是未定义的。因此,你应该确保每个线程只被分离或等待一次。
线程的取消:
pthread_cancel
是 POSIX 线程(pthread)库中用于请求取消另一个线程的函数。当对某个线程调用pthread_cancel
时,它会向该线程发送一个取消请求。然而,这并不意味着线程会立即停止执行;实际上,线程如何响应这个取消请求取决于多个因素,包括线程的取消状态和取消类型。取消状态
每个线程都有一个取消状态,它可以是
PTHREAD_CANCEL_ENABLE
(允许取消)或PTHREAD_CANCEL_DISABLE
(禁用取消)。默认情况下,线程的取消状态是PTHREAD_CANCEL_ENABLE
,即允许取消。但是,线程可以调用pthread_setcancelstate
来改变其取消状态。取消类型
除了取消状态之外,每个线程还有一个取消类型,它决定了线程如何响应取消请求。取消类型可以是
PTHREAD_CANCEL_DEFERRED
(延迟取消)或PTHREAD_CANCEL_ASYNCHRONOUS
(异步取消)。
延迟取消(Deferred Cancellation):这是默认的取消类型。在这种情况下,取消请求不会立即生效,而是会在线程的下一个取消点(cancellation point)上被检查。取消点是线程执行过程中特定的函数调用点,在这些点上,线程会检查是否有挂起的取消请求。如果线程被请求取消,并且它到达了一个取消点,那么它将开始清理工作(如释放资源、调用清理处理程序等),并最终终止执行。
异步取消(Asynchronous Cancellation):在这种模式下,取消请求可以在任何时候生效,而不仅仅是在取消点上。但是,需要注意的是,并非所有系统都支持异步取消,并且它的行为可能不如延迟取消那样可预测。
取消点 man 7 pthreads
取消点是线程执行过程中可能会检查取消请求的函数调用点。POSIX 标准定义了一系列的标准取消点,包括但不限于:
- 阻塞系统调用(如
read
、write
、select
、poll
等)- 某些库函数(如
pthread_testcancel
、pthread_cond_wait
、pthread_cond_timedwait
、sem_wait
等)线程可以通过调用
pthread_testcancel
来在其执行路径中的任意点创建一个自定义的取消点。取消处理器
线程可以安装一个取消处理器(cancel handler),它是一个在用户定义的函数中指定的清理例程,当线程被取消时将执行该例程。线程可以通过调用
pthread_setcanceltype
并设置类型为PTHREAD_CANCEL_DEFERRED
(如果尚未设置)和调用pthread_cleanup_push
来注册清理函数。注意事项
- 当线程被取消时,它会尝试释放其持有的所有锁(除非这些锁是以
PTHREAD_MUTEX_ROBUST
属性创建的),并调用已注册的清理处理器。- 并非所有资源都能自动在取消时得到清理,因此设计时需要考虑到这一点。
- 线程取消是一种协作机制,它依赖于线程在取消点检查取消请求。如果线程从未到达取消点,则它可能不会响应取消请求。
- 异步取消的行为可能因系统而异,且不如延迟取消那样可靠,因此在需要可预测行为的情况下应谨慎使
- 取消成功的例子:
- 取消失败的例子: ps -elLf
- 手动添加取消点
- pthread_testcancel
- 结论:
- cancel 会立刻修改目标线程的取消标志位,
- 目标线程运行到一些特殊的函数(cancelation point)
- (运行线程终止清理函数来保护释放资源)
- 取消点函数调用完前会终止线程;
- 线程的终止异步终止会导致资源泄露的问题;
- 泄露资源如果是加锁资源会导致后果严重
资源清理栈---自动根据申请多少资源,就释放多少资源
- 资源清理栈里存放了函数指针,函数指针对应了资源释放行为,申请资源之后把对应得释放行为压栈。
- 线程因为pthread_cancel 或exit终止时将栈清空;(只有这种终止才能调用资源清理栈)
- 可以主动调用pthread_cleanup_pop释放资源;
- 包装资源清理行为;
- 资源清理栈的使用:
linux规定:push 和 pop必须在同一个作用域中成对出现;
pthread_cancel (给另一个线程发送取消请求);
整理资源清理:
cleanup的目的:线程无论在什么时候终止,都会根据申请的资源执行合理的释放行为;
核心数据结构:清理函数栈;
1、每次申请资源后,把清理操作入栈,
2、弹栈的时机
pthread_exit ; pthread_cancel;按LIFO全弹
pthread_clean_pop;//弹一个(参数填正数);
不要使用return;
3、push 和pop成对出现;
4、有了pop不要手动回收资源;
线程同步
同步(Synchronization)
同步是一种机制,用于控制多个线程之间的执行顺序,以确保它们按照预定的方式执行。在多线程环境中,线程可能会同时访问共享资源,这可能导致数据竞争、条件竞争或其他并发问题。同步机制可以确保线程之间的有序性,防止这些问题的发生。
同步的常见方式包括:
互斥锁(Mutexes):互斥锁是最基本的同步机制之一,用于确保在任意时刻只有一个线程可以访问某个资源。当一个线程想要访问受保护的资源时,它必须先获取与该资源相关联的互斥锁。如果锁已被其他线程持有,则当前线程将阻塞,直到锁被释放。
条件变量(Condition Variables):条件变量用于线程间的同步,通常与互斥锁一起使用。它们允许线程在某些条件不满足时挂起,并在条件变为满足时被唤醒。
信号量(Semaphores):信号量是一种更通用的同步机制,用于控制对多个资源的访问。它有一个非负整数值,表示可用资源的数量。线程可以通过P(wait)操作来请求资源(如果资源数量为0,则线程阻塞),通过V(signal)操作来释放资源(增加资源数量,并可能唤醒等待的线程)。
互斥(Mutual Exclusion)
互斥是同步的一种特殊形式,它确保了在任意时刻只有一个线程可以访问某个资源。这是通过互斥锁来实现的,互斥锁是一种特殊的锁,用于保护共享资源免受并发访问的干扰。
信号量(Semaphores)
信号量是一种用于控制对共享资源访问的计数器。POSIX标准定义了两种信号量:
无名信号量:也称为内存中的信号量,它们是在进程的地址空间中分配的,因此只能被同一进程中的线程访问。无名信号量通常通过
sem_init
、sem_wait
(P操作)、sem_post
(V操作)等函数来操作。有名信号量:有名信号量存储在文件系统中,因此可以被同一台机器上的不同进程访问。它们通过
sem_open
、sem_close
、sem_unlink
等函数来创建、关闭和删除。由于它们存储在文件系统中,因此即使进程终止,信号量的状态也会被保留,直到它们被显式删除。POSIX信号量的操作
初始化:无名信号量通过
sem_init
函数初始化,有名信号量通过sem_open
函数创建时初始化。P操作(申请资源):通过
sem_wait
函数执行,如果信号量的值大于0,则将其减1,并立即返回;如果信号量的值为0,则调用线程阻塞,直到信号量的值变为大于0。V操作(释放资源):通过
sem_post
函数执行,将信号量的值加1。如果信号量的值因此变为非零,并且有其他线程在等待该信号量(即,被sem_wait
阻塞),则这些线程中的一个将被唤醒。
信号量:
- pthread_mutex_lock() 和 pthread_mutex_unlock()
- 功能:互斥锁(Mutex)的加锁和解锁操作,用于保护共享数据。
- 原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 参数:
mutex
是指向互斥锁对象的指针。 - 返回值:成功时返回0,失败时返回错误码。
- pthread_cond_wait() 和 pthread_cond_signal() / pthread_cond_broadcast()
- 功能:条件变量(Condition Variable)的等待、信号发送和广播操作,用于线程间的同步。
- 原型:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
- 参数:
cond
是指向条件变量对象的指针。mutex
是在调用pthread_cond_wait()
时应该持有的互斥锁。
- 返回值:成功时返回0,失败时返回错误码。
线程属性
- pthread_attr_init()、pthread_attr_setdetachstate() 等
- 功能:初始化线程属性对象,并设置线程的属性(如分离状态、堆栈大小等)。
- 这些函数用于在创建线程之前配置线程的各种属性。
线程库的历史
LinuxThreads:
- 起源:LinuxThreads 是 Linux 早期支持多线程的一个库,由 Inter 公司(这里可能是指 Intel 或某个误写的公司名称)的开发者们贡献。
- 特点:LinuxThreads 使用一种称为“线程管理器”或“线程守护进程”的模型来管理线程。这种模型在多个线程之间共享进程空间,但每个线程在内核中并不直接对应一个线程。这导致了性能瓶颈和扩展性问题。
NPTL(Native POSIX Threads Library):
- 起源:NPTL 是由 Red Hat(小红帽公司)的开发者们设计并实现的一个线程库,旨在改进 Linux 系统中多线程的性能和扩展性。
- 特点:NPTL 实现了 POSIX 线程(pthreads)的接口,并且将用户级线程(pthread 库创建的线程)与内核级线程(由操作系统直接管理的线程)进行 1:1 的映射。这种映射方式显著提高了线程的性能和响应能力,因为每个线程都可以直接由内核调度,减少了上下文切换的开销。
- 兼容性:从 c 库 2.3 版本和 Linux 内核 2.6 版本开始,NPTL 成为了默认的线程库。
错误处理
在多线程程序中,错误处理是一个重要且复杂的主题。由于多个线程可能同时运行并访问共享资源,因此需要特别注意错误处理的方式,以避免数据竞争和竞态条件。
为什么不能使用 perror:
- 全局变量 errno:
perror
函数依赖于全局变量errno
来确定最后一个系统调用的错误代码,并将其转换为可读的错误消息。在多线程环境中,如果多个线程同时修改或读取errno
,可能会导致数据竞争和不确定的结果。 - 并发访问:由于多线程可能并发地执行系统调用并设置
errno
,使用perror
可能会导致输出与预期的错误代码不匹配,从而误导开发者。
替代方案:
- 使用返回值:多线程函数应该通过返回值来报告错误。返回值可以是一个特定的错误码,该错误码可以通过其他函数(如
strerror
)转换为可读的错误消息。 - 线程局部变量:对于需要存储线程特定错误信息的场景,可以使用线程局部变量(Thread-Local Storage, TLS)来避免全局变量的竞争问题。
strerror:
- 用途:
strerror
函数接受一个错误码作为参数,并返回指向错误消息字符串的指针。由于strerror
返回的指针可能指向静态分配的缓冲区,在多线程环境中使用时需要小心,以避免潜在的数据竞争。然而,在许多现代实现中,strerror_r
(可重入的版本)或strerror_l
(线程局部版本)被推荐使用以避免这些问题。
信号量:
有名信号量(POSIX有名信号量)
- 定义与特点:
- 有名信号量是一种可以被多个进程或线程共享的信号量。
- 它有一个唯一的名称,这个名称通常是一个以“/”开头的字符串,类似于文件系统中的路径名。
- 有名信号量是通过文件系统中的特殊文件来维护的,但这些文件并不直接存储信号量的值,而是作为信号量的标识符。
- 有名信号量的值是持久的,即使创建它的进程结束,信号量仍然存在,并且值也不会改变。
- 使用场景:
- 主要用于不同进程之间的同步和互斥。
- 可以通过名称在不同进程间共享和访问。
- 主要操作函数:
- sem_open():创建或打开一个有名信号量。
- sem_close():关闭一个有名信号量(注意,这并不会删除信号量,只是关闭对它的访问)。
- sem_unlink():删除一个有名信号量。
- sem_wait()/sem_trywait()/sem_timedwait():等待信号量变为可用(即值大于0),或尝试非阻塞地等待,或在指定时间内等待。
- sem_post():增加信号量的值,表示释放了一个资源。
无名信号量(POSIX基于内存的信号量)
- 定义与特点:
- 无名信号量是一种只能在同一进程内部或共享内存区中的多个进程间共享的信号量。
- 它不是一个文件,而是一个变量,因此只能作用于能够访问该变量的线程或进程。
- 无名信号量的值是与内核关联的,当创建它的进程终止时,该信号量也会被销毁。
- 使用场景:
- 主要用于同一进程内的线程同步,或在通过共享内存连接的多个进程间同步。
- 主要操作函数:
- sem_init():初始化一个无名信号量。
- sem_destroy():销毁一个无名信号量。
- sem_wait()/sem_trywait()/sem_timedwait():与有名信号量相同,用于等待信号量变为可用。
- sem_post():与有名信号量相同,用于增加信号量的值。
总结
- 共享范围:有名信号量可以在不同进程间共享,无名信号量只能在同一进程内部或共享内存区中的多个进程间共享。
- 持久性:有名信号量的值是持久的,无名信号量的值随进程终止而销毁。
- 操作方式:两者都使用类似的函数接口(如sem_wait()、sem_post())进行操作,但有名信号量需要通过sem_open()等函数进行创建和打开,而无名信号量则通过sem_init()进行初始化。
二元信号量(Binary Semaphore)
- 定义与特点:
- 二元信号量是最简单的信号量形式,它只有两种状态:占用(通常表示为0)和非占用(通常表示为1)。
- 它适用于那些只能被一个线程或进程独占访问的资源。
- 当资源被占用时,二元信号量的值为0;当资源未被占用时,二元信号量的值为1。
- 使用场景:
- 在需要确保同一时间只有一个线程或进程可以访问共享资源的场景中,二元信号量非常有用。
- 例如,在打印机控制、互斥锁实现等场景中,二元信号量可以用来确保资源的独占访问。
- 操作:
- P操作(Wait/Take):当线程或进程需要访问共享资源时,它会尝试执行P操作来获取二元信号量。如果信号量的值为1(表示资源未被占用),则将其减1(变为0),表示资源已被占用,线程或进程可以继续执行。如果信号量的值为0(表示资源已被占用),则线程或进程将被阻塞,直到信号量被释放。
- V操作(Signal/Give):当线程或进程完成对共享资源的访问后,它会执行V操作来释放二元信号量。这会将信号量的值加1(从0变为1),表示资源已被释放,等待该资源的线程或进程可以被唤醒。
多元信号量(Counting Semaphore)
- 定义与特点:
- 多元信号量(也称为计数信号量)可以有多个状态,用于表示某个共享资源的可用数量。
- 它允许多个线程或进程并发访问共享资源,但数量受到信号量初始值的限制。
- 使用场景:
- 在允许多个线程或进程并发访问共享资源的场景中,多元信号量非常有用。
- 例如,在控制对共享内存区域、数据库连接池或其他类型共享资源的访问时,多元信号量可以确保资源的正确分配和释放。
- 操作:
- P操作(Wait/Take):与二元信号量类似,但在多元信号量中,P操作会尝试减少信号量的值。如果减少后的值仍然大于等于0,则表示还有可用资源,线程或进程可以继续执行;如果小于0,则表示资源已被全部占用,线程或进程将被阻塞。
- V操作(Signal/Give):当线程或进程完成对共享资源的访问后,它会执行V操作来增加信号量的值。如果增加前的值小于0,则表示有等待该资源的线程或进程被阻塞,此时会唤醒一个或多个等待的线程或进程。
竞争条件:
互斥锁
用结构体封装:共享资源;
死锁;
线程阻塞:等待永远不可能为真的条件成立;
1 两个管道的建立:申请多个资源的顺序有问题;
2持有锁的线程终止:
(在线程终止的任何分支都要解锁);
(如果线程可能被cancel,在加锁之后立刻pthread_cleanup_push;;;;pthread_mutex_lock不是取消点);
3一个线程对同一把锁加锁两次:
mutex的底层实现原理
mutex 互斥睡眠锁(加锁睡眠)
rwlock读写锁 (多读少写)
pthread_spin_lock:自旋锁(加锁时陷入while(1));开锁时间快,无上下文切换;
(如果条件马上会就绪,优先用自旋锁);
mutex依赖futex(futex底层包括了自旋锁和睡眠锁);
锁和二元信号量的区别(用法区别):
加锁--------解锁(限制,哪个线程加锁,哪个线程解锁);
P ----------------V
解决一个线程对同一把锁加锁两次:
上策:不使用这种代码;
中策:非阻塞加锁:
int pthread_mutex_trylock();
未加锁状态则加锁;若已经加锁,立即返回;
while + trylock 可以实现自旋锁;
可能陷入活锁;
trylock 可能导致活锁;
让任意一个线程执行完操作后,sleep随机时间解决活锁;
下策:可重入锁
使用锁实现生产者消费者;
火车票卖票场景;
把查看票的数量也要放入锁的保护:
修改锁的属性:影响重复加锁的行为
普通锁;检错锁;递归锁(可重入锁)
可重入锁的特点:
一个线程对同一把锁重复加锁,只是增加锁的应用计数;(加了几次锁就解几次);
其他线程只有锁的引用计数为0,不能加锁
用锁实现同步:
同步:事件的执行顺序是固定的;
利用mutex实现;
条件变量:实现事件的同步;
无竞争事件回合场景:
1、设计一个条件,决定本线程是否要等待
2、如果不满足,调用wait(原语)会使本线程陷入等待
3、另一个线程会运行,直到将条件改成满足,通知signal (原语)阻塞的线程恢复就绪;
进程池使用:主线程执行,子线程wait;
条件变量的接口:
条件变量要配合锁一起使用;
使用条件变量的一般流程:
1、弄清楚事件的发送顺序;先(signal) 后wait;
为什么条件变量要配合锁来使用:
pthread_cond_wait 的内部实现:
前半:
1、判断有没有加锁:
2、把自己加入唤醒队列,
3、解锁并陷入等待,(解锁和等待是原子的)
后半:(收到signal之后)
1、使自己就绪
2、加锁,
3、持有锁之后继续运行;
在signal时应有线程已用wait而阻塞,否则这个signal会丢失;
主线程执行完后,进入sleep,子线程唤醒,发现主线程持有锁,只能等待主线程解锁之后才能持有锁;
使用条件变量的惯用法:
1、希望wait的线程先执行:
a、先加锁(保护flag)
b、判断是否状态满足,
c、调用cond_wait;
d、执行后续事件,记得要解锁
2、先执行
a、先加锁
b、执行事件
c、修改flag并用signal通知
d、事件完毕之后再解锁;
使用条件变量实现生产者和消费者:
火车票案例: 放票:1 ; 售票 2;
初始20张,一但窗口卖完,放票临时加10;
先售票,直到为0,再放票;
放票wait;
共享资源:
生产者和消费者:
最初有10个商品,每1s生产1个,每0.5s消费1个;
pthread_cond_timewait:等待绝对时间(某一个时刻);实现高精度等待
broadcast :广播
如果多个线程等待在同一个唤醒队列里,signal只能唤醒队首线程,使用braodcast可以全部唤醒,但执行的只有队首;
操作系统内核可能会有虚假唤醒:
把条件变量的条件改成while:
在调用 pthread_cond_wait
之前,你通常需要检查某个条件是否为真。如果条件为真,则可能不需要等待;如果条件为假,则进入等待状态。
while (!condition) { | |
// 条件不满足,需要等待 | |
} |
注意,这里使用 while
循环而不是 if
语句是因为可能会出现“虚假唤醒”(spurious wakeup),即线程可能在条件未变为真时被唤醒。
线程安全:(相对库函数或系统调用)
库函数在多线程的情况下运行不正确;(如果这些函数使用了堆栈空间或数据段空间);
ctime返回的指针在数据段;
线程安全的版本:
buf表示主调函数分配空间的地址;
可重入和不可重入:
一个函数在调用过程中,有机会再次调用自己;
同步:递归;
异步:信号;多线程; (重入);
如果一个函数在重入的结果和不重入的结果一样--可重入函数;
可重入函数的特点:
不加锁不访问共享的区域;
不修改代码段;
不调用不可重入的函数;
可重入的函数基本上就是线程安全函数;