导航:首页 > 操作系统 > 读写锁clinux

读写锁clinux

发布时间:2023-05-23 13:29:54

A. 如何对读写锁进行处理

信号量强调的是线程(或进程)间的同步:“信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都 在sem_wait的时候,就阻塞在那里)。当信号量为单值信号量是,也可以完成一个资源的互斥访问。

有名信号量:可以用于不同进程间或多线程间的互斥与同步

创建打开有名信号量

sem_t *sem_open(const char *name, int oflag);

sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);

成功返回信号量指针;失败返回SEM_FAILED,设置errnoname是文件路径名,但不能写成/tmp/a.sem这样的形式,因为在linux下,sem都是在/dev/shm目录下,可写成"/mysem"或"mysem",创建出来的文件都 是"/dev/shm/sem.mysem",mode设置为0666,value设置为信号量的初始值.所需信号灯等已存在条件下指定O_CREAT|O_EXCL却是个错误。

关闭信号量,进程终止时,会自动调用它

int sem_close(sem_t *sem);

成功返回0;失败返回-1,设置errno

删除信号量,立即删除信号量名字,当其他进程都关闭它时,销毁它

int sem_unlink(const char *name);

等待信号量,测试信号量的值,如果其值小于或等于0,那么就等待(阻塞);一旦其值变为大于0就将它减1,并返回

int sem_wait(sem_t *sem);

int sem_trywait(sem_t *sem);

成功返回0;失败返回-1,设置errno

当信号量的值为0时,sem_trywait立即返回,设置errno为EAGAIN。如果被某个信号中断,sem_wait会过早地返回,设置errno为EINTR

发出信号量,给它的值加1,然后唤醒正在等待该信号量的进程或线程

int sem_post(sem_t *sem);

成功返回0;失败返回-1,不会改变它的值,设置errno,该函数是异步信号安全的,可以在信号处理程序里调用它无名信号量,用于进程体内各线程间的互斥和同步,使用如下API(无名信号量,基于内存的信号量)

(1)、sem_init

功能:用于创建一个信号量,并初始化信号量的值。

头文件:

函数原型: int sem_init (sem_t* sem, int pshared, unsigned int value);

函数传入值: sem:信号量。pshared:决定信号量能否在几个进程间共享。由于目前LINUX还没有实现进程间共享信息量,所以这个值只能取0。

(2)其他函数。

int sem_wait (sem_t* sem);

int sem_trywait (sem_t* sem);

int sem_post (sem_t* sem);

int sem_getvalue (sem_t* sem);

int sem_destroy (sem_t* sem);

功能:sem_wait和sem_trywait相当于P操作,它们都能将信号量的值减一,两者的区别在于若信号量的值小于零时,sem_wait将会阻塞进程,而sem_trywait则会立即返回。sem_post相当于V操作,它将信号量的值加一,同时发出唤醒的信号给等待的进程(或线程)。

sem_getvalue 得到信号量的值。

sem_destroy 摧毁信号量。

如果某个基于内存的信号灯是在不同进程间同步的,该信号灯必须存放在共享内存区中,这要只要该共享内存区存在,该信号灯就存在。

互斥锁(又名互斥量)强调的是资源的访问互斥:互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的”

也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。

在linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化:

对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER, 或者调用pthread_mutex_init.

对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy.

原型:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restric attr);

int pthread_mutex_destroy(pthread_mutex_t *mutex);

头文件:

返回值: 成功则返回0, 出错则返回错误编号.

说明: 如果使用默认的属性初始化互斥量, 只需把attr设为NULL. 其他值在以后讲解.

首先说一下加锁函数:

头文件:

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

返回值: 成功则返回0, 出错则返回错误编号.

说 明: 具体说一下trylock函数, 这个函数是非阻塞调用模式, 也就是说, 如果互斥量没被锁住, trylock函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量 被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态.

再说一下解所函数:

头文件:

原型: int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值: 成功则返回0, 出错则返回错误编号.

条件变量常与互斥锁同时使用,达到线程同步的目的:条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。在发 送信号时,如果没有线程 等待在该条件变量上,那么信号将丢失;而信号量有计数值,每次信号量post操作都会被记录

  1. 互斥锁必须是谁上锁就由谁来解锁,而信号量的wait和post操作不必由同一个线程执行。

  2. 2. 互斥锁要么被锁住,要么被解开,和二值信号量类似

  3. 3. sem_post是各种同步技巧中,唯一一个能在信号处理程序中安全调用的函数

  4. 4. 互斥锁是为上锁而优化的;条件变量是为等待而优化的; 信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性

  5. 5. 互斥锁,条件变量都只用于同一个进程的各线程间,而信号量(有名信号量)可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。

  6. 6. 信号量有计数值,每次信号量post操作都会被记录,而条件变量在发送信号时,如果没有线程在等待该条件变量,那么信号将丢失。

  7. 读写锁

  8. 读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。

  9. 读写锁可以由三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写

  10. 锁。

  11. 在读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞直到所有的线程释放读锁。虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

  12. 读写锁非常适合于对数据结构读的次数远大于写的情况。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为当前只有一个线程可以在写模式下拥 有这个锁。当读写锁在读状态下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结构可以被多个获得读模式锁的线程读取。

  13. 读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当他以写模式锁住时,它是以独占模式锁住的。

  14. 初始化和销毁:

  15. #include

  16. int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

  17. int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

  18. 成功则返回0, 出错则返回错误编号.

  19. 同互斥量以上, 在释放读写锁占用的内存之前, 需要先通过thread_rwlock_destroy对读写锁进行清理工作, 释放由init分配的资源.

  20. 读和写:

  21. #include

  22. int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

  23. int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

  24. int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

  25. 成功则返回0, 出错则返回错误编号.

  26. 这3个函数分别实现获取读锁, 获取写锁和释放锁的操作. 获取锁的两个函数是阻塞操作, 同样, 非阻塞的函数为:

  27. #include

  28. int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

  29. int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

  30. 成功则返回0, 出错则返回错误编号.

  31. 非阻塞的获取锁操作, 如果可以获取则返回0, 否则返回错误的EBUSY.

  32. 虽然读写锁提高了并行性,但是就速度而言并不比互斥量快.

  33. 可能这也是即使有读写锁存在还会使用互斥量的原因,因为他在速度方面略胜一筹。这就需要我们在写程序的时候综合考虑速度和并行性并找到一个折中。

  34. 比如: 假设使用互斥量需要0.5秒,使用读写锁需要0.8秒。在类似学生管理系统这类中,可能百分之九十的时间都是查询操作,那么假如现在突然来个个20个请求,如果使用的是互斥量,那么最后的那个查询请求被满足需要10后。这样,估计没人能受得了。而使用读写锁,应为 读锁能够多次获得。所以所有的20个请求,每个请求都能在1秒左右得到满足。

  35. 也就是说,在一些写操作比较多或是本身需要同步的地方并不多的程序中我们应该使用互斥量,而在读操作远大于写操作的一些程序中我们应该使用读写锁来进行同步

  36. 条件变量(condition)

  37. 条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

  38. 条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其它线程在获得互斥量之前不会察觉到这种改变,因此必须锁定互斥量以后才能计算条件。

  39. 条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件

  40. 变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

  41. 1. 初始化:

  42. 条件变量采用的数据类型是pthread_cond_t, 在使用之前必须要进行初始化, 这包括两种方式:

  43. 静态: 可以把常量PTHREAD_COND_INITIALIZER给静态分配的条件变量.

  44. 动态: pthread_cond_init函数, 是释放动态条件变量的内存空间之前, 要用pthread_cond_destroy对其进行清理.

  45. #include

  46. int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);

  47. int pthread_cond_destroy(pthread_cond_t *cond);

  48. 成功则返回0, 出错则返回错误编号.

  49. 注意:条件变量占用的空间并未被释放。

  50. 当pthread_cond_init的attr参数为NULL时, 会创建一个默认属性的条件变量; 非默认情况以后讨论.

  51. 2. 等待条件:

  52. #include

  53. int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restric mutex);

  54. int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);

  55. 成功则返回0, 出错则返回错误编号.

  56. 这两个函数分别是阻塞等待和超时等待.

  57. 等待条件函数等待条件变为真, 传递给pthread_cond_wait的互斥量对条件进行保护, 调用者把锁住的互斥量传递给函数. 函数把调用线程放到等待条件的线程列表上, 然后对互斥量解锁, 这两个操作是原子的. 这样 便关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道, 这样线程就不会错过条件的任何变化.

  58. 当pthread_cond_wait返回时, 互斥量再次被锁住.

  59. pthread_cond_wait函数的返回并不意味着条件的值一定发生了变化,必须重新检查条件的值。

  60. pthread_cond_wait函数返回时,相应的互斥锁将被当前线程锁定,即使是函数出错返回。

  61. 阻塞在条件变量上的线程被唤醒以后,直到pthread_cond_wait()函数返回之前条件的值都有可能发生变化。所以函数返回以后,在锁定相应的互斥锁之前,必须重新测试条 件值。最好的测试方法是循环调用pthread_cond_wait函数,并把满足条件的表达式置为循环的终止条件。如:

  62. pthread_mutex_lock();

  63. while (condition_is_false)

  64. pthread_cond_wait();

  65. pthread_mutex_unlock();

  66. 阻塞在同一个条件变量上的不同线程被释放的次序是不一定的。

  67. 注意:pthread_cond_wait()函数是退出点,如果在调用这个函数时,已有一个挂起的退出请求,且线程允许退出,这个线程将被终止并开始执行善后处理函数,而这时和条 件变量相关的互斥锁仍将处在锁定状态。

  68. pthread_cond_timedwait函数到了一定的时间,即使条件未发生也会解除阻塞。这个时间由参数abstime指定。函数返回时,相应的互斥锁往往是锁定的,即使是函数出错返回。

  69. 注意:pthread_cond_timedwait函数也是退出点。

  70. 超时时间参数是指一天中的某个时刻。使用举例:

  71. pthread_timestruc_t to;

  72. to.tv_sec = time(NULL) + TIMEOUT;

  73. to.tv_nsec = 0;

  74. 超时返回的错误码是ETIMEDOUT。

  75. 3. 通知条件:

  76. #include

  77. int pthread_cond_signal(pthread_cond_t *cond);

  78. int pthread_cond_broadcast(pthread_cond_t *cond);

  79. 成功则返回0, 出错则返回错误编号.

  80. 这两个函数用于通知线程条件已经满足. 调用这两个函数, 也称向线程或条件发送信号. 必须注意, 一定要在改变条件状态以后再给线程发送信号.

B. linux 多线程环境下的几种锁机制

NO1
互斥量(Mutex)
互斥量是实现最简单的锁类型,因此有一些教科书一般以互斥量为例对锁原语进行描述。互斥量的释放并不仅仅依赖于释放操作,还可以引入一个定皮返时器属性。如果在释放操作执行前发生定时器超时,则互斥量也会释放代码块或共享存储区供其他线程访问。当有异常发生时,可使用try-finally语句来确保互斥量被释放。定时器状态或try-finally语句的使用可以避免产生死锁。

递归锁(Recursive
Lock)
递归锁是指可以被当前持有该锁的线程重复获取,而不会导致该线程产生死锁的锁类型。对递归锁而言,只有在当前持有线程的获取锁操作都有一个释放操作与之对应时,其他线程才可以获取该锁。因此,在使用递归锁时,必须要用足够的释放锁操作来平衡获取锁操作,实现这一目标的最佳方式是在单入口单出口代码块的两头一一对应地使用获取、释放操作,做法和在普通锁中一样。递归锁在递归函数中最有用。但是,总的来说,递归锁比非递归锁速度要慢。需要注意的是:调用线程获得几次递归锁必须释放几次递归锁。

以下为一个递归锁的示例:

[cpp] view plain
Recursive_Lock L

void recursiveFunction (int count) {

L->acquire()

if (count > 0) {

count = count - 1;

recursiveFunction(count);

}

L->release();

}

读写锁(Read-Write
lock) 读写锁又称为共享独占锁(shared-exclusive
lock)、多读单写锁(multiple-read/single-write lock)或者非互斥信号量(non-mutual
exclusion
semaphore)。读写锁允许多个线程同时进行读访问,但是在某一时刻却最多只能由一个线程执行写操作。对于多个线程需要同时读共享数据却并不一定进行写操作的应用来说,读写锁是一种高效的同步机制。对于较长的共享数据,只为其设置一个读写锁会导致较长的访问时间,最好将其划分为多个小段并设置多个读写锁以进行同步。

这个读写锁我们在学习数据库的时候应该很熟悉的哟!

旋转锁(Spin
Lock)
旋转锁是一种非阻塞锁,由某个线程独占。腔指采伍握配用旋转锁时,等待线程并不静态地阻塞在同步点,而是必须“旋转”,不断尝试直到最终获得该锁。旋转锁多用于多处理器系统中。这是因为,如果在单核处理器中采用旋转锁,当一个线程正在“旋转”时,将没有执行资源可供另一释放锁的线程使用。旋转锁适合于任何锁持有时间少于将一个线程阻塞和唤醒所需时间的场合。线程控制的变更,包括线程上下文的切换和线程数据结构的更新,可能比旋转锁需要更多的指令周期。旋转锁的持有时间应该限制在线程上下文切换时间的50%到100%之间(Kleiman,1996年)。在线程调用其他子系统时,线程不应持有旋转锁。对旋转锁的不当使用可能会导致线程饿死,因此需谨慎使用这种锁机制。旋转锁导致的饿死问题可使用排队技术来解决,即每个等待线程按照先进先出的顺序或者队列结构在一个独立的局部标识上进行旋转。

学习了这些,果然受益匪浅,在今后的coding中,我得挨个试试咯。

C. linux 互斥锁和读写锁的区别与联系

信号量与互斥锁之间的区别:
1. 互斥量用于线程的互斥,信号量用于线程的同步。
这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
2. 互斥量值只能为0/1,信号量值可以为非负整数。
也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
3. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

D. linux有没有多进程间的读写锁

Linux共享内存可以不用加锁,不过需要一种机制来标记共享内存的读写状态;也就是说要让两个进程知道:1)负责写入的进程,必须知道当前共享内存是否可以写入,上一次的写入内容是否有被负责读取的进程读走;2)负责读取的进程,必须知道当前共享内存是否需要读取,防止重复读取。一般的这种标记机制是通过以下方式来简单实现:1)通过读写锁来控制;2)共享内存上设置一个地方,专门存放当前共享内存的读写状态;

E. linux rcu锁问题怎么查

众所周知,为了保护共享数据,需要一些同步机制,如自旋锁(spinlock),读写锁(rwlock),它们使用起来非常简单,而且是一种很有效的同步机制,在UNIX系统和Linux系统中得到了广泛的使用。但是随着计算机硬件的快速发展,获得这种锁的开销相对于CPU的速度在成倍地增加,原因很简单,CPU的速度与访问内存的速度差距越来越大,而这种锁使用了原子操作指令,它需要原子地访问内存,也就说获得锁的开销与访存速度相关,另外在大部分非x86架构上获取锁使用了内存栅(Memory Barrier),这会导致处理器流水线停滞或刷新,因此它的开销相对于CPU速度而言就越来越大。

在操作系统中,数据一致性访问是一个非常重要的部分,通常我们可以采用锁机制实现数据的一致性访问。例如,semaphore、spinlock机制,在访问共享数据时,首先访问锁资源,在获取锁资源的前提下才能实现数据的访问。这种原理很简单,根本的思想就是在访问临界资源时,首先访问一个全局的变量(锁),通过全局变量的状态来控制线程对临界资源的访问。但是,这种思想是需要硬件支持的,硬件需要配合实现全局变量(锁)的读-修改-写,现代CPU都会提供这样的原子化指令。采用锁机制实现数据访问的一致性存在如下两个问题:

1、 效率问题。锁机制的实现需要对内存的原子化访问,这种访问操作会破坏流水线操作,降低了流水线效率。这是影响性能的一个因素。另外,在采用读写锁机制的情况下,写锁是排他锁,无法实现写锁与读锁的并发操作,在某些应用下回降低性能。

2、 扩展性问题。当系统中CPU数量增多的时候,采用锁机制实现数据的同步访问效率偏低。并且随着CPU数量的增多,效率降低,由此可见锁机制实现的数据一致性访问扩展性差。

为了解决上述问题,Linux中引进了RCU机制。该机制在多CPU的平台上比较适用,对于读多写少的应用尤其适用。RCU的思路实际上很简单,下面对其进行描述:

1、 对于读操作,可以直接对共享资源进行访问,但是前提是需要CPU支持访存操作的原子化,现代CPU对这一点都做了保证。但是RCU的读操作上下文是不可抢占的(这一点在下面解释),所以读访问共享资源时可以采用read_rcu_lock(),该函数的工作是停止抢占。

2、 对于写操作,其需要将原来的老数据作一次备份(),然后对备份数据进行修改,修改完毕之后再用新数据更新老数据,更新老数据时采用了rcu_assign_pointer()宏,在该函数中首先屏障一下memory,然后修改老数据。这个操作完成之后,需要进行老数据资源的回收。操作线程向系统注册回收方法,等待回收。采用数据备份的方法可以实现读者与写者之间的并发操作,但是不能解决多个写着之间的同步,所以当存在多个写者时,需要通过锁机制对其进行互斥,也就是在同一时刻只能存在一个写者。

3、 在RCU机制中存在一个垃圾回收的daemon,当共享资源被update之后,可以采用该daemon实现老数据资源的回收。回收时间点就是在update之前的所有的读者全部退出。由此可见写者在update之后是需要睡眠等待的,需要等待读者完成操作,如果在这个时刻读者被抢占或者睡眠,那么很可能会导致系统死锁。因为此时写者在等待读者,读者被抢占或者睡眠,如果正在运行的线程需要访问读者和写者已经占用的资源,那么死锁的条件就很有可能形成了。

F. Linux进程间通信(互斥锁、条件变量、读写锁、文件锁、信号灯)

为了能够有效的控制多个进程之间的沟通过程,保证沟通过程的有序和和谐,OS必须提供一定的同步机制保证进程之间不会自说自话而是有效的协同工作。比如在 共享内存的通信方式中,两个或者多个进程都要对共享的内存进行数据写入,那么怎么才能保证一个进程在写入的过程中不被其它的进程打断,保证数据的完整性 呢?又怎么保证读取进程在读取数据的过程中数据不会变动,保证读取出的数据是完整有效的呢?

常用的同步方式有: 互斥锁、条件变量、读写锁、记录锁(文件锁)和信号灯.

互斥锁:

顾名思义,锁是用来锁住某种东西的,锁住之后只有有钥匙的人才能对锁住的东西拥有控制权(把锁砸了,把东西偷走的小偷不在我们的讨论范围了)。所谓互斥, 从字面上理解就是互相排斥。因此互斥锁从字面上理解就是一点进程拥有了这个锁,它将排斥其它所有的进程访问被锁住的东西,其它的进程如果需要锁就只能等待,等待拥有锁的进程把锁打开后才能继续运行。 在实现中,锁并不是与某个具体的变量进行关联,它本身是一个独立的对象。进(线)程在有需要的时候获得此对象,用完不需要时就释放掉。

互斥锁的主要特点是互斥锁的释放必须由上锁的进(线)程释放,如果拥有锁的进(线)程不释放,那么其它的进(线)程永远也没有机会获得所需要的互斥锁。

互斥锁主要用于线程之间的同步。

条件变量:

上文中提到,对于互斥锁而言,如果拥有锁的进(线)程不释放锁,其它进(线)程永远没机会获得锁,也就永远没有机会继续执行后续的逻辑。在实际环境下,一 个线程A需要改变一个共享变量X的值,为了保证在修改的过程中X不会被其它的线程修改,线程A必须首先获得对X的锁。现在假如A已经获得锁了,由于业务逻 辑的需要,只有当X的值小于0时,线程A才能执行后续的逻辑,于是线程A必须把互斥锁释放掉,然后继续“忙等”。如下面的伪代码所示:

1.// get x lock

2.while(x

G. Linux下各种锁的理解和使用及总结解决epoll惊群问题(面试常考)-

锁出现的原因

临界资源是什么: 多线程执行流所共享的资源

锁的作用是什么, 可以做原子操作, 在多线程中针对临界资源的互斥访问... 保证一个时刻只有一个线程可以持有锁对于临界资源做修改操作...

任何一个线程如果需要修改,向临界资源做写入操作都必须持有锁,没有持有锁就不能对于临界资源做写入操作.

锁 : 保证同一时刻只能有一个线程对于临界资源做写入操作 (锁地功能)

再一个直观地代码引出问题,再从指令集的角度去看问题

上述一个及其奇怪的结果,这个结果每一次运行都可能是不一样的,Why ? 按照我们本来的想法是每一个线程 + 20000000 结果肯定应该是60000000呀,可以就是达不到这个值

为何? (深入汇编指令来看) 一定将过程放置到汇编指令上去看就可以理解这个过程了.

a++; 或者 a += 1; 这些操作的汇编操作是几个步骤?

其实是三个步骤:

正常情况下,数据少,操作的线程少,问题倒是不大,想一想要是这样的情况下,操作次数大,对齐操作的线程多,有些线程从中间切入进来了,在运算之后还没写回内存就另外一个线程切入进来同时对于之前的数据进行++ 再写回内存, 啥效果,多次++ 操作之后结果确实一次加加操作后的结果。 这样的操作 (术语叫做函数的重入) 我觉得其实就是重入到了汇编指令中间了,还没将上一次运算的结果写回内存就重新对这个内存读取再运算写入,结果肯定和正常的逻辑后的结果不一样呀

来一幅图片解释一下

咋办? 其实问题很清楚,我们只需要处理的是多条汇编指令不能让它中间被插入其他的线程运算. (要想自己在执行汇编指令的时候别人不插入进来) 将多条汇编指令绑定成为一条指令不就OK了嘛。

也就是原子操作!!!

不会原子操作?操作系统给咱提供了线程的 绑定方式工具呀:mutex 互斥锁(互斥量), 自旋锁(spinlock), 读写锁(readers-writer lock) 他们也称作悲观锁. 作用都是一个样,将多个汇编指令锁成为一条原子操作 (此处的汇编指令也相当于如下的临界资源)

悲观锁:锁如其名,每次都悲观地认为其他线程也会来修改数据,进行写入操作,所以会在取数据前先加锁保护,当其他线程想要访问数据时,被阻塞挂起

乐观锁:每次取数据的时候,总是乐观地认为数据不会被其他线程修改,因此不上锁。但是在更新数据前, 会判断其他数据在更新前有没有对数据进行修改。

互斥锁

最为常见使用地锁就是互斥锁, 也称互斥量. mutex

特征,当其他线程持有互斥锁对临界资源做写入操作地时候,当前线程只能挂起等待,让出CPU,存在线程间切换工作

解释一下存在线程间切换工作 : 当线程试图去获取锁对临界资源做写入操作时候,如果锁被别的线程正在持有,该线程会保存上下文直接挂起,让出CPU,等到锁被释放出来再进行线程间切换,从新持有CPU执行写入操作

互斥锁需要进行线程间切换,相比自旋锁而言性能会差上许多,因为自旋锁不会让出CPU, 也就不需要进行线程间切换的步骤,具体原理下一点详述

加互斥量(互斥锁)确实可以达到要求,但是会发现运行时间非常的长,因为线程间不断地切换也需要时间, 线程间切换的代价比较大.

相关视频推荐

你绕不开的组件—锁,4个方面手撕锁的多种实现

“惊群”原理、锁的设计方案及绕不开的“死锁”问题

学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括 C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等),免费分享

自旋锁

spinlock.自旋锁.

对比互斥量(互斥锁)而言,获取自旋锁不需要进行线程间切换,如果自旋锁正在被别的线程占用,该线程也不会放弃CPU进行挂起休眠,而是恰如其名的在哪里不断地循环地查看自旋锁保持者(持有者)是否将自旋锁资源释放出来... (自旋地原来就是如此)

口语解释自旋:持有自旋锁的线程不释放自旋锁,那也没有关系呀,我就在这里不断地一遍又一遍地查询自旋锁是否释放出来,一旦释放出来我立马就可以直接使用 (因为我并没有挂起等待,不需要像互斥锁还需要进行线程间切换,重新获取CPU,保存恢复上下文等等操作)

哪正是因为上述这些特点,线程尝试获取自旋锁,获取不到不会采取休眠挂起地方式,而是原地自旋(一遍又一遍查询自旋锁是否可以获取)效率是远高于互斥锁了. 那我们是不是所有情况都使用自旋锁就行了呢,互斥锁就可以放弃使用了吗????

解释自旋锁地弊端:如果每一个线程都仅仅只是需要短时间获取这个锁,那我自旋占据CPU等待是没啥问题地。要是线程需要长时间地使用占据(锁)。。。 会造成过多地无端占据CPU资源,俗称站着茅坑不拉屎... 但是要是仅仅是短时间地自旋,平衡CPU利用率 + 程序运行效率 (自旋锁确实是在有些时候更加合适)

自旋锁需要场景:内核可抢占或者SMP(多处理器)情况下才真正需求 (避免死锁陷入死循环,疯狂地自旋,比如递归获取自旋锁. 你获取了还要获取,但是又没法释放)

自旋锁的使用函数其实和互斥锁几乎是一摸一样地,仅仅只是需要将所有的mutex换成spin即可

仅仅只是在init存在些许不同

何为惊群,池塘一堆, 我瞄准一条插过去,但是好似所有的都像是觉着自己正在被插一样的四处逃窜。 这个就是惊群的生活一点的理解

惊群现象其实一点也不少,比如说 accept pthread_cond_broadcast 还有多个线程共享epoll监视一个listenfd 然后此刻 listenfd 说来 SYN了,放在了SYN队列中,然后完成了三次握手放在了 accept队列中了, 现在问题是这个connect我应该交付给哪一个线程处理呢.

多个epoll监视准备工作的线程 就是这群 (),然后connet就是鱼叉,这一叉下去肯定是所有的 epoll线程都会被惊醒 (多线程共享listenfd引发的epoll惊群)

同样如果将上述的多个线程换成多个进程共享监视 同一个 listenfd 就是(多进程的epoll惊群现象)

咱再画一个草图再来理解一下这个惊群:

如果是多进程道理是一样滴,仅仅只是将所有的线程换成进程就OK了

终是来到了今天的正题了: epoll惊群问题地解决上面了...

首先 先说说accept的惊群问题,没想到吧accept 平时大家写它的多线程地时候,多个线程同时accept同一个listensock地时候也是会存在惊群问题地,但是accept地惊群问题已经被Linux内核处理了: 当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理

但是对于epoll的惊群问题,内核却没有直接进行处理。哪既然内核没有直接帮我们处理,我们应该如何针对这种现象做出一定的措施呢?

惊群效应带来的弊端: 惊群现象会造成epoll的伪唤醒,本来epoll是阻塞挂起等待着地,这个时候因为挂起等待是不会占用CPU地。。。 但是一旦唤醒就会占用CPU去处理发生地IO事件, 但是其实是一个伪唤醒,这个就是对于线程或者进程的无效调度。然而进程或者线程地调取是需要花费代价地,需要上下文切换。需要进行进程(线程)间的不断切换... 本来多核CPU是用来支持高并发地,但是现在却被用来无效地唤醒,对于多核CPU简直就是一种浪费 (浪费系统资源) 还会影响系统的性能.

解决方式(一般是两种)

Nginx的解决方式:

加锁:惊群问题发生的前提是多个进程(线程)监听同一个套接字(listensock)上的事件,所以我们只让一个进程(线程)去处理监听套接字就可以了。

画两张图来理解一下:

上述还没有进行一个每一个进程都对应一个listensock 而是多线程共享一个listensock 运行结果如下

所有的线程同时被唤醒了,但是实际上会处理连接的仅仅只是一个线程,

咱仅仅只是将主线程做如上这样一个简单的修改,每一个线程对应一个listensock;每一个线程一个独有的监视窗口,将问题抛给内核去处理,让内核去负载均衡 : 结果如下

仅仅唤醒一个线程来进行处理连接,解决了惊群问题

本文通过介绍两种锁入手,以及为什么需要锁,锁本质就是为了保护,持有锁你就有权力有能力操作写入一定的临界保护资源,没有锁你就不行需要等待,本质其实是将多条汇编指令绑定成原子操作

然后介绍了惊群现象,通过一个巧妙地例子,扔一颗石子,只是瞄准一条鱼扔过去了,但是整池鱼都被惊醒了,

对应我们地实际问题就是, 多个线程或者进程共同监视同一个listensock。。。。然后IO连接事件到来地时候本来仅仅只是需要一个线程醒过来处理即可,但是却会使得所有地线程(进程)全部醒过来,造成不必要地进程线程间切换,多核CPU被浪费喔,系统资源被浪费

处理方式 一。 Nginx 源码加互斥锁处理。。 二。设置SO_REUSEPORT, 使得多个进程线程可以同时连接同一个port , 为每一个进程线程搞一个listensock... 将问题抛给内核去处理,让他去负载均衡地仅仅将IO连接事件分配给一个进程或线程

H. linux rcu原理

RCU, Read-Copy-Update,是Linux内核中的一种同步机制。RCU常被描述为读写锁的替代品,它的特点是读者并不需要直接与写者进行同步,读者与写者也能并发的执行。

来一张图片来描述下大体的操作吧羡薯:

多个读者可以并发访问临界资源,同时使用rcu_read_lock/rcu_read_unlock来标定临界区;
写者(updater)在更新临界资源的时候,拷贝一份副本作为基础进行修改,当所有读者离开临界区后,把指向旧临界资源的指针指向更新后的副本,并对旧资源进行回收处理;
图中只显兄羡者示一个写者,当存在多个写者的时候,需要在写者之派模间进行互斥处理。

I. Linux C++多线程同步的四种方式

From : https://blog.csdn.net/qq_39382769/article/details/96075346

1.同一个线程内部,指令按照先后顺序执行;但不同线程之间的指令很难说清楚是哪一个先执行,在并发情况下,指令执行的先后顺序由内核决定。

如果运行的结果依赖于不同线程执行的先后的话,那么就会形成竞争条件,在这样的情况下,计算的结果很难预知,所以应该尽量避免竞争条件的形成。

2.最常见的解决竞争条件的方法是:将原先分离的两个指令构成一个不可分割的原子操作,而其他任务不能插入到原子操作中!

3.对多线程来说,同步指的是在一定时间内只允许某一个线程访问某个资源,而在此时间内,不允许其他线程访问该资源!

互斥锁
条件变量
读写锁
信号量

一种特殊的全局变量,拥有lock和unlock两种状态。
unlock的互斥锁可以由某个线程获得,一旦获得,这个互斥锁会锁上变成lock状态,此后只有该线程由权力打开该锁,其他线程想要获得互斥锁,必须得到互斥锁再次被打开之后。

1.互斥锁的初始化, 分为静态初始化和动态初始化.

2.互斥锁的相关属性及分类

(1) attr表示互斥锁的属性;

(2) pshared表示互斥锁的共享属性,由两种取值:

1)PTHREAD_PROCESS_PRIVATE:锁只能用于一个进程内部的两个线程进行互斥(默认情况)

2)PTHREAD_PROCESS_SHARED:锁可用于两个不同进程中的线程进行互斥,使用时还需要在进程共享内存中分配互斥锁,然后为该互斥锁指定属性就可以了。

互斥锁存在缺点:
(1)某个线程正在等待共享数据内某个条件出现。

(2)重复对数据对象加锁和解锁(轮询),但是这样轮询非常耗费时间和资源,而且效率非常低,所以互斥锁不太适合这种情况。

当线程在等待满足某些条件时,使线程进入睡眠状态;一旦条件满足,就换线因等待满足特定条件而睡眠的线程。
程序的效率无疑会大大提高。

1)创建

静态方式:pthread_cond_t cond PTHREAD_COND_INITIALIZER

动态方式:int pthread_cond_init(&cond,NULL)

Linux thread 实现的条件变量不支持属性,所以NULL(cond_attr参数)

2)注销

int pthread_cond_destory(&cond)

只有没有线程在该条件变量上,该条件变量才能注销,否则返回EBUSY

因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程!(请参考条件变量的底层实现)

3)等待

条件等待:int pthread_cond_wait(&cond,&mutex)

计时等待:int pthread_cond_timewait(&cond,&mutex,time)

1.其中计时等待如果在给定时刻前条件没有被满足,则返回ETIMEOUT,结束等待

2.无论那种等待方式,都必须有一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait形成竞争条件!

3.在调用pthread_cond_wait前必须由本线程加锁

4)激发

激发一个等待线程:pthread_cond_signal(&cond)

激发所有等待线程:pthread_cond_broadcast(&cond)

重要的是,pthread_cond_signal不会存在惊群效应,也就是是它最多给一个等待线程发信号,不会给所有线程发信号唤醒,然后要求他们自己去争抢资源!

pthread_cond_broadcast() 唤醒所有正在pthread_cond_wait()的同一个条件变量的线程。注意:如果等待的多个现场不使用同一个锁,被唤醒的多个线程执行是并发的。

pthread_cond_broadcast & pthread_cond_signal

1.读写锁比互斥锁更加具有适用性和并行性

2.读写锁最适用于对数据结构的读操作读操作次数多余写操作次数的场合!

3.锁处于读模式时可以线程共享,而锁处于写模式时只能独占,所以读写锁又叫做共享-独占锁。

4.读写锁有两种策略:强读同步和强写同步

强读同步:
总是给读者更高的优先权,只要写者没有进行写操作,读者就可以获得访问权限

强写同步:
总是给写者更高的优先权,读者只能等到所有正在等待或者执行的写者完成后才能进行读

1)初始化的销毁读写锁

静态初始化:pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER

动态初始化:int pthread_rwlock_init(rwlock,NULL),NULL代表读写锁采用默认属性

销毁读写锁:int pthread_rwlock_destory(rwlock)

在释放某个读写锁的资源之前,需要先通过pthread_rwlock_destory函数对读写锁进行清理。释放由pthread_rwlock_init函数分配的资源

如果你想要读写锁使用非默认属性,则attr不能为NULL,得给attr赋值

int pthread_rwlockattr_init(attr),给attr初始化

int pthread_rwlockattr_destory(attr),销毁attr

2)以写的方式获取锁,以读的方式获取锁,释放读写锁

int pthread_rwlock_rdlock(rwlock),以读的方式获取锁

int pthread_rwlock_wrlock(rwlock),以写的方式获取锁

int pthread_rwlock_unlock(rwlock),释放锁

上面两个获取锁的方式都是阻塞的函数,也就是说获取不到锁的话,调用线程不是立即返回,而是阻塞执行,在需要进行写操作的时候,这种阻塞式获取锁的方式是非常不好的,你想一下,我需要进行写操作,不但没有获取到锁,我还一直在这里等待,大大拖累效率

所以我们应该采用非阻塞的方式获取锁:

int pthread_rwlock_tryrdlock(rwlock)

int pthread_rwlock_trywrlock(rwlock)

互斥锁只允许一个线程进入临界区,而信号量允许多个线程进入临界区。

1)信号量初始化
int sem_init(&sem,pshared, v)
pshared为0,表示这个信号量是当前进程的局部信号量。
pshared为1,表示这个信号量可以在多个进程之间共享。
v为信号量的初始值。

返回值:
成功:0,失败:-1

2)信号量值的加减
int sem_wait(&sem):以原子操作的方式将信号量的值减去1

int sem_post(&sem):以原子操作的方式将信号量的值加上1

3)对信号量进行清理

int sem_destory(&sem)

阅读全文

与读写锁clinux相关的资料

热点内容
单片机编程取反 浏览:897
51单片机课程设计课题 浏览:900
手机淘宝登录怎么加密码 浏览:485
linux快捷方式图标 浏览:38
阳光车险的app叫什么名字 浏览:462
购买单片机的器件时需要给商家啥 浏览:535
并行编译技术的发展 浏览:550
阿里云服务器安装管理 浏览:551
java手机开发教程 浏览:675
我的世界怎么删除服务器数据 浏览:672
linux内存子系统 浏览:973
加密思维币 浏览:691
魅族访客文件夹 浏览:53
添加的文件夹怎么找 浏览:618
程序员涉黄 浏览:701
maven编译resources下的js 浏览:522
ubuntu文件移动命令 浏览:230
安卓i怎么查找苹果手机 浏览:952
云服务器宕机概率 浏览:233
在线买药用什么app知乎 浏览:816