线程间同步

    前面专门找了一些资料去了解函数的可重入性以及线程安全同步,我们知道在多线程并发访问共享数据时候往往会出现数据冲突,这跟之前的函数可重入性是一样的.比如两个线程要对一个全局变量加1.但这个过程在x86平台上并非原子操作,它可能会经过以下三步才能完成:

1:从内存中读取变量到寄存器中

2:寄存器加1

3:将寄存器的值写回内存

 

这一个非原子操作可想而知,如果有多线程并发执行,那么这三步有可能就在线程间交叉执行.如:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
int counter;
/* incremented by threads */
void *doit(void *);
int main(int argc, char **argv)
{
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, &doit, NULL);
pthread_create(&tidB, NULL, &doit, NULL);
/* wait for both threads to terminate */
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void *doit(void *vptr)
{
int
i, val;
/*
* Each thread fetches, prints, and increments the counter
NLOOP times.
* The value of the counter should increase monotonically.
*/
for (i = 0; i < NLOOP; i++) {
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(),
val + 1);
counter = val + 1;
}
return NULL;
}
 由#define NLOOP 5000
可知,我们创建的两个线程分别对counter进行了5000次加1操作,最终结果应该是10000才对,但我们看输出却是:
6cb6c700: 1
6cb6c700: 2
6cb6c700: 3
6cb6c700: 4
6cb6c700: 5
6cb6c700: 6
6cb6c700: 7
6cb6c700: 8
6cb6c700: 9
6cb6c700: 10
6cb6c700: 11
6cb6c700: 12
6cb6c700: 13
6cb6c700: 14
6cb6c700: 15
6cb6c700: 16
6cb6c700: 17
6cb6c700: 18
6cb6c700: 19
6cb6c700: 20
6cb6c700: 21
6d36d700: 1
6d36d700: 2
6d36d700: 3
6d36d700: 4
6d36d700: 5
6d36d700: 6
6d36d700: 7
6d36d700: 8
6d36d700: 9
6d36d700: 10
6d36d700: 11
6d36d700: 12
6d36d700: 13
6d36d700: 14
6cb6c700: 22
6cb6c700: 23
6cb6c700: 24
6cb6c700: 25
6cb6c700: 26
6cb6c700: 27
6cb6c700: 28
6cb6c700: 29
6cb6c700: 30
6cb6c700: 31
6cb6c700: 32
6cb6c700: 33
.......................
这个输出的不同会因不同的CPU调度,总线周期而定.

普遍情况下,我们为保证共享数据在多线程访问下的正确性,会引入互斥锁(Mutex Exclusive Lock,它与自旋锁的区别请百度).引入锁在目的在于使线程对数据的"读-修改-写"形成一组原子操作,要么执行完,要么不执行,不能被打断

使用锁的基本过程一般包括,1:锁初始化  2:取得锁 3:释放锁,对于互斥锁有如下函数声名
Mutex用pthread_mutex_t 类型的变量表示,可以这样初始化和销毁:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
返回值:成功返回0,失败返回错误号。
pthread_mutex_init 函数对Mutex做初始化,参数attr 设定Mutex的属性,如果attr 为NULL 则表
示缺省属性,本章不详细介绍Mutex属性,感兴趣的读者可以参考[APUE2e]。
用pthread_mutex_init 函数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex变量
是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER 来初始
化,相当于用pthread_mutex_init 初始化并且attr 参数为NULL 。Mutex的加锁和解锁操作可以用
下列函数:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号。
一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调
用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调
用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经
被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。
 
现在我们就用Mutex来改写一下刚才的程序:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
int counter;
/* incremented by threads */
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
void *doit(void *);
int main(int argc, char **argv)
{
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, doit, NULL);
pthread_create(&tidB, NULL, doit, NULL);
/* wait for both threads to terminate */
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void *doit(void *vptr)
{
int
i, val;
/*
* Each thread fetches, prints, and increments the counter
NLOOP times.
* The value of the counter should increase monotonically.
*/
for (i = 0; i < NLOOP; i++) {
pthread_mutex_lock(&counter_mutex);//如果了解了函数的可重入性与线程安全我们就知道,我们的锁应该是加在全局变量上
val = counter;//counter全局变量是导致非线程安全的所在
printf("%x: %d\n", (unsigned int)pthread_self(),
val + 1);
counter = val + 1;
pthread_mutex_unlock(&counter_mutex);//释放锁
}
return NULL;
}
 这样运行一下就可得到10000的正确结果.

在这里简单初充一下Mutex的简单实现原理,
当Mutex为1时表示锁空闲(未被取得),为0时表示锁已被取得.当线程进行lock() or trylock()操作时,如果Mutex为1,则被取得.
如果为0时,那么调用lock的线程则会被挂起,如果不想被挂起则调用trylock().


lock:
if(mutex > 0){
mutex = 0;
return 0;
} else
挂起等待;
goto lock;
unlock:
mutex = 1;
唤醒等待Mute
 而对于unlock()操作可以有多种实现,可以只唤醒一个等待线程,也可以让等待线程队列去竞争这个锁资料(可能按优先级,FIFO等调度算法去选择).

由以上可知,我们引进Mutex是为了实现一组原子操作,这个互斥过程在大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存
器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问
内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待
总线周期。现在我们把lock和unlock的伪代码改一下(以x86的xchg指令为例):
lock:
movb $0, %al
xchgb %al, mutex
if(al寄存器的内容 > 0){
return 0;
} else
挂起等待;
goto lock;
unlock:
movb $1, mutex
唤醒等待Mutex的线程;
return 0;
 
unlock中的释放锁操作同样只用一条指令实现,以保证它的原子性。

Condition Variable

线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件
不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执
行。在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或者唤醒等待这
个条件的线程。Condition Variable用pthread_cond_t 类型的变量表示,可以这样初始化和销
毁:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
 
返回值:成功返回0,失败返回错误号。
和Mutex的初始化和销毁类似,pthread_cond_init函数初始化一个Condition Variable,attr 参
数为NULL 则表示缺省属性,pthread_cond_destroy 函数销毁一个Condition Variable。如
果Condition Variable是静态分配的,也可以用宏定义PTHEAD_COND_INITIALIZER 初始化,相当于
用pthread_cond_init函数初始化并且attr 参数为NULL 。Condition Variable的操作可以用下列函
数:
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
 
返回值:成功返回0,失败返回错误号。
可见,一个Condition Variable总是和一个Mutex搭配使用的。一个线程可以调
用pthread_cond_wait在一个Condition Variable上阻塞等待,这个函数做以下三步操作:
1. 释放Mutex
2. 阻塞等待
3. 当被唤醒时,重新获得Mutex并返回
pthread_cond_timedwait 函数还有一个额外的参数可以设定等待超时,如果到达了abstime 所指
定的时刻仍然没有别的线程来唤醒当前线程,就返回ETIMEDOUT 。一个线程可以调
用pthread_cond_signal 唤醒在某个Condition Variable上等待的另一个线程,也可以调
用pthread_cond_broadcast唤醒在这个Condition Variable上等待的所有线程。
下面的程序演示了一个生产者-消费者的例子,生产者生产一个结构体串在链表的表头上,消费
者从表头取走结构体。
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
struct msg {
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
struct msg *mp;
for (;;) {
pthread_mutex_lock(&lock);
while (head == NULL)
pthread_cond_wait(&has_product, &lock);
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock);
printf("Consume %d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p)
{
struct msg *mp;
for (;;) {
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;
printf("Produce %d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
执行结果如下:
$ ./a.out
Produce 744
Consume 744
Produce 567
Produce 881
Consume 881
Produce 911
Consume 911
Consume 567
Produce 698
Consume 698
 
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值