今天说一下线程间的通信
Linux线程间通信与同步
同一进程下所有线程,共享全局变量、堆、文件描述符。线程天然共享内存,但会出现数据竞争。线程通信主要靠共享内存 + 同步工具。管道、消息队列、shm共享内存属于进程间IPC,线程虽然也能用,但太重,线程一般不用。
用到头文件:#include <pthread.h>
1.裸用全局变量(错误例子)
int cnt = 0;
void* thr_func(void *arg)
{
int i;
for(i = 0; i < 10000; i++)
{
cnt++;
}
return NULL;
}
开两个线程,预期20000,实际结果偏小。
原因cnt++不是单条CPU指令:读,自增,写回。线程切换会互相覆盖,数据丢失。必须加锁保护临界区。
2.互斥锁 pthread_mutex_t
作用:同一时间只允许一个线程进入临界代码段。
接口:
1)pthread_mutex_init() 动态初始化锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
attr传NULL代表默认属性。
静态初始化直接写:pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
2)pthread_mutex_lock(pthread_mutex_t *mutex);
拿锁。锁被别人占着,当前线程阻塞休眠,直到拿到锁才返回。
3)pthread_mutex_trylock(pthread_mutex_t *mutex);
尝试加锁,非阻塞。拿不到直接返回EBUSY,不会睡觉。不建议死循环trylock,浪费CPU。
4)pthread_mutex_unlock(pthread_mutex_t *mutex);
解锁。必须是谁加锁谁解锁,不能A线程lock,B线程unlock,行为未定义。
5)pthread_mutex_destroy(pthread_mutex_t *mutex);
销毁锁,释放资源。
正确使用模板:
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
int cnt = 0;
void* thr_func(void *arg)
{
int i;
for(i = 0; i < 10000; i++)
{
pthread_mutex_lock(&mtx);
cnt++;
pthread_mutex_unlock(&mtx);
}
return NULL;
}
坑点:lock之后,如果return、break直接跳出,忘记unlock,直接死锁。
死锁两种常见场景:普通锁重复加锁;A拿锁1等锁2,B拿锁2等锁1。
3.条件变量 pthread_cond_t
互斥锁只管访问互斥,解决不了"等待某个条件成立"。比如生产者放数据,消费者没数据就休眠,不要while死循环轮询占满CPU,就用条件变量。
接口:
静态初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态初始化:int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);
1)pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
重点:调用wait的时候,线程必须已经持有这把mtx互斥锁。
做两件原子操作:
①线程进入条件变量等待队列休眠
②自动释放mtx锁,别的线程可以抢到锁修改共享数据。
被唤醒之后,函数返回之前会自动重新获取mtx锁,才返回。
重点:wait睡醒,条件不一定真满足!操作系统会产生虚假唤醒。判断条件必须用while,绝对不能用if。
2)pthread_cond_signal(pthread_cond_t *cond);
唤醒至少一个阻塞在cond上的线程,叫醒其中一个。
3)pthread_cond_broadcast(pthread_cond_t *cond);
广播,把所有睡在这个cond上面的线程全部唤醒。
4)pthread_cond_destroy(pthread_cond_t *cond);销毁条件变量。
极简生产者消费者伪代码:
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int buf = 0;
//消费者
void* consumer(void* arg)
{
pthread_mutex_lock(&mtx);
while(buf == 0) //必须while,不能if
{
pthread_cond_wait(&cond, &mtx);
}
//走到这里buf有数据
printf("消费:%d\n",buf);
buf = 0;
pthread_mutex_unlock(&mtx);
return NULL;
}
//生产者
void* producer(void* arg)
{
pthread_mutex_lock(&mtx);
buf = 100;
pthread_cond_signal(&cond); //叫醒消费者
pthread_mutex_unlock(&mtx);
return NULL;
}
4.读写锁 pthread_rwlock_t
场景:读多写少。很多线程可以同时读;写的时候,所有读、写全部阻塞。读共享,写独占。
接口:
静态初始化:pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
动态:pthread_rwlock_init()
pthread_rwlock_rdlock(&rwlock); 加读锁,多个线程可以同时拿读锁
pthread_rwlock_wrlock(&rwlock); 加写锁,独占
pthread_rwlock_tryrdlock(&rwlock);尝试读锁非阻塞
pthread_rwlock_trywrlock(&rwlock);尝试写锁非阻塞
pthread_rwlock_unlock(&rwlock);释放锁
pthread_rwlock_destroy(&rwlock);销毁
坑:大量读锁持续占用,写线程会一直拿不到锁,写饥饿。
5.自旋锁 pthread_spinlock_t
普通互斥锁拿不到就休眠,切换线程有系统开销。自旋锁拿不到锁的时候,CPU空转循环忙等,不切换上下文。
适合:锁保持时间特别短。如果锁占用时间长,CPU直接跑满,不适合。
接口:
pthread_spin_init()初始化
pthread_spin_lock()自旋加锁,拿不到就原地循环
pthread_spin_trylock()非阻塞尝试拿
pthread_spin_unlock()解锁
pthread_spin_destroy()销毁
6.POSIX无名信号量 sem_t(线程同步常用)
头文件 #include <semaphore.h>
注意这是POSIX信号量,不是system V的信号量。可以用于线程同步,也可以用于进程。
接口:
sem_t sem;
int sem_init(sem_t *sem, int pshared, unsigned int value);
pshared=0,用于同一个进程内部线程;value信号量初始值。
sem_wait(sem_t *sem); 信号量大于0就减1继续走;等于0线程阻塞休眠。
sem_trywait(sem_t *sem);非阻塞版本。
sem_post(sem_t *sem);信号量+1,唤醒等待的线程。
sem_destroy(sem_t *sem);销毁无名信号量。
可以实现生产者消费者,也可以做锁。信号量可以记录资源计数,互斥锁只能0/1。
7.线程也能使用信号
pthread_kill(线程tid, sig);给指定线程发送信号。
注意:信号是进程全局资源,多线程下信号处理很容易乱,线程开发尽量少用信号做通信。
总结区分:
1.互斥锁:保护临界资源,同一时间只一个线程访问。
2.条件变量:配合互斥锁,做等待‑唤醒逻辑,解决生产者消费者。必须while判断防虚假唤醒。
3.读写锁:读多写少场景。
4.自旋锁:锁持有极短,避免线程切换开销;不能长时间占用。
5.sem_t信号量:做资源计数同步。
重点提醒:
线程之间天然共享内存,这是线程最主要通信方式。上面全部工具,本质都是用来解决"共享内存带来的数据竞争问题"。
管道、FIFO、消息队列、systemV共享内存属于进程间通信,线程能用,但是开销大,线程一般优先用pthread系列同步接口。