linuxmutex
mutex相关的函数并不是linux kernel实现的,而是glibc实现的,源码位于nptl目录下。
http://ftp.gnu.org/pub/gnu/glibc/glibc-2.3.5.tar.gz
首先说数据结构:
typedef union
{
struct
{
int __lock;
unsigned int __count;
int __owner;
unsigned int __nusers;
/* KIND must stay at this position in the structure to maintain
binary compatibility. */
int __kind;
int __spins;
} __data;
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;
int __lock; 资源竞争引用计数
int __kind; 锁类型,init 函数中mutexattr 参数传递,该参数可以为NULL,一般为 PTHREAD_MUTEX_NORMAL
结构体其他元素暂时不了解,以后更新。
/*nptl/pthread_mutex_init.c*/
int
__pthread_mutex_init (mutex, mutexattr)
pthread_mutex_t *mutex;
const pthread_mutexattr_t *mutexattr;
{
const struct pthread_mutexattr *imutexattr;
assert (sizeof (pthread_mutex_t) <= __SIZEOF_PTHREAD_MUTEX_T);
imutexattr = (const struct pthread_mutexattr *) mutexattr ?: &default_attr;
/* Clear the whole variable. */
memset (mutex, '\0', __SIZEOF_PTHREAD_MUTEX_T);
/* Copy the values from the attribute. */
mutex->__data.__kind = imutexattr->mutexkind & ~0x80000000;
/* Default values: mutex not used yet. */
// mutex->__count = 0; already done by memset
// mutex->__owner = 0; already done by memset
// mutex->__nusers = 0; already done by memset
// mutex->__spins = 0; already done by memset
return 0;
}
init函数就比较简单了,将mutex结构体清零,设置结构体中__kind属性。
/*nptl/pthread_mutex_lock.c*/
int
__pthread_mutex_lock (mutex)
pthread_mutex_t *mutex;
{
assert (sizeof (mutex->__size) >= sizeof (mutex->__data));
pid_t id = THREAD_GETMEM (THREAD_SELF, tid);
switch (__builtin_expect (mutex->__data.__kind, PTHREAD_MUTEX_TIMED_NP))
{
…
default:
/* Correct code cannot set any other type. */
case PTHREAD_MUTEX_TIMED_NP:
simple:
/* Normal mutex. */
LLL_MUTEX_LOCK (mutex->__data.__lock);
break;
…
}
/* Record the ownership. */
assert (mutex->__data.__owner == 0);
mutex->__data.__owner = id;
#ifndef NO_INCR
++mutex->__data.__nusers;
#endif
return 0;
}
该函数主要是调用LLL_MUTEX_LOCK, 省略部分为根据mutex结构体__kind属性不同值做些处理。
宏定义函数LLL_MUTEX_LOCK最终调用,将结构体mutex的__lock属性作为参数传递进来
#define __lll_mutex_lock(futex) \
((void) ({ \
int *__futex = (futex); \
if (atomic_compare_and_exchange_bool_acq (__futex, 1, 0) != 0) \
__lll_lock_wait (__futex); \
}))
atomic_compare_and_exchange_bool_acq (__futex, 1, 0)宏定义为:
#define atomic_compare_and_exchange_bool_acq(mem, newval, oldval) \
({ __typeof (mem) __gmemp = (mem); \
__typeof (*mem) __gnewval = (newval); \
\
*__gmemp == (oldval) ? (*__gmemp = __gnewval, 0) : 1; })
这个宏实现的功能是:
如果mem的值等于oldval,则把newval赋值给mem,放回0,否则不做任何处理,返回1.
由此可以看出,当mutex锁限制的资源没有竞争时,__lock 属性被置为1,并返回0,不会调用__lll_lock_wait (__futex); 当存在竞争时,再次调用lock函数,该宏不做任何处理,返回1,调用__lll_lock_wait (__futex);
void
__lll_lock_wait (int *futex)
{
do
{
int oldval = atomic_compare_and_exchange_val_acq (futex, 2, 1);
if (oldval != 0)
lll_futex_wait (futex, 2);
}
while (atomic_compare_and_exchange_bool_acq (futex, 2, 0) != 0);
}
atomic_compare_and_exchange_val_acq (futex, 2, 1); 宏定义:
/* The only basic operation needed is compare and exchange. */
#define atomic_compare_and_exchange_val_acq(mem, newval, oldval) \
({ __typeof (mem) __gmemp = (mem); \
__typeof (*mem) __gret = *__gmemp; \
__typeof (*mem) __gnewval = (newval); \
\
if (__gret == (oldval)) \
*__gmemp = __gnewval; \
__gret; })
这个宏实现的功能是,当mem等于oldval时,将mem置为newval,始终返回mem原始值。
此时,futex等于1,futex将被置为2,并且返回1. 进而调用
lll_futex_wait (futex, 2);
#define lll_futex_timed_wait(ftx, val, timespec) \
({ \
DO_INLINE_SYSCALL(futex, 4, (long) (ftx), FUTEX_WAIT, (int) (val), \
(long) (timespec)); \
_r10 == -1 ? -_retval : _retval; \
})
该宏对于不同的平台架构会用不同的实现,采用汇编语言实现系统调用。不过确定的是调用了Linux kernel的futex系统调用。
futex在linux kernel的实现位于:kernel/futex.c
SYSCALL_DEFINE6(futex, u32 __user *, uaddr, int, op, u32, val,
struct timespec __user *, utime, u32 __user *, uaddr2,
u32, val3)
{
struct timespec ts;
ktime_t t, *tp = NULL;
u32 val2 = 0;
int cmd = op & FUTEX_CMD_MASK;
if (utime && (cmd == FUTEX_WAIT || cmd == FUTEX_LOCK_PI ||
cmd == FUTEX_WAIT_BITSET ||
cmd == FUTEX_WAIT_REQUEUE_PI)) {
if (_from_user(&ts, utime, sizeof(ts)) != 0)
return -EFAULT;
if (!timespec_valid(&ts))
return -EINVAL;
t = timespec_to_ktime(ts);
if (cmd == FUTEX_WAIT)
t = ktime_add_safe(ktime_get(), t);
tp = &t;
}
/*
* requeue parameter in 'utime' if cmd == FUTEX_*_REQUEUE_*.
* number of waiters to wake in 'utime' if cmd == FUTEX_WAKE_OP.
*/
if (cmd == FUTEX_REQUEUE || cmd == FUTEX_CMP_REQUEUE ||
cmd == FUTEX_CMP_REQUEUE_PI || cmd == FUTEX_WAKE_OP)
val2 = (u32) (unsigned long) utime;
return do_futex(uaddr, op, val, tp, uaddr2, val2, val3);
}
futex具有六个形参,pthread_mutex_lock最终只关注了前四个。futex函数对参数进行判断和转化之后,直接调用do_futex。
long do_futex(u32 __user *uaddr, int op, u32 val, ktime_t *timeout,
u32 __user *uaddr2, u32 val2, u32 val3)
{
int clockrt, ret = -ENOSYS;
int cmd = op & FUTEX_CMD_MASK;
int fshared = 0;
if (!(op & FUTEX_PRIVATE_FLAG))
fshared = 1;
clockrt = op & FUTEX_CLOCK_REALTIME;
if (clockrt && cmd != FUTEX_WAIT_BITSET && cmd != FUTEX_WAIT_REQUEUE_PI)
return -ENOSYS;
switch (cmd) {
case FUTEX_WAIT:
val3 = FUTEX_BITSET_MATCH_ANY;
case FUTEX_WAIT_BITSET:
ret = futex_wait(uaddr, fshared, val, timeout, val3, clockrt);
break;
…
default:
ret = -ENOSYS;
}
return ret;
}
省略部分为对其他cmd的处理,pthread_mutex_lock函数最终传入的cmd参数为FUTEX_WAIT,所以在此只关注此分之,分析futex_wait函数的实现。
static int futex_wait(u32 __user *uaddr, int fshared,
u32 val, ktime_t *abs_time, u32 bitset, int clockrt)
{
struct hrtimer_sleeper timeout, *to = NULL;
struct restart_block *restart;
struct futex_hash_bucket *hb;
struct futex_q q;
int ret;
… … //delete parameters check and convertion
retry:
/* Prepare to wait on uaddr. */
ret = futex_wait_setup(uaddr, val, fshared, &q, &hb);
if (ret)
goto out;
/* queue_me and wait for wakeup, timeout, or a signal. */
futex_wait_queue_me(hb, &q, to);
… … //other handlers
return ret;
}
futex_wait_setup 将线程放进休眠队列中,
futex_wait_queue_me(hb, &q, to);将本线程休眠,等待唤醒。
唤醒后,__lll_lock_wait函数中的while (atomic_compare_and_exchange_bool_acq (futex, 2, 0) != 0); 语句将被执行,由于此时futex在pthread_mutex_unlock中置为0,所以atomic_compare_and_exchange_bool_acq (futex, 2, 0)语句将futex置为2,返回0. 退出循环,访问用户控件的临界资源。
/*nptl/pthread_mutex_unlock.c*/
int
internal_function attribute_hidden
__pthread_mutex_unlock_usercnt (mutex, decr)
pthread_mutex_t *mutex;
int decr;
{
switch (__builtin_expect (mutex->__data.__kind, PTHREAD_MUTEX_TIMED_NP))
{
… …
default:
/* Correct code cannot set any other type. */
case PTHREAD_MUTEX_TIMED_NP:
case PTHREAD_MUTEX_ADAPTIVE_NP:
/* Normal mutex. Nothing special to do. */
break;
}
/* Always reset the owner field. */
mutex->__data.__owner = 0;
if (decr)
/* One less user. */
--mutex->__data.__nusers;
/* Unlock. */
lll_mutex_unlock (mutex->__data.__lock);
return 0;
}
省略部分是针对不同的__kind属性值做的一些处理,最终调用 lll_mutex_unlock。
该宏函数最终的定义为:
#define __lll_mutex_unlock(futex) \
((void) ({ \
int *__futex = (futex); \
int __val = atomic_exchange_rel (__futex, 0); \
\
if (__builtin_expect (__val > 1, 0)) \
lll_futex_wake (__futex, 1); \
}))
atomic_exchange_rel (__futex, 0);宏为:
#define atomic_exchange_rel(mem, value) \
(__sync_synchronize (), __sync_lock_test_and_set (mem, value))
实现功能为:将mem设置为value,返回原始mem值。
__builtin_expect (__val > 1, 0) 是编译器优化语句,告诉编译器期望值,也就是大多数情况下__val > 1 ?是0,其逻辑判断依然为if(__val > 1)为真的话执行 lll_futex_wake。
现在分析,在资源没有被竞争的情况下,__futex 为1,那么返回值__val则为1,那么 lll_futex_wake (__futex, 1); 不会被执行,不产生系统调用。 当资源产生竞争的情况时,根据对pthread_mutex_lock 函数的分析,__futex为2, __val则为2,执行 lll_futex_wake (__futex, 1); 从而唤醒等在临界资源的线程。
lll_futex_wake (__futex, 1); 最终会调动同一个系统调用,即futex, 只是传递的cmd参数为FUTEX_WAKE。
在linux kernel的futex实现中,调用
static int futex_wake(u32 __user *uaddr, int fshared, int nr_wake, u32 bitset)
{
struct futex_hash_bucket *hb;
struct futex_q *this, *next;
struct plist_head *head;
union futex_key key = FUTEX_KEY_INIT;
int ret;
if (!bitset)
return -EINVAL;
ret = get_futex_key(uaddr, fshared, &key);
if (unlikely(ret != 0))
goto out;
hb = hash_futex(&key);
spin_lock(&hb->lock);
head = &hb->chain;
plist_for_each_entry_safe(this, next, head, list) {
if (match_futex (&this->key, &key)) {
if (this->pi_state || this->rt_waiter) {
ret = -EINVAL;
break;
}
/* Check if one of the bits is set in both bitsets */
if (!(this->bitset & bitset))
continue;
wake_futex(this);
if (++ret >= nr_wake)
break;
}
}
spin_unlock(&hb->lock);
put_futex_key(fshared, &key);
out:
return ret;
}
该函数遍历在该mutex上休眠的所有线程,调用wake_futex进行唤醒,
static void wake_futex(struct futex_q *q)
{
struct task_struct *p = q->task;
/*
* We set q->lock_ptr = NULL _before_ we wake up the task. If
* a non futex wake up happens on another CPU then the task
* might exit and p would dereference a non existing task
* struct. Prevent this by holding a reference on p across the
* wake up.
*/
get_task_struct(p);
plist_del(&q->list, &q->list.plist);
/*
* The waiting task can free the futex_q as soon as
* q->lock_ptr = NULL is written, without taking any locks. A
* memory barrier is required here to prevent the following
* store to lock_ptr from getting ahead of the plist_del.
*/
smp_wmb();
q->lock_ptr = NULL;
wake_up_state(p, TASK_NORMAL);
put_task_struct(p);
}
wake_up_state(p, TASK_NORMAL); 的实现位于kernel/sched.c中,属于linux进程调度的技术。
2. Linux mutex为什么不能用在中断函数
Linux mutex不能用在中断函数原因:Backtrace来看,应该是i2c_transfer中调用mutex_lock导致schele调用。
pthread_mutex_lock(&qlock);表示尝试去把qlock上锁,它会先判断qlock是否已经上锁,如果已经上锁这个线程就会停在这一步直到其他线程把锁解开。它才继续运行。所以代码中要么是线程1先执行完后执行线程2,要么就是线程2先执行,再执行线程1.而线程3一开始就执行了。
中断函数防止方法:
要防止中断冲突,其实就是要知道什么设备容易产生中断冲突,只要知道了这点,在使用这些设备时稍微注意一下就可以了。下面我列出一些容易冲突的设备,希望对读者有用。
1、声卡:一些早期的ISA型声卡,系统很有可能不认,就需要用户手动设置(一般为5)。
2、内置调制解调器和鼠标:一般鼠标用COM1,内置调制解调器使用COM2的中断(一般为3),这时要注意此时COM2上不应有其它设备。
3. 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连接事件分配给一个进程或线程
4. linux下互斥锁mutex,貌似锁不上呢
多线程的效果就是同一时间各个线程都在执行。
加锁不是给线程上锁。
pthread_mutex_lock(&qlock);表示尝试去把qlock上锁,它会先判断qlock是否已经上锁,如果已经上锁这个线程就会停在这一步直到其他线程把锁解开。它才继续运行。
所以代码中要么是线程1先执行完后执行线程2,要么就是线程2先执行,再执行线程1.而线程3一开始就执行了。
互斥量mutex是用来给多线程之间的贡献资源上锁的。也就是同一个时间只允许一个线程去访问该资源(资源:比如对文件的写操作)。
现在来回答楼主的问题:
不是只要在pthread_mutex_lock(&qlock)与pthread_mutex_unlock(&qlock)之间的代码执行,其他的都不能介入吗?
其他的都不能介入,不是整个进程只运行这一个线程,其他线程都停住了。
“不能介入“这个动作需要程序员自己设计来保证:好比前面提到的文件读写操作。为了防止多个线程同时对文件进行写入操作,这就需要把资源上锁了。
如果只有线程1加锁,那是不是这个锁就没有意义了呢?
这个理解可以有
5. Linux 多线程编程(二)2019-08-10
三种专门用于线程同步的机制:POSIX信号量,互斥量和条件变量.
在Linux上信号量API有两组,一组是System V IPC信号量,即PV操作,另外就是POSIX信号量,POSIX信号量的名字都是以sem_开头.
phshared参数指定信号量的类型,若其值为0,就表示这个信号量是当前进程的局部信号量,否则该信号量可以在多个进程之间共享.value值指定信号量的初始值,一般与下面的sem_wait函数相对应.
其中比较重要的函数sem_wait函数会以原子操作的方式将信号量的值减一,如果信号量的值为零,则sem_wait将会阻塞,信号量的值可以在sem_init函数中的value初始化;sem_trywait函数是sem_wait的非阻塞版本;sem_post函数将以原子的操作对信号量加一,当信号量的值大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒.
这些函数成功时返回0,失败则返回-1并设置errno.
生产者消费者模型:
生产者对应一个信号量:sem_t procer;
消费者对应一个信号量:sem_t customer;
sem_init(&procer,2)----生产者拥有资源,可以工作;
sem_init(&customer,0)----消费者没有资源,阻塞;
在访问公共资源前对互斥量设置(加锁),确保同一时间只有一个线程访问数据,在访问完成后再释放(解锁)互斥量.
互斥锁的运行方式:串行访问共享资源;
信号量的运行方式:并行访问共享资源;
互斥量用pthread_mutex_t数据类型表示,在使用互斥量之前,必须使用pthread_mutex_init函数对它进行初始化,注意,使用完毕后需调用pthread_mutex_destroy.
pthread_mutex_init用于初始化互斥锁,mutexattr用于指定互斥锁的属性,若为NULL,则表示默认属性。除了用这个函数初始化互斥所外,还可以用如下方式初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER。
pthread_mutex_destroy用于销毁互斥锁,以释放占用的内核资源,销毁一个已经加锁的互斥锁将导致不可预期的后果。
pthread_mutex_lock以原子操作给一个互斥锁加锁。如果目标互斥锁已经被加锁,则pthread_mutex_lock则被阻塞,直到该互斥锁占有者把它给解锁.
pthread_mutex_trylock和pthread_mutex_lock类似,不过它始终立即返回,而不论被操作的互斥锁是否加锁,是pthread_mutex_lock的非阻塞版本.当目标互斥锁未被加锁时,pthread_mutex_trylock进行加锁操作;否则将返回EBUSY错误码。注意:这里讨论的pthread_mutex_lock和pthread_mutex_trylock是针对普通锁而言的,对于其他类型的锁,这两个加锁函数会有不同的行为.
pthread_mutex_unlock以原子操作方式给一个互斥锁进行解锁操作。如果此时有其他线程正在等待这个互斥锁,则这些线程中的一个将获得它.
三个打印机轮流打印:
输出结果:
如果说互斥锁是用于同步线程对共享数据的访问的话,那么条件变量就是用于在线程之间同步共享数据的值.条件变量提供了一种线程之间通信的机制:当某个共享数据达到某个值时,唤醒等待这个共享数据的线程.
条件变量会在条件不满足的情况下阻塞线程.且条件变量和互斥量一起使用,允许线程以无竞争的方式等待特定的条件发生.
其中pthread_cond_broadcast函数以广播的形式唤醒所有等待目标条件变量的线程,pthread_cond_signal函数用于唤醒一个等待目标条件变量线程.但有时候我们可能需要唤醒一个固定的线程,可以通过间接的方法实现:定义一个能够唯一标识目标线程的全局变量,在唤醒等待条件变量的线程前先设置该变量为目标线程,然后采用广播的方式唤醒所有等待的线程,这些线程被唤醒之后都检查该变量以判断是否是自己.
采用条件变量+互斥锁实现生产者消费者模型:
运行结果:
阻塞队列+生产者消费者
运行结果:
6. Linux 线程同步有哪些方法
一、互斥锁(mutex)
1.
初始化锁。在Linux下,线程的互斥量数据类型是pthread_mutex_t。在使用前,要对它进行初始化。
静态分配:pthread_mutex_t
mutex
=
PTHREAD_MUTEX_INITIALIZER;
动态分配:int
pthread_mutex_init(pthread_mutex_t
*mutex,
const
pthread_mutex_attr_t
*mutexattr);
2.
加锁。对共享资源的访问,要对互斥量进行加锁,如果互斥量已经上了锁,调用线程会阻塞,直到互斥量被解锁。
int
pthread_mutex_lock(pthread_mutex
*mutex);
int
pthread_mutex_trylock(pthread_mutex_t
*mutex);
3.
解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
int
pthread_mutex_unlock(pthread_mutex_t
*mutex);
4.
销毁锁。锁在是使用完成后,需要进行销毁以释放资源。
int
pthread_mutex_destroy(pthread_mutex
*mutex);
二、条件变量(cond)
1.
初始化条件变量。
静态态初始化,pthread_cond_t
cond
=
PTHREAD_COND_INITIALIER;
动态初始化,int
pthread_cond_init(pthread_cond_t
*cond,
pthread_condattr_t
*cond_attr);
2.
等待条件成立。释放锁,同时阻塞等待条件变量为真才行。timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)
int
pthread_cond_wait(pthread_cond_t
*cond,
pthread_mutex_t
*mutex);
int
pthread_cond_timewait(pthread_cond_t
*cond,pthread_mutex
*mutex,const
timespec
*abstime);
3.
激活条件变量。pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)
int
pthread_cond_signal(pthread_cond_t
*cond);
int
pthread_cond_broadcast(pthread_cond_t
*cond);
//解除所有线程的阻塞
4.
清除条件变量。无线程等待,否则返回EBUSY
int
pthread_cond_destroy(pthread_cond_t
*cond);
三、信号量(sem)
1.
信号量初始化。
int
sem_init
(sem_t
*sem
,
int
pshared,
unsigned
int
value);
这是对由sem指定的信号量进行初始化,设置好它的共享选项(linux
只支持为0,即表示它是当前进程的局部信号量),然后给它一个初始值VALUE。
2.
等待信号量。给信号量减1,然后等待直到信号量的值大于0。
int
sem_wait(sem_t
*sem);
3.
释放信号量。信号量值加1。并通知其他等待线程。
int
sem_post(sem_t
*sem);
4.
销毁信号量。我们用完信号量后都它进行清理。归还占有的一切资源。
int
sem_destroy(sem_t
*sem);
7. Linux进程间通信(互斥锁、条件变量、读写锁、文件锁、信号灯)
为了能够有效的控制多个进程之间的沟通过程,保证沟通过程的有序和和谐,OS必须提供一定的同步机制保证进程之间不会自说自话而是有效的协同工作。比如在 共享内存的通信方式中,两个或者多个进程都要对共享的内存进行数据写入,那么怎么才能保证一个进程在写入的过程中不被其它的进程打断,保证数据的完整性 呢?又怎么保证读取进程在读取数据的过程中数据不会变动,保证读取出的数据是完整有效的呢?
常用的同步方式有: 互斥锁、条件变量、读写锁、记录锁(文件锁)和信号灯.
互斥锁:
顾名思义,锁是用来锁住某种东西的,锁住之后只有有钥匙的人才能对锁住的东西拥有控制权(把锁砸了,把东西偷走的小偷不在我们的讨论范围了)。所谓互斥, 从字面上理解就是互相排斥。因此互斥锁从字面上理解就是一点进程拥有了这个锁,它将排斥其它所有的进程访问被锁住的东西,其它的进程如果需要锁就只能等待,等待拥有锁的进程把锁打开后才能继续运行。 在实现中,锁并不是与某个具体的变量进行关联,它本身是一个独立的对象。进(线)程在有需要的时候获得此对象,用完不需要时就释放掉。
互斥锁的主要特点是互斥锁的释放必须由上锁的进(线)程释放,如果拥有锁的进(线)程不释放,那么其它的进(线)程永远也没有机会获得所需要的互斥锁。
互斥锁主要用于线程之间的同步。
条件变量:
上文中提到,对于互斥锁而言,如果拥有锁的进(线)程不释放锁,其它进(线)程永远没机会获得锁,也就永远没有机会继续执行后续的逻辑。在实际环境下,一 个线程A需要改变一个共享变量X的值,为了保证在修改的过程中X不会被其它的线程修改,线程A必须首先获得对X的锁。现在假如A已经获得锁了,由于业务逻 辑的需要,只有当X的值小于0时,线程A才能执行后续的逻辑,于是线程A必须把互斥锁释放掉,然后继续“忙等”。如下面的伪代码所示:
1.// get x lock
2.while(x