一、posix 信号量
信号量的概念参见这里。前面也讲过system v 信号量,现在来说说posix 信号量。
system v 信号量只能用于进程间同步,而posix 信号量除了可以进程间同步,还可以线程间同步。system v 信号量每次PV操作可以是N,但Posix 信号量每次PV只能是1。除此之外,posix 信号量还有命名和匿名之分(man 7 sem_overview):
1、命名信号量
名字以/somename 形式分辨,只能有一个/ ,且总长不能超过NAME_MAX - 4(一般是251)。
需要用sem_open 函数创建或打开,PV操作分别是sem_wait 和 sem_post,可以使用sem_close 关闭,删除用sem_unlink。
命名信号量用于不共享内存的进程间同步(内核实现),类似system v 信号量。
2、匿名信号量
存放在一块共享内存中,如果是线程共享,这块区域可以是全局变量;如果是进程共享,可以是system v 共享内存(shmget 创建,shmat 映射),也可以是 posix 共享内存(shm_open 创建,mmap 映射)。
匿名信号量必须用sem_init 初始化,sem_init 函数其中一个参数pshared决定了线程共享还是进程共享,也可以用sem_post 和sem_wait 进行操作,在共享内存释放前,匿名信号量要先用sem_destroy 销毁。
有关这些函数的具体参数可以man 一下。
二、互斥锁
对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,MutualExclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
Mutex用pthread_mutex_t类型的变量表示,pthread_mutex_init函数对Mutex做初始化,参数attr设定Mutex的属性,如果attr为NULL则表示缺省属性,具体看结构体:有人建议开发中总是设置 PTHREAD_MUTEX_RECURSIVE 属性,避免死锁。
// 互斥量属性: 同一线程可多次加锁
pthread_mutexattr_t m_attr;
pthread_mutexattr_init(&m_attr);
pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_RECURSIVE);
1
2 3 4 5 6 7 8 9 10 |
struct pthread_mutexattr_t
{ enum lock_type // 使用pthread_mutexattr_settype来更改 { PTHREAD_MUTEX_TIMED_NP [ default] //当一个线程加锁后,其余请求锁的线程形成等待队列,在解锁后按优先级获得锁。 PTHREAD_MUTEX_ADAPTIVE_NP // 动作最简单的锁类型,解锁后所有线程重新竞争。 PTHREAD_MUTEX_RECURSIVE_NP // 允许同一线程对同一锁成功获得多次。当然也要解锁多次。其余线程在解锁时重新竞争。 PTHREAD_MUTEX_ERRORCHECK_NP // 若同一线程请求同一锁,返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP动作相同。 } type; } attr; |
用pthread_mutex_init函数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。
一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。
上面的具体函数可以man 一下。
三、生产者消费者问题
生产者消费者问题概念参见这里。下面使用posix 信号量和互斥锁一起来演示:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
#include #include #include #include #include #include #include #include #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while( 0) #define CONSUMERS_COUNT 1 #define PRODUCERS_COUNT 1 #define BUFFSIZE 10 int g_buffer[BUFFSIZE]; unsigned short in = 0; unsigned short out = 0; unsigned short produce_id = 0; unsigned short consume_id = 0; sem_t g_sem_full; sem_t g_sem_empty; pthread_mutex_t g_mutex; pthread_t g_thread[CONSUMERS_COUNT + PRODUCERS_COUNT]; void *consume( void *arg) { int i; int num = ( int)arg; while ( 1) { printf( "%d wait buffer not empty\n", num); sem_wait(&g_sem_empty); pthread_mutex_lock(&g_mutex); for (i = 0; i < BUFFSIZE; i++) { printf( "%02d ", i); if (g_buffer[i] == - 1) printf( "%s", "null"); else printf( "%d", g_buffer[i]); if (i == out) printf( "\t<--consume"); printf( "\n"); } consume_id = g_buffer[out]; printf( "%d begin consume product %d\n", num, consume_id); g_buffer[out] = - 1; out = (out + 1) % BUFFSIZE; printf( "%d end consume product %d\n", num, consume_id); pthread_mutex_unlock(&g_mutex); sem_post(&g_sem_full); sleep( 1); } return NULL; } void *produce( void *arg) { int num = ( int)arg; int i; while ( 1) { printf( "%d wait buffer not full\n", num); sem_wait(&g_sem_full); pthread_mutex_lock(&g_mutex); for (i = 0; i < BUFFSIZE; i++) { printf( "%02d ", i); if (g_buffer[i] == - 1) printf( "%s", "null"); else printf( "%d", g_buffer[i]); if (i == in) printf( "\t<--produce"); printf( "\n"); } printf( "%d begin produce product %d\n", num, produce_id); g_buffer[in] = produce_id; in = (in + 1) % BUFFSIZE; printf( "%d end produce product %d\n", num, produce_id++); pthread_mutex_unlock(&g_mutex); sem_post(&g_sem_empty); sleep( 5); } return NULL; } int main( void) { int i; for (i = 0; i < BUFFSIZE; i++) g_buffer[i] = - 1; sem_init(&g_sem_full, 0, BUFFSIZE); sem_init(&g_sem_empty, 0, 0); pthread_mutex_init(&g_mutex, NULL); for (i = 0; i < CONSUMERS_COUNT; i++) pthread_create(&g_thread[i], NULL, consume, ( void *)i); for (i = 0; i < PRODUCERS_COUNT; i++) pthread_create(&g_thread[CONSUMERS_COUNT + i], NULL, produce, ( void *)i); for (i = 0; i < CONSUMERS_COUNT + PRODUCERS_COUNT; i++) pthread_join(g_thread[i], NULL); sem_destroy(&g_sem_full); sem_destroy(&g_sem_empty); pthread_mutex_destroy(&g_mutex); return 0; } |
与这里的程序相比,程序逻辑没太大变化,只是用pthread_mutex_lock 替代了 sem_mutex,其次这里是演示线程间同步,现在上述程序生产者消费者各一个线程,但生产者睡眠时间是消费者的5倍,故消费者会经常阻塞在sem_wait(&g_sem_empty) 上面,因为缓冲区经常为空,可以将PRODUCTORS_COUNT 改成5,即有5个生产者线程和1个消费者线程,而且生产者睡眠时间还是消费者的5倍,从动态输出可以看出,基本上就动态平衡了,即5个生产者一下子生产了5份东西,消费者1s消费1份,刚好在生产者继续生产前消费完。
四、自旋锁和读写锁简介
(一)、自旋锁
自旋锁类似于互斥锁,它的性能比互斥锁更高。
自旋锁与互斥锁很重要的一个区别在于,线程在申请自旋锁的时候,线程不会被挂起,它处于忙等待的状态,一般用于等待时间比较短的情形。
pthread_spin_init
pthread_spin_destroy
pthread_spin_lock
pthread_spin_unlock
(二)、读写锁
1、只要没有线程持有给定的读写锁用于写,那么任意数目的线程可以持有读写锁用于读
2、仅当没有线程持有某个给定的读写锁用于读或用于写时,才能分配读写锁用于写
3、读写锁用于读称为共享锁,读写锁用于写称为排它锁
pthread_rwlock_init
pthread_rwlock_destroy
int pthread_rwlock_rdlock
int pthread_rwlock_wrlock
int pthread_rwlock_unlock
更多有关linux中的锁问题可以参考这篇文章 :《透过Linux内核看无锁编程》
http://www.ibm.com/developerworks/cn/linux/l-cn-lockfree/
参考:
《linux c 编程一站式学习》
《UNP》