多线程高级编程
多线程高级编程
1、线程同步(POSIX API)
多线程编程复杂度:并发和异步机制带来了线程间资源竞争的无序性。
为解决这一复杂度,因此引入线程同步机制,来实现线程间正确有序共享数据
线程同步:
概念:指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。
基本思想:同步各个线程对资源(如全局变量、文件)的访问。
所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。
数据混乱原因:
- 资源共享(独享资源则不会)
- 调度随机(意味着数据访问会出现竞争)
- 线程间缺乏必要的同步机制(可改)
死锁:
- 线程试图对同一个互斥量A加锁两次(反复加锁)
- 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁
线程同步方法:
- 互斥锁
- 读写锁
- 条件变量
- 信号量
1)互斥锁mutex
概念:
互斥锁(也称互斥量)是线程同步的一种机制,用来保护多线程的共享资源。
工作流程:
- 创建一个互斥锁
- 初始化一个互斥锁
- 在进入临界区前把互斥锁加锁(防止其他线程进入临界区)
- 访问共享资源
- 退出临界区时互斥锁解锁(让别的线程有机会进入临界区)
- 最后不使用互斥锁时则销毁
注意:锁的粒度越小越好(访问共享数据前,加锁。访问结束立即解锁)
互斥锁,本质是结构体,我们可以看成初值为1 (pthread_mutex_init() 函数调用成功)
加锁:--操作 阻塞线程
结束:++操作 唤醒阻塞在锁上的线程
try锁:尝试加锁,成功--。失败,返回。同时设置错误号 EBUSY
操作函数:
pthread_mutex_init函数pthread_mutex_destroy函数pthread_mutex_lock函数pthread_mutex_trylock函数pthread_mutex_unlock函数
以上5个函数的返回值都是:成功返回0,失败返回错误号
pthread_mutex_t类型,其本质是一个结构体,但应用时可简单当成整数看待pthread_mutex_t mutex变量mutex只有两种取值1、0、
加锁和解锁
lock 尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
unlock 主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
例如:T1 T2 T3 T4 使用一把 mutex 锁。T1 加锁成功,其他线程均阻塞,直至 T1 解锁。T1 解锁后,T2 T3 T4 均被唤醒,并自动再次尝试加锁。
可假想 mutex 锁 init 成功初值为 1。lock 功能是将 mutex--。而 unlock 则将 mutex++。
①pthread_mutex_init
动态初始化一个互斥锁 --> 初值可看作 1
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
restrict关键字:用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成
②pthread_mutex_lock
互斥锁上锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
如果调用该函数时互斥锁已经被其它线程上锁了,则调用该函数的线程将阻塞
③pthread_mutex_trylock
互斥锁上锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
如果调用该函数时互斥锁已经被其它线程上锁了,调用该函数的线程不会阻塞,而是立即返回,并且函数返回EBUSY
④pthread_mutex_unlock
互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
注意:
pthread_mutex_unlock和pthread_mutex_lock要成对使用
⑤pthread_mutex_destroy
互斥锁销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//线程同步示例
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include<unistd.h>
pthread_mutex_t mutex; //定义一把互斥锁,mutex可以想象成一个整数,锁的值为1
void *tfn(void *arg)
{
srand(time(NULL));
while(1){
pthread_mutex_lock(&mutex); //互斥锁加锁,可以想象成 锁-- (1 --> 0)
printf("hello");
sleep(rand() % 3); //模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
printf("world\n");
pthread_mutex_unlock(&mutex); //互斥锁解锁,可以想象成 锁++ (0 --> 1)
sleep(rand() % 3);
}
return NULL;
}
int main()
{
pthread_t tid;
srand(time(NULL));
int ret = pthread_mutex_init(&mutex, NULL); //初始化互斥锁
if(ret != 0){
fprintf(stderr, "mutex init error:%s\n", strerror(ret));
exit(1);
}
pthread_create(&tid, NULL, tfn, NULL);
while(1){
pthread_mutex_lock(&mutex); //互斥锁加锁
printf("HELLO");
sleep(rand() % 3);
printf("WORLD\n");
sleep(rand() % 3);
pthread_mutex_unlock(&mutex); //互斥锁解锁
}
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex); //销毁互斥锁
return 0;
}
2)读写锁
与互斥量类似,但读写锁允许更高的并行性(当读线程多的时候,提高访问效率)。
对资源的访问会存在两种情况(两种状态):
- 独占(写操作—写锁)
- 共享(读操作—读锁)
读写锁特性
- “写锁“时,解锁前,所有对该锁加锁的线程会被阻塞
- ”读锁时“,若线程以读模式对其加锁会成功;若以写模式加锁会阻塞
- ”读锁时“,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程,那么读写锁会阻塞随后的读锁请求。优先满足写锁。读锁、写锁并行阻塞,写锁优先级高
读写锁也叫共享—独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
读写锁非常适合于对数据结构读的次数远大于写的情况。
①主要应用函数
pthread_rwlock_initpthread_rwlock_destroy函数pthread_rwlock_rdlock函数pthread_rwlock_wrlock函数pthread_rwlock_tryrdlock函数pthread_rwlock_trywrlock函数pthread_rwlock_unlock函数
以上7个函数的返回值:成功返回0,失败返回错误号
pthread_rwlock_t类型 用于定义一个读写锁变量pthread_rwlock_t rwlock;
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int counter;
pthread_rwlock_t rwlock; //全局的读写锁
//3个线程不定时写同一全局资源,5个线程不定时读同一全局资源
void *th_write(void *arg)
{
int t;
int i = (int)arg; //long i = (long)arg
while(1){
pthread_rwlock_wrlock(&rwlock); //以写模式加锁,写独占
t = counter;
usleep(1000);
printf("======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(10000);
}
return NULL;
}
void *th_read(void *arg)
{
int i = (int)arg; //long i = (long)arg
while(1){
pthread_rwlock_rdlock(&rwlock); //读线程间,读写共享
printf("------read %d: %lu: %d\n", i, pthread_self(),counter);
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
return NULL;
}
int main(void)
{
int i;
pthread_t tid[8];
pthread_rwlock_init(&rwlock, NULL); //实际中注意检查返回值
for(i = 0; i < 3; i++){
pthread_create(&tid[i], NULL, th_write, (void *)i);
}
for(i = 0; i < 5; i++){
pthread_create(&tid[i+3], NULL, th_read, (void *)i);
}
for(i = 0; i < 8; i++){
pthread_join(tid[i], NULL);
}
pthread_rwlock_destroy(&rwlock); //销毁读写锁
return 0;
}
3)条件变量
条件变量本身不是锁。但它也可以造成线程阻塞。
通常与互斥锁配合使用。给多线程提供一个会合的场所。
为什么要使用条件变量
线程抢占互斥锁时,线程A抢到了互斥锁,但是条件不满足,线程A就会让出互斥锁让给其他线程,然后等待其他线程唤醒他;一旦条件满足,线程就可以被唤醒,并且拿互斥锁去访问共享区。经过这中设计能让进程运行更稳定。
主要应用函数
pthread_cond_init函数pthread_cond_destroy函数pthread_cond_wait函数pthread_cond_timedwait函数pthread_cond_signal函数pthread_cond_broadcast函数
以上6个函数返回值:成功返回0,失败返回错误号
pthread_cond_t类型 用于定义条件变量pthread_cond_t cond;
①pthread_cond_init
//静态初始化:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
//动态初始化:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
②pthread_cond_destroy
作用:
销毁一个条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
③pthread_cond_wait 函数(重)
作用:(非常重要 三点)
1.阻塞等待条件变量 cond(参 1)满足
2.释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
3.当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);1和2 两步为一个原子操作。
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
④pthread_cond_timedwait
作用:
限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
⑤pthread_cond_signal
作用:
唤醒(至少)一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
⑥pthread_cond_broadcast 函数
作用:
唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
条件变量使用步骤:
消费者:
- 创建锁
pthread_mutex_t mutex - 初始化锁
pthread_mutex_init(&mutex, NULL); - 加锁
pthread_mutex_lock(&mutex); - 等待条件满足
pthread_cond_wait(&cond, &mutex)- 阻塞条件变量
- 解锁unlock(mutex)----- 10s
- 加锁lock(mutex)
- 访问共享数据
- 解锁、释放条件变量。销毁锁
生产者:
- 生产数据
- 加锁
pthread_mutex_lock(&mutex) - 将数据放置到 公共区域
- 解锁
pthread_mutex_unlock(&mutex); - 通知阻塞在条件变量上的线程
pthread_cond_signal()pthread_cond_broadcast()
- 循环生产后面的数据
/*借助条件变量模拟 生产者-消费者 问题*/
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
void err_thread(int ret, char *str)
{
if(ret != 0){
fprintf(stderr, "%s:%s\n", str, strerror(ret));
pthread_exit(NULL);
}
}
/*链表作为共享数据,需被互斥量保护*/
struct msg
{
struct msg *next;
int num;
};
struct msg *head;
/*静态初始化 一个条件变量 和 一个互斥量*/
pthread_cond_t has_data = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *producer(void *arg)
{
struct msg *mp;
while (1){
mp=(struct msg*)malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1; //模拟生产一个产品
printf("-Produce------------------%d\n", mp->num);
pthread_mutex_lock(&mutex); //加锁 互斥量
mp->next = head; //写公共区域
head = mp;
pthread_mutex_unlock(&mutex); //解锁 互斥量
pthread_cond_signal(&has_data); //将等待在该条件变量上的一个线程唤醒
sleep(rand() % 3);
}
return NULL;
}
void *consumer(void *arg)
{
struct msg *mp;
while(1){
pthread_mutex_lock(&mutex);
if(head == NULL){ //头指针为空,说明没有节点 可以为if吗
pthread_cond_wait(&has_data, &mutex); //阻塞等待条件变量
} //pthread_cond_wait 返回时,重写加锁mutex
mp = head;
head = mp->next; //模拟消费掉一个产品
pthread_mutex_unlock(&mutex); //解锁 互斥量
printf("-Consume %lu---%d\n", pthread_self(), mp->num);
free(mp);
sleep(rand() % 3);
}
return NULL;
}
int main(int argc, char *argv[])
{
int ret;
pthread_t pid, cid;
srand(time(NULL));
ret = pthread_create(&pid, NULL, producer, NULL); //生产者
if(ret != 0){
err_thread(ret, "pthread_create producer eoor");
}
ret = pthread_create(&cid, NULL, consumer, NULL); //消费者
if(ret != 0){
err_thread(ret, "pthread_create consumer eoor");
}
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
4)信号量semaphore
应用于线程、进程间同步
相当于进化版的互斥量——初始化值为N,可以允许N个线程同时访问共享资源
- 简单的说就是进化版的互斥锁(1~N)计数器
- 记录当前可利用的资源数,当资源数量<=0时会阻塞,当资源数量>时才开始进行操作,另外信号量的操作均为原子操作
信号量是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发
主要应用函数:
sem_init函数sem_destroy函数sem_wait函数sem_trywait函数sem_timedwait函数sem_post函数
以上6个函数的返回值:
成功返回0,失败返回-1;同时设置errno。(注意:它们没有pthread前缀)
sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)sem_t sem;规定信号量不能<0。头文件<semaphore.h>
信号量基本操作:
sem_wait: (类比pthread_mutex_lock)
一次调用,做一次--操作,当信号量的值为0时,再次--,就会阻塞
sem_post: (类比pthread_mutex_unlock)
一次调用,做一次++操作,当信号量的值为N时,再次++就会阻塞
但由于
sem_t的实现对用户隐藏,所以所谓的++、--操作只能通过函数来实现,而不能直接++、--符号。
①sem_init 函数
作用:
初始化一个信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 参 1:sem 信号量
- 参 2:pshared 取 0 用于线程间;取非 0(一般为 1)用于进程间
- 参 3:value 指定信号量初值(N值)
- 无静态初始化
信号量的初值,决定了占用信号量的线程(进程)的个数。
②sem_destroy 函数
作用:
销毁一个信号量
int sem_destroy (sem_t *sem);
③sem_wait 函数
作用:
给信号量加锁 –
int sem_wait(sem_t *sem);
④sem_post 函数
作用:
给信号量解锁 ++
int sem_post(sem_t *sem);
⑤sem_trywait 函数
作用:
尝试对信号量加锁 (与 sem_wait 的区别类比 lock 和 trylock)
int sem_trywait(sem_t *sem);
⑥sem_timedwait 函数
作用:
限时尝试对信号量加锁
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- 参 2:
abs_timeout采用的是绝对时间。 - 定时1秒:
信号量—>生产者消费者问题:
用两个信号量来控制,一个初始为0,表示一开始生产的数量为0。另外一个初始的数量是可以存放商品的空格数,初始化就是格子数N。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<semaphore.h>
#include<unistd.h>
#define NUM 5
int queue[NUM];
sem_t blank_number, product_number; //两个信号量控制
void *producer(void *arg) //生产
{
int p = 0;
while (1)
{
sem_wait(&blank_number); //空格位置--
queue[p] = rand() % 1000 + 1; //放入队列
printf("-----product---- %d \n", queue[p]);
sem_post(&product_number); //生产数量++
p = (p + 1) % NUM; //借助下标 实现循环放入 模拟队列
sleep(rand() % 1);
}
}
void *consumer(void *arg) //消费者
{
int c = 0;
while (1)
{
sem_wait(&product_number); //产品数量--
printf("-Consumer--- %d \n", queue[c]);
queue[c] = 0;
sem_post(&blank_number); //空格数量++
c = (c + 1) % NUM;
sleep(rand() % 3);
}
}
int main()
{
pthread_t pid, cid;
sem_init(&blank_number, 0, NUM); //初始化信号量为5,线程间共享--0
sem_init(&product_number, 0, 0); //产品数为0
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}
2、C++11/14中的线程同步
C++11/14提供两种方式进行线程同步
- 互斥锁(较多)
- 条件变量(较少,自行了解)
头文件:
#include < mutex >
C++11中互斥锁有4种,并对应着4种不同的类
- 基本互斥锁,对应类为
std::mutex - 递归互斥锁,对应类为
std::recursive_mutex - 定时互斥锁,对应类为
std::time_mutex - 定时递归互斥锁,对应类为
std::time_mutex
1)基本互斥锁
类 std::mutex的成员函数
| 成员函数 | 说明 |
|---|---|
mutex |
构造函数 |
lock |
互斥锁上锁 |
Try_lock |
如果互斥锁没有上锁,则上锁 |
native_handle |
得到本地互斥锁句柄 |
上锁—解锁
void lock();
void unlock(); //两者都要被调用线程配对使用
函数lock尝试锁住互斥锁:
如果互斥锁当前没有被上锁,则当前线程可以成功上锁,即当前线程拥有互斥锁,直到当前线程调用解锁函数unlock
如果互斥锁已经被其他线程上锁了,则当前线程挂起,直到互斥锁被其它线程解锁。
如果当前互斥锁被上锁,再次调用当前线程上锁,则会产生死锁
//多线程统计计数器到10万
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
volatile int counter(0); //定义一个全局变量,当作计数器,用于累加
mutex mtx;
void thrfunc()
{
for(int i = 0; i < 10000; i++){
mtx.lock(); //互斥锁上锁
++counter; //计数器累加
mtx.unlock(); //互斥锁解锁
}
}
int main(int argc, char *argv[])
{
thread threads[10];
for(int i = 0; i < 10; i++)
threads[i] = thread(thrfunc); //启动10个线程
for(auto& th:threads) th.join(); //等待10个线程结束
cout << "count to " << counter << " successfully \n";
return 0;
}
2)定时互斥锁
类std::time_mutex成员函数
| 成员函数 | 说明 |
|---|---|
| mutex | 构造函数 |
| lock | 互斥锁上锁 |
| try_lock | 若互斥锁没有上锁,则努力上锁,但不阻塞 |
| try_lock_for | 若互斥锁没有上锁,则努力一段时间上锁,这段时间内阻塞,过了这段时间就退出 |
| try_lock_until | 努力上锁,知道某个时间点,时间点到达之前将一直阻塞 |
| native_handle | 得到本地互斥锁句柄 |
函数try_lock尝试锁住互斥锁,若互斥锁被其他线程占有,则当前线程也不会被阻塞,线程调用会出现3种情况:
- 如果当前互斥锁没有被其它线程锁住,则该线程锁住互斥锁,知道调用unlock释放互斥锁
- 如果当前互斥锁被其他线程锁住,则当前调用线程返回false,而并不会被阻塞掉
- 如果当前互斥锁被当前调用线程锁住,则会产生死锁
bool try_lock(); //注意有下画线
如果函数成功上锁,则返回true,否则返回false。该函数不会阻塞,不能上锁时立即返回false
//用非阻塞上锁版本改写——多线程统计计数器到10万
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
volatile int counter(0); //定义一个全局变量,当作计数器,用于累加
mutex mtx; //用户保护counter的互斥锁
void thrfunc()
{
if(mtx.try_lock()) //互斥锁上锁
{
++counter;
mtx.unlock(); //互斥锁解锁
}
else
{
cout << "try_lock false\n";
}
}
int main(int argc, char *argv[])
{
thread threads[10];
for(int i = 0; i < 10; i++)
threads[i] = thread(thrfunc); //启动10个线程
for(auto& th:threads) th.join(); //等待10个线程结束
cout << "count to " << counter << " successfully \n";
return 0;
}

浙公网安备 33010602011771号