文章目录
1、设置线程属性
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
void sys_err(char* str)
{
perror(str);
exit(-1);
}
void* tfn(void* arg)
{
printf("thread;pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(int argc, char* argv[])
{
pthread_t tid;
pthread_attr_t attr;
int ret = pthread_attr_init(&attr);
if (ret != 0) {
perror("pthread_attr_init error");
exit(1);
}
ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置线程属性为分离态
if (ret != 0) {
perror("pthread_attr_setdetachstate error");
exit(1);
}
ret = pthread_create(&tid, &attr, tfn, NULL); //创建线程
if (ret != 0) {
perror("pthread_create error");
exit(1);
}
ret = pthread_attr_destroy(&attr); //销毁线程属性
if (ret != 0) {
perror("pthread_attr_destroy error");
exit(1);
}
ret = pthread_join(tid, NULL); //回收线程 等待阻塞
if (ret != 0) { //设置分离后,无需回收,故会出错
perror("pthread_join error");
exit(1);
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
pthread_exit((void*)0);
return 0;
}
输出结果
2、线程同步
概念:协同步调,对公共区域数据按先后顺序访问。 防止数据混乱,产生与时间有关的错误。多个控制访问同一共享资源,必须同步。
一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回,且同时其他线程为 保证数据一致性,不能调用该功能。例如:银行取钱。
3、互斥锁
锁mutex的使用:建议锁,锁不会限制资源访问。
对公共数据进行保护。 所有线程应该在访问公共数据前先拿锁再访问,但锁本身不具备强制性。
使用mutex(互斥量、互斥锁)一般步骤:
1
、pthread_mutex_t lock
创建一个pthread_mutex_t类型的名为locks的变量,本质是结构体类型。
2
、pthread_mutex_init:初始化,相当于初值为1
3
、pthread_mutex_lock:加锁 1减减 -> 0
4
、访问共享数据(stdout)
5
、pthread_mutex_unlock 解锁 0加加 ->1,同时将阻塞在该锁上的所有线程全部唤醒
6
、pthread_mutex_destroy 销毁锁
初始化互斥量:
pthread_mutex_t mutex
1、pthread_mutex_init(&mutex, NULL):动态初始化
参数
互斥锁mutex(传出)
互斥锁属性attr(传入)
restrict关键字,用来限定指针变量,被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。
2、pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER:静态初始化
注意事项:
尽量保证锁的粒度,越小越好,访问共享数据前。加锁。访问结束,立即解锁。
互斥锁,本质为结构体,可以看作整数。初始化后为1。 pthread_mutex_init()函数调用成功。
加锁 --操作 阻塞线程 锁只有两种状态
解锁 ++操作 唤醒阻塞在锁上的线程
try锁 尝试加锁:
成功 减减(- -)
失败 返回错误号(EBUSY) 不阻塞
4、借助互斥锁管理共享数据实现同步
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
pthread_mutex_t mutex; //定义一把互斥锁
void* tfn(void* arg)
{
srand(time(NULL));
while (1) {
pthread_mutex_lock(&mutex); //加锁
printf("hello ");
sleep(rand() % 3); //模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
printf("world\n");
pthread_mutex_unlock(&mutex); //解锁
sleep(rand() % 3);
}
return NULL;
}
int main(int argc, char* argv[])
{
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");
pthread_mutex_unlock(&mutex); //解锁
sleep(rand() % 3);
}
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex); //销毁互斥锁
return 0;
}
输出结果如下
5、读写锁
读写锁:锁只有一把
以读方式给数据加锁–读锁
以写方式给数据加锁–写锁。解锁前,所有对该加锁的线程都会被阻塞。
读共享,写独占,多个线程去读都可以加锁成功。
写锁优先级高,当读和写一起来的时候,优先写锁操作。
相较于互斥量而言,当读线程多的时候,提高访问效率。
使用场景:适合于对数据结构读的次数远大于写。
读写锁的操作:
1
、pthread_rwlock_t rwlock
2
、pthread_rwlock_init(&rwlock, NULL)
3
、pthread_rwlock_rdlock(&rwlock)
4
、pthread_rwlock_wrlock(&rwlock)
5
、pthread_rwlock_unlock(&rwlock)
6
、pthread_rwlock_destroy(&rwlock)
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int counter;
pthread_rwlock_t rwlock; //定义一个全局的读写锁
//3个线程不定时写同一全局资源,5个线程不定时读同一全局资源
void* th_write(void* arg)
{
int t;
long i = (long)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock); //以写模式加锁,写独占
t = counter;
usleep(1000);
printf("---------write %ld: %lu: counter = %d ++counter = %d\n", i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(10000);
}
return NULL;
}
void* th_read(void* arg)
{
long i = (long)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock); //读线程间,读锁共享
printf("-----------read %ld: %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], NULL, th_read, (void*)i);
for (i = 0; i < 8; i++)
pthread_join(tid[i], NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}
输出结果
6、自旋锁
struct list_head done_list; //此链表中的缓冲区已填充数据,可以出队被用户空间取走使用
spinlock_t done_lock; //保护done_list链表的自旋锁
自旋锁 done_lock,用于保护 done_list 链表。自旋锁是一种常见的同步机制,用于在多线程环境中保护共享资源免受并发访问的影响。
自旋锁的特点是当某个线程尝试获取锁时,如果锁已经被其他线程占用,该线程不会进入睡眠状态,而是会一直循环等待,直到锁可用为止。因此,自旋锁适用于保护临界区较小且锁被占用时间较短的情况。
在这里,done_lock 用于保护 done_list 链表,以确保在多线程环境下对该链表的访问是线程安全的。这意味着任何试图对 done_list 进行修改或访问的线程都必须先获取 done_lock 锁,操作完成后再释放锁,以避免竞争条件和数据不一致性问题。
补充:自旋锁是一种多线程同步的机制,用于保护临界区资源的访问。与互斥锁不同,自旋锁并不会将线程阻塞在等待资源释放的地方,而是采用循环的方式一直尝试获取锁,直到成功为止。这种方式称为"自旋",因为线程在获取锁之前会一直循环忙等(自旋)。
自旋锁有两种状态:被锁定(Locked)和未锁定(Unlocked)。当一个线程尝试获取自旋锁时,如果锁处于未锁定状态,那么线程将将其状态设置为锁定,并继续执行临界区代码。如果自旋锁已经被其他线程锁定,那么线程将继续在循环中自旋,直到锁被释放为止。
7、互斥锁和自旋锁的区别
互斥锁(Mutex
:
- 阻塞行为:当一个线程试图获取一个已经被其他线程持有的互斥锁时,它会被阻塞,直到锁被释放。这种阻塞行为可以减少CPU的空转时间。
- 适用范围:互斥锁适用于锁持有时间较长的情况下,因为线程被阻塞,不会消耗CPU资源。
- 上下文切换:由于线程会被阻塞,需要切换上下文,这会有一些开销。
- 优点:适用于锁持有时间较长的情况,可以让线程释放CPU,以便执行其他任务。
- 缺点:在高性能需求的情况下,锁竞争导致的阻塞可能引起性能下降。
自旋锁(Spinlock)
:
- 循环等待:当一个线程试图获取一个已经被其他线程持有的自旋锁时,它会一直循环等待(空转)直到锁被释放。这种等待消耗CPU资源。
- 适用范围:自旋锁适用于锁持有时间很短的情况,因为它避免了上下文切换的开销。
- 上下文切换: 自旋锁不阻塞线程,避免了上下文切换,但可能会消耗大量的CPU时间。
- 优点:在锁持有时间短的情况下,避免了线程被阻塞和上下文切换,减少了锁竞争对性能的影响。
- 缺点:如果锁持有时间较长,自旋锁会消耗大量的CPU资源,导致性能下降。
总体来说,选择使用互斥锁还是自旋锁取决于锁持有时间的长短。如果锁持有时间较长,建议使用互斥锁以避免CPU资源的浪费;如果锁持有时间较短,建议使用自旋锁以避免上下文切换的开销。
在多核处理器上,自旋锁的效率通常比较高,因为在自旋等待期间,线程可以利用CPU时间片继续执行其他任务,而不是被操作系统挂起和重新调度。
应用
:
互斥锁:vb2_queue的互斥锁,使缓冲队列的操作串行化 因为会进行数据的copy,时间长
自旋锁:
1、队列缓冲区链表的增删改查 链表操作时间较短
2、在网卡发送网络包时,要求较高的实时性,网卡是PCIE,并行网络包发送速度会很快
8、条件变量
条件变量:本身不是锁,但是通常结合mutex锁来使用。
初始化条件变量:首先定义一个条件变量 pthread_cond_t cond;
方式1:动态初始化 pthread_cond_init (&cond, NULL);
方式2:静态初始化 pthread_cond_t cond = PTHREAD_COND_INITIALIZER
pthread_cond_wait函数:
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)
阻塞等待一个条件变量
作用
1
、阻塞等待条件变量cond满足。
2
、释放已经掌握的互斥锁mutex。相当于pthread_mutex_unlock(&mutex)
3
、被唤醒时,函数返回时解除阻塞,重新申请获取互斥锁。2和3有一个历时的时长。
1、2步为原子操作。同时完成,不可分。
pthread_cond_timedwait 超时等待
pthread_cond_signal 唤醒(至少)一个阻塞在条件变量上的线程
pthread_cond_broadcast 唤醒全部阻塞在条件变量上的 所有线程
pthread_cond_destroy 销毁条件变量
9、条件变量实现生产者消费者模型
//借助条件变量模拟 生产者-消费者 问题
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.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 %lu---%d\n", pthread_self(), 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;
}
10、信号量
信号量:应用于线程、进程同步。支持多个线程访问共享区域
相当于初始化值为 N的互斥量,N值表示可以同时访问共享数据区的线程数。保证同步的同时,提高并发。信号量的初值,决定了占用信号量的线程的个数。
sem_t sem
:定义结构体类型,N代表线程数量,N不能<0,头文件#include <semaphore.h>。
sem_init
(sem_t *sem , int pshared, unsigned int value)
参数:
sem:信号量
pshared: 0:用于线程间同步, 1:用于进程间同步
value:N值(指定同时访问的进程数)
sem_destroy()
sem_wait()
类比于pthread_mutex_lock。 一次调用,做一次–减减操作。 当信号量的值为0时,再–减减就会阻塞。
sem_post()
类比于pthread_mutex_unlock。一次调用,做一次++操作。 当信号量的值为N时,再加加就会阻塞。
11、信号量实现生产者消费者模型
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <semaphore.h>
#define NUM 5
int queue[NUM]; //全局数组实现环形队列
sem_t blank_number, product_number; //空格子信号量 5,产品信号量 0
void* producer(void* arg)
{
int i = 0;
while (1) {
sem_wait(&blank_number); //生产者将空格子数--,为0则阻塞等待
queue[i] = rand() % 1000 + 1; //生产一个产品
printf("---Produce---%d\n", queue[i]);
sem_post(&product_number); //将产品数++
i = (i + 1) % NUM; //借助下标实现环形
sleep(rand() % 1);
}
}
void* consumer(void* arg)
{
int i = 0;
while (1) {
sem_wait(&product_number); //消费者将产品数--, 为0则阻塞等待
printf("-Consume---%d\n", queue[i]);
queue[i] = 0; //消费一个产品
sem_post(&blank_number); //消费掉以后,格子数++
i = (i + 1) % NUM;
sleep(rand() % 3);
}
}
int main(int argc, char* argv[])
{
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;
}
输出结果如下
12、RCU
RCU(Read-Copy-Update)是一种高效的同步机制,主要用于优化多线程或多核环境中的读取操作,减少写操作对读取操作的影响。它特别适用于读操作频繁而写操作相对少的场景。
RCU 的工作原理
1、读取:
读取操作通常不需要加锁,读操作只需访问数据的快照。
在 RCU 中,读取操作可以并发执行,因为它们读取的数据版本是稳定的,不会被写操作修改。
2、更新:
写操作不会直接修改正在被读取的数据。相反,它会创建数据的一个新的版本。
更新操作通常涉及三个步骤:
复制:创建数据的新版本。
更新:将指向数据的引用更新为新版本。
回收:等待所有之前的读取操作完成,然后回收旧版本的数据。
3、延迟回收:
RCU 使用延迟回收机制,即使数据的旧版本被更新,旧版本的数据在新版本被完全替换后,仍然会保留一段时间,直到确保所有读取操作完成后再回收。
内核源码:kernel-4.9\net\bluetooth\6lowpan.c
static void disconnect_devices(void)
{
struct lowpan_btle_dev *entry, *tmp, *new_dev;
struct list_head devices;
INIT_LIST_HEAD(&devices);
/* We make a separate list of devices because the unregister_netdev()
* will call device_event() which will also want to modify the same
* devices list.
*/
rcu_read_lock();
list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
new_dev = kmalloc(sizeof(*new_dev), GFP_ATOMIC);
if (!new_dev)
break;
new_dev->netdev = entry->netdev;
INIT_LIST_HEAD(&new_dev->list);
list_add_rcu(&new_dev->list, &devices);
}
rcu_read_unlock();
}
它首先通过 RCU 读锁创建一个副本列表,防止在遍历 bt_6lowpan_devices 列表时发生竞争条件。