一、多线程的使用
1.1、线程的创建
函数原型:
描述: pthread_create()函数在调用进程中启动一个新线程。新线程通过调用start_routine()开始执行;arg作为start_routine()的唯一参数传递。
新线程以以下方式之一终止: (1)它调用pthread_exit(),指定一个退出状态值,该值可用于调用pthrread_join()的同一进程中的另一个线程,即pthrread_join()可以接收pthread_exit()返回的值。 (2)它从start_routine()返回。这相当于使用return语句中提供的值调用pthread_exit()。 (3)它被pthread_cancel()取消。 (4)进程中的任何线程都调用exit(),或者主线程执行main()的返回。这将导致进程中所有线程的终止。
参数介绍:
参数
含义
attr
attr参数指向pthread_attr_t结构,其内容在线程创建时用于确定新线程的属性;使用pthread_attr_init()和相关函数初始化该结构。如果attr为空,则使用默认属性创建线程。
thread
在返回之前,成功调用pthread_create()将新线程的ID存储在thread指向的缓冲区中;此标识符用于在后续调用其他pthreads函数时引用线程。
start_routine
线程入口函数
arg
线程入口函数的参数
返回值: 成功时,返回0;出错时,它返回一个错误号,并且*thread的内容未定义。
错误号:
错误号
含义
EAGAIN
资源不足,无法创建另一个线程。
AGAIN A
遇到系统对线程数量施加的限制。可能触发此错误的限制有很多:已达到RLIMIT_NPROC软资源限制【通过setrlimit()设置】,该限制限制了真实用户ID的进程和线程数;已达到内核对进程和线程数的系统范围限制,即/proc/sys/kernel/threads max【请参阅proc()】;或者达到最大pid数/proc/sys/kernel/pid_max【见proc()】。
EINVAL
属性中的设置无效。
EPERM
没有设置attr中指定的调度策略和参数的权限。
其他: 新线程继承创建线程的信号掩码【pthread_sigmask()】的副本。新线程的挂起信号集为空【sigpending()】。新线程不继承创建线程的备用信号堆栈【sigaltstack()】。 新线程的CPU时间时钟的初始值为0【参见pthread_getcpuclockid()】。
示例代码:
讯享网
1.2、线程的终止
新线程以以下方式之一终止: (1)它调用pthread_exit(),指定一个退出状态值,该值可用于调用pthrread_join()的同一进程中的另一个线程,即pthrread_join()可以接收pthread_exit()返回的值。 (2)它从start_routine()返回。这相当于使用return语句中提供的值调用pthread_exit()。 (3)它被pthread_cancel()取消。 (4)进程中的任何线程都调用exit(),或者主线程执行main()的返回。这将导致进程中所有线程的终止。
pthread_exit()函数原型:
描述: (1)pthread_exit()函数终止调用线程并通过retval返回一个值,该值(如果线程是可连接的)可用于调用pthrea_join()的同一进程中的另一个线程,即可被pthrea_join()接收返回值。
(2)任何由pthread_cleanup_push()建立的尚未弹出的清理处理程序都会弹出(与它们被推送的顺序相反)并执行。如果线程具有任何特定于线程的数据,则在执行清理处理程序后,将以未指定的顺序调用相应的析构函数。
(3)当线程终止时,进程共享资源(例如互斥体、条件变量、信号量和文件描述符)不会被释放,使用atexit()注册的函数也不会被调用。
(4)进程中的最后一个线程终止后,进程通过调用exit()终止,退出状态为零;因此,释放进程共享资源并调用使用atexit()注册的函数。
返回值:此函数不返回调用方。
错误:此函数始终成功。
注意: (1)从除主线程之外的任何线程的start函数执行返回将导致隐式调用pthread_exit(),使用函数的返回值作为线程的退出状态。 (2)为了允许其他线程继续执行,主线程应该通过调用pthread_exit()而不是exit()来终止。 (3)retval指向的值不应位于调用线程的堆栈上,因为该堆栈的内容在线程终止后未定义。
pthread_cancel()函数原型:
讯享网
描述: pthread_cancel()函数向线程thread发送取消请求。目标线程是否以及何时响应取消请求取决于该线程控制的两个属性:其可取消性state和type。
由pthread_setcancelstate()设置线程的可取消状态可以启用(新线程的默认状态)或禁用。如果线程已禁用取消,则取消请求将保持排队状态,直到线程启用取消。如果线程已启用取消,则其可取消性类型决定何时取消。
由pthread_setcanceltype()确定的线程的取消类型可以是异步的或延迟的(新线程的默认值)。异步可取消性意味着线程可以随时取消(通常是立即取消,但系统不保证)。延迟可取消性意味着取消将被延迟,直到线程下一次调用作为取消点的函数。pthreads()中提供了作为或可能是取消点的函数列表。
执行取消请求时,线程将执行以下步骤(按顺序):
上述步骤相对于pthread_cancel()调用异步发生;pthread_cancel()的返回状态仅通知调用方取消请求是否已成功排队。
被取消的线程终止后,使用pthread_join()与该线程的连接将获得pthrea_canceled作为线程的退出状态。(使用线程连接是知道取消已完成的唯一方法。)
返回值:成功时,返回0;出错时,返回非零错误号。
错误:ESRCH,找不到ID为thread的线程。
1.3、线程的等待
函数原型:
描述: pthread_join()函数等待线程指定的线程终止。如果该线程已经终止,则pthread_join()立即返回。thread指定的线程必须是可连接的。
如果retval不为空,则pthread_join()将目标线程的退出状态(即,目标线程提供给pthrea_exit()的值)复制到retval所指向的位置。如果目标线程被取消,则PTHREAD_CANCELED被置于retval中。
如果多个线程同时尝试与同一线程联接,则结果是未定义的。如果调用pthread_join()的线程被取消,那么目标线程将保持可连接状态(即,它不会被分离)。
返回值:成功时,返回0;出错时,它返回错误号。
错误号:
错误号
含义
EDEADLK
检测到死锁(例如,两个线程试图彼此连接);或thread指定调用线程。
EINVAL
线程不是可连接线程。
EINVAL
另一个线程已在等待加入此线程。
ESRCH
找不到ID为线程的线程。
1.4、线程的属性
函数原型:
描述: pthread_attr_init()函数使用默认属性值初始化attr指向的线程属性对象。在这个调用之后,可以使用各种相关函数(下方列出)设置对象的各个属性,然后可以在创建线程的一个或多个pthread_create()调用中使用该对象。
对已初始化的线程属性对象调用pthread_attr_init()会导致未定义的行为。
当不再需要线程属性对象时,应使用pthread_attr_destroy()函数将其销毁。 销毁线程属性对象对使用该对象创建的线程没有影响。

线程属性对象被销毁后,可以使用pthread_attr_init()对其重新初始化。任何其他使用已销毁线程属性对象的方法都会产生未定义的结果。
返回值: 成功时,这些函数返回0;出错时,它们返回一个非零错误号。
错误: 在Linux上,这些函数总是成功的(但可移植和未来验证的应用程序应该处理可能的错误返回)。
pthread_attr_t类型应被视为不透明的:除通过pthreads函数外,对对象的任何访问都是不可移植的,并产生未定义的结果。
示例代码:
二、无原子操作
在多个线程中,对一个变量不断操作,如果没有原子操作会怎么样? 示例代码:
上述代码执行结果理论上是,但是最后结果是。也就是无原子操作下的执行结果小于理论值。 原因在于,执行idx++时汇编代码是:
也就是c语言是一条语句,但真正执行时是三条命令。在无原子操作时,就可能出现如下情况:
原意要自增两次,然而实际只自增了一次,因此无原子操作下的执行结果小于理论值。
三、互斥锁
让临界资源只允许在一个线程中执行。
pthread_mutex_init()
函数原型:
函数描述: 互斥锁的初始化。 pthread_mutex_init() 函数是以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性。如果参数attr为空(NULL),则使用默认的互斥锁属性,默认属性为快速互斥锁 。 互斥锁的属性在创建锁的时候指定,在实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。
返回: 成功会返回零,其他任何返回值都表示出现了错误。 成功后,互斥锁被初始化为未锁住态。
pthread_mutex_destroy()
用于注销一个互斥锁,函数原型:
销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此pthread_mutex_destroy()仅仅检查锁状态(锁定状态则返回EBUSY)。
pthread_mutex_lock()和pthread_mutex_trylock()
函数原型:
描述: 互斥引用的互斥对象通过调用 pthread_mutex_lock()被锁定。如果互斥锁已被锁定,则调用线程将阻塞,直到互斥体变为可用。此操作将返回由处于锁定状态的互斥所引用的互斥对象,其中调用线程是其所有者。 函数 pthread_mutex_trylock()与 pthread_mutex_lock()相同,只是如果互斥引用的互斥对象当前被锁定(由任何线程,包括当前线程锁定),则调用将立即返回。
互斥类型
含义
PTHREAD_MUTEX_NORMAL
不提供死锁检测。尝试重新锁定互斥锁会导致死锁。如果线程尝试解锁它尚未锁定的互斥锁或已解锁的互斥体,则会导致未定义的行为。
PTHREAD_MUTEX_ERRORCHECK
提供错误检查。如果线程尝试重新锁定已锁定的互斥锁,则会返回错误。如果线程尝试解锁尚未锁定的互斥体或已解锁的互斥体,则将返回错误。
PTHREAD_MUTEX_RECURSIVE
互斥锁将保留锁定计数的概念。当线程首次成功获取互斥锁时,锁定计数将设置为 1。每次线程重新锁定此互斥锁时,锁定计数都会递增 1。每次线程解锁互斥体时,锁定计数都会减少 1。当锁定计数达到零时,互斥锁将可供其他线程获取。如果线程尝试解锁尚未锁定的互斥体或已解锁的互斥体,则将返回错误。
PTHREAD_MUTEX_DEFAULT
尝试递归锁定互斥会导致未定义的行为。如果互斥体未被调用线程锁定,则尝试解锁该互斥体会导致未定义的行为。如果互斥体未锁定,则尝试解锁互斥体会导致未定义的行为。
返回值: 如果成功,pthread_mutex_lock()和 pthread_mutex_unlock() 函数返回零。否则,将返回一个错误号以指示错误。 如果获取了互斥引用的互斥对象上的锁,则函数 pthread_mutex_trylock() 返回零。否则,将返回一个错误号以指示错误。
如果出现以下情况,pthread_mutex_lock()和pthread_mutex_trylock()函数将失败:
错误代码
含义
EINVAL
互斥体是使用具有值PTHREAD_PRIO_PROTECT的协议属性创建的,并且调用线程的优先级高于互斥体的当前优先级上限。
EBUSY
无法获取互斥体,因为它已被锁定。
EINVAL
互斥体指定的值不引用初始化的互斥体对象。
EAGAIN
无法获取互斥锁,因为已超过互斥锁的最大递归锁数。
EDEADLK
当前线程已拥有互斥体。
EPERM
当前线程不拥有互斥体。
这些函数不会返回错误代码EINTR。
pthread_mutex_unlock()函数原型:
描述: pthread_mutex_unlock() 函数释放互斥引用的互斥对象。释放互斥体的方式取决于互斥体的 type 属性。如果在调用 pthread_mutex_unlock()时,互斥所引用的互斥对象上存在阻塞的线程,从而导致互斥体变为可用,则调度策略用于确定哪个线程应获取互斥。(在PTHREAD_MUTEX_RECURSIVE互斥锁的情况下,当计数达到零并且调用线程不再对此互斥锁时,互斥锁将变为可用)。
如果信号被传递到等待互斥体的线程,则在信号处理程序返回时,线程将恢复等待互斥体,就好像它没有被中断一样。
返回值: 如果成功,返回零。否则,将返回一个错误号以指示错误。
示例代码
上述代码执行结果是。也就是互斥锁下的执行结果等于理论值。
五、自旋锁
自旋锁的接口和mutex类似。 函数原型:
示例代码
上述代码执行结果是。也就是自旋锁下的执行结果等于理论值。
互斥锁与自旋锁的区别:
- 互斥锁与自旋锁的接口类似,但是底层实现有一定差异。
- mutex在发现锁已经被占用时,会让出CPU资源,然后等待有解锁时唤醒去抢锁。
- spin在发现锁已经被占用时,会一直等着,直到抢到锁。
死锁,死锁的两种情况:
(1)如果两个线程先后调用两次lock,第二次调用lock时,由于锁已被占用,该线程会挂起等待别的线程释放锁,然后锁正是被自己占用着的,该线程又被挂起不能释放锁,因此就永远处于挂起等待状态了,进入死锁。 (2)线程1和线程2。线程1获得锁1,线程2获得锁2,此时线程1调用lock企图获得锁2,结果是需要挂起等待线程2释放锁2,而此时线程2也调用了lock企图获得锁1,结果是线程2挂起等待线程1释放锁1,进入死锁。
避免死锁: (1)共享资源操作前一定要获得锁。 (2)完成操作以后一定要释放锁。 (3)尽量短时间地占用锁。 (4)有多锁, 如获得顺序是abc连环扣, 释放顺序也应该是abc。 (5)线程错误返回时应该释放它所获得的锁。 (6)写程序是尽量避免同时获得多个锁。如果一定要这么做,所有线程在需要多个锁时都按相同的先后顺序获得锁,则不会出现死锁。
六、原子操作
原子操作就是用一条指令解决问题;多条执行命令变为一条执行命令,使其不可分割。
CAS,全称Compare And Swap。翻译过来就是先比较再赋值,顺序不可变;也就是先对比,如果值一致再赋值,如果不一致就不赋值。
常见的原子操作: (1)加,add (2)减,sub (3)自增,inc (4)自减,dec (5)比较赋值,cas
注意,c语言的一条语句执行可能有副作用,但原子操作是没有副作用的。 示例代码:
七、总结
对临界资源操作时,常用原子操作和锁。 锁有互斥锁、自旋锁、读写锁等,其他应用程序实现的业务锁如悲观锁、乐观锁等。 在两种情况下容易陷入死锁: (1)线程调用两次lock,第一次已经获得锁,第二次发现锁已占用进入等待,而锁是被自己占用,进入无线等待的死锁。 (2)多个线程多个锁的情况,线程1获得锁1然后请求锁2,线程2获得锁2然后请求锁1,互相等待,进入锁。
原子操作就是通过一条指令解决问题,封装的CAS将多条执行命令变为一条执行命令,使其不可分割。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/137.html