操作系统之线程间同步方式

    技术2023-12-18  74

    比起进程复杂的通信机制(管道、匿名管道、消息队列、信号量、共享内存、内存映射以及socket等),线程间通信要简单的多。因为同一进程的不同线程共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段,所以线程之间可以方便、快速地共享信息。只需要将数据复制到共享(全局或堆)变量中即可。不过,要避免出现多个线程试图同时修改同一份信息也即得使用线程同步功能。

     

    线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。

    预备知识

    线程的主要优势在于, 能够通过全局变量来共享信息( 因为 所有线程共享相同的全局和堆变量(共享代码区、数据区、堆区),但每个线程都配有用来存放局部变量和函数调用链接信息的私有栈。 不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正由其他线程修改的变量。 临界区(critical section) 是指访问某一共享资源的代码片段,并且这段代码的执行应为 原子(atomic)操作(C++11支持原子操作类),亦即,同时访问同一共享资源的其他线程不应中断该片段的执行。

     

    线程同步实例:

    i++; 编译器依然会将这条语句转换成机器码,其执行步骤仍旧等同于以下语句。 int temp = i; temp = temp + 1; i = tmep; 其操作不属于原子操作,因为上述3条语句会随时因为CPU分配时间到期而遭到打断。

    为避免线程更新共享变量时所出现问题,使用互斥量( mutex 是 mutual exclusion 的缩写)来确保同时仅有一个线程可以访问某项共享资源。更为全面的说法是,可以使用互斥量来保证对任意共享资源的原子访问(C++11在多线程编程也是存在原子类型的),而保护共享变量是其最常见的用法。

    一旦线程锁定互斥量,随即成为该互斥量的所有者。只有所有者才能给互斥量解锁。这一属性改善了使用互斥量的代码结构,也顾及到对互斥量实现的优化。因为所有权的关系,有时会使用术语获取(acquire)和释放(release)来替代加锁和解锁。

     

    互斥量mutex(又称互斥锁)

    互斥量本质上是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。

    互斥量mutex是 mutual exclusion 的缩写。同一时刻,互斥量只有一个,只能持有该互斥量的线程有权去访问系统的公共资源。 互斥量既可以像静态变量那样分配,也可以在运行时动态创建(例如,通过 malloc( )在一块内存中分配)。

    互斥量是属于pthread_mutex_t 结构体类型的变量(相当于只能取0 或 1),在使用之前必须对其进行初始化。

    相关函数

    pthread_mutex_init函数(初始化动态互斥量)

    #include<pthread.h> int pthread_mutex_init(pthread_mutex *restrict mutex, const pthread_mutexattr_t *restrict attr) 参数: mutex:传出参数;(调用完之后其值为 mutex == 1) attr:互斥量属性,传NULL选用默认属性;(默认是用于线程之间,可以修改属性用于进程之间同步) 【注】 restrict关键字:只用于限制指针;编译器修改该指针指向内存中内容的操作,只能通过本指针完成。 不能通过除本指针以外的其他变量或指针修改。

    宏:PTHREAD_MUTEX_INITIALIZER(初始化静态互斥量且携带默认属性

    pthread_mutex_lock函数:加锁。可理解为mutex-- (或 -1)

    int pthread_mutex_lock(pthread_mutex * mutex);

    pthread_mutex_trylock函数

    尝试加锁;如果信号量已然锁定,对其执行函数 pthread_mutex_trylock()会失败并返回 EBUSY 错误; 除此之外,该函数与 pthread_mutex_lock()行为相同。

    int pthread_mutex_trylock(pthread_mutex * mutex); 死锁:是 指两个或两个进程在执行过程中,因争夺资源而造成的相互等待的现象;若无外力的作用,他们都将不发进行下去。   解决:pthread_mutex_trylock:当请求加锁失败时自动解除自身的锁; 产生 死锁 的4个必要条件为 互斥条件:一份资源只能分配到一个进程,不能被其他进程共享; 请求保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放; 不可剥夺条件:已经分配到进程的资源,在未使用完之前不能强行剥夺; 循环等待条件:进程间形成一种有序(首位相连)的循环等待资源的关系; 以上必要条件,只要系统发生死锁,这些条件必然成立,而只要有一个条件不满足,则死锁就不会发生 因此要避免死锁的产生,只要让上述 3个必要条件不成立即可 因为 互斥条件无法被破环

     

    pthread_mutex_timedlock函数: 调用者可以指定一个附加参数 abstime(设置线程等待获取互斥量时休眠的时间限制)外,函数 pthread_mutex_timedlock()与 pthread_mutex_lock()没有差别。如果参数 abstime 指定的时间间隔期满,而调用线程又没有获得对互斥量的所有权,那么函数 pthread_mutex_timedlock()返回 ETIMEDOUT 错误。

    pthread_mutex_unlock函数:解锁。可理解为mutex++(或 +1)

    int pthread_mutex_unlock(pthread_mutex * mutex);

    pthread_mutex_destory函数:销毁一个互斥锁

    int pthread_mutex_destory(pthread_mutex * mutex);

     

    互斥量使用实例如下:

    #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<pthread.h> pthread_mutex_t mutex;//定义锁 void* my_main_thread_func(void* arg) { srand( time(NULL) ); while(1){ pthread_mutex_lock( &mutex );//加锁 printf("thread hello "); sleep( rand() % 3 ); printf("world \n"); pthread_mutex_unlock( &mutex );//解锁 sleep( rand() % 3 ); } return NULL; } int main(void) { pthread_t tid; srand( time(NULL) ); pthread_mutex_init(&mutex, NULL);//互斥锁初始化 pthread_create(&tid, NULL, my_main_thread_func, NULL);//创建线程 while(1){ pthread_mutex_lock( &mutex );//加锁 printf("main Hello "); sleep( rand() % 3); printf("World\n"); pthread_mutex_unlock( &mutex );//解锁 sleep( rand() % 3); } pthread_cancel(tid); // 线程取消 pthread_join( tid, NULL); // 线程回收 pthread_mutex_destory(&mutex);//销毁锁 return 0; }

    互斥量属性

    在函数int pthread_mutex_init(pthread_mutex *restrict mutex, const pthread_mutexattr_t *restrict attr)中,参数attr是指定互斥量属性的传入参数; 通过 pthread_mutexattr_t 类型对象对互斥量属性进行设置;

    pthread_mutexattr_init 函数: pthread_mutexattr_settype 函数:设置互斥量类型; pthread_mutexattr_destory 函数: 对于互斥量类型 PTHREAD_MUTEX_NORMAL:该类型的互斥量不具有死锁检测(自检)功能 PTHREAD_MUTEX_ERRORCHECK: 此类互斥量的所有操作都会执行错误检查 PTHREAD_MUTEX_RECURSIVE:递归互斥量维护有一个锁计数器。当线程第 1 次取得互斥量时,会将锁计数器置 1。 后续由同一线程执行的每次加锁操作会递增锁计数器的数值,而解锁操作则递减计数器计数。只有当锁计数器值降至 0 时, 才会释放( release,亦即可为其他线程所用)该互斥量

    互斥量属性设置实例:

    int s,type; pthread_mutex_t mtx; pthread_mutexattr_t mtxAttr; s=pthread_mutexattr_init(&mtxAttr); s=pthread_mutexattr_settype(&mtxAttr,PTHREAD_MUTEX_ERRORCHECK); s=pthread_mutex_init(mtx,&mtxAttr); s=pthread_mutexattr_destroy(&mtxAttr);/* No 1onger needed*/ 以上均省略了出错判断的步骤!

    自旋锁(spin)

    当临界区(critical section)是一段很小的代码,线程进入临界区,马上就会退出。这时,如果使用互斥量,会导致大量的上下文切换,所以就用了自旋锁。自旋锁的使用与互斥量类似,但是等待自旋锁时,进程不会释放cpu,而是一直占用cpu。所以自旋锁适用情况是临界区的代码不会执行时间太长,而且竞争不是太激烈。否则大量等待自旋锁的线程就像死锁一样。 #include <linux/spinlock.h> void spin_lock_init(spinlock_t *lock); void spin_lock(spinlock_t *lock); void spin_unlock(spinlock_t *lock);

    内核中也有自旋锁,在linux下提供了应用层的自旋锁。

       

    条件变量(Condition Variable)

    条件变量允许一个线程就某个共享变量或其他共享资源的满足某种条件时通知其他线程,不满足条件时让其他线程堵塞,等待条件满足的通知。本身不是锁,但可以造成线程阻塞,通常配合互斥锁使用。 如同互斥量一样,条件变量的分配,有静态和动态之分。 条件变量的数据类型是 pthread_cond_t。类似于互斥量, 使用条件变量前必须对其初始化。对于经由静态分配的条件变量,将其赋值为 PTHREAD_COND_INITALIZER(宏) 即完成初始化操作。 使用 pthread_cond_init( )做了动态初始化。  

    相关函数

    pthread_cond_init函数:动态初始化条件变量;   pthread_cond_wait函数:阻塞线程,直至收到条件变量唤醒; int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); 函数作用: 1、阻塞等待条件变量cond满足; 2、释放互斥锁 = pthread_mutex_unlock( &mutex ); 3、当被唤醒,pthread_cond_wait函数返回,解除阻塞并重新申请获取互斥所pthread_mutex_lock( &mutex ) 返回值: 参数:

    pthread_cond_timedwait函数:显示等待一个条件变量;

    int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); 返回值: 参数: abstime:需要使用绝对时间; 【注】绝对时间(相对于1970/01/01 00:00:01 过去的时间)的获取 time_t cur = time(NULL); struct timespec t; t.tv_sec = cur + 1;//定时1s 因此可以有:pthread_cond_timedwait(&cond, &mutex, &t); pthread_cond_signal 函数:对参数指定的条件变量发送信号,唤醒至少一个阻塞在条件变量上的线程;   pthread_cond_broadcast 函数:唤醒所有阻塞在条件变量上的线程;   pthread_cond_destroy 函数: 销毁一个条件变量;    

    生产者-消费者模型

    这是面向过程的高效的编程设计模式,而非GoF提出的23中面向对象的设计模式;

    优点:

    生产者和消费者是两个类,,解耦合。。

    生产者和消费者可以是两个独立的并发主体,,支持并发。。

    生产者-消费者模式的实现方法:Synchronized、Lock、Semaphore、BlockingQueue四种;

      条件变量实生产者消费者模型现代码如下: #include<stdio.h> #include<unistd.h> #include<pthread.h> #include<stdlib.h> struct msg{ int num; struct msg *next; }; struct msg *head; struct msg *mp; //静态初始化 pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;//条件变量 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//互斥量 void* consumer(void* arg) { while(1) { pthread_mutex_lock( &lock);//加锁 while( head == NULL) pthread_cond_wait( &has_product, &lock);//在此函数中有加锁操作 mp = head; head = mp->next;//模拟消费一个产品 pthread_mutex_unlock( &lock);//解锁 printf("consumer---%d\n", mp->num); free(mp);//线程之间是共享堆的 sleep( rand() % 5); } return NULL; } void* producer(void* arg) { while(1) { 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); } return NULL; } int main(void) { pthread_t ptid, ctid; srand( time(NULL) ); pthread_create( &ptid, NULL, producer, NULL); pthread_create( &ctid, NULL, consumer, NULL); pthread_join(ptid, NULL); pthread_join(ctid, NULL); return 0; }

    信号量(semaphore)

    此处讲的是POSIX 信号量,内容可见《UNIX环境高级编程(第二版)》P436 和《Linux_UNIX系统编程手册上下册》 P940。而非System V 信号量;

    信号量:是一个计数器,用于控制多个进程间对共享资源的访问;

    信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。是进化版的互斥锁(信号量是信号量,与信号无关,别搞混了)

    POSIX 信号量分为两种: 命名信号量和匿名信号量;

    命名信号量:这种信号量拥有一个名字。通过使用相同的名字调用 sem_open(),不相关的进程能够访问同一个信号量。

    匿名信号量:这种信号量没有名字,相反,它位于内存中一个预先商定的位置处。匿名信号量可以在进程之间或一组线程之间共享。当在进程之间共享时,信号量必须位于一个共享内存区域中(System V、 POSIX 或 mmap( ))。当在线程之间共享时,信号量可以位于被这些线程共享的一块内存区域中(如在堆上或在一个全局变量中)。

    命名信号量

    要使用命名信号量必须要使用下列函数。

    sem_open()函数打开或创建一个信号量并返回一个句柄以供后续调用使用, 如果这个调用会创建信号量的话还会对所创建的信号量进行初始化。 sem_post(sem)函数递增一个信号量值。 sem_wait(sem)函数递减一个信号量值。 sem_getvalue()函数获取一个信号量的当前值。 sem_close()函数删除调用进程与它之前打开的一个信号量之间的关联关系。 sem_unlink()函数删除一个信号量名字并将其标记为在所有进程关闭该信号量时删除该信号量。

    sem_post函数:递增一个信号量;

    # include<semaphore.h> int sem_post(semt * sem); 如果在 sem_post()调用之前信号量的值为 0,并且其他某个进程(或线程) 正在因等待递减这个信号量而阻塞,那么该进程会被唤醒,它的 sem_wait()调用会继续往前执行来递减这个信号量。

    sem_wait函数:递减一个信号量;

    # include<semaphore.h> int sem_wait(sem_t * sem); 如果信号量的当前值大于 0,那么 sem_wait()会立即返回。如果信号量的当前值等于 0, 那么 sem_wait()会阻塞直到信号量的值大于 0 为止, 当信号量值大于 0 时该信号量值就被递减并且 sem_wait()会返回。

    名信号量

    匿名信号量也被称为基于内存的信号量。 是类型为 sem_t(使用期间可当做整数使用) 并存储在应用程序分配的内存中的变量。通过将这个信号量放在由几个进程或线程共性的内存区域中就能够使这个信号量对这些进程或线程可用。 未命名信号量所使用的函数与操作命名信号量使用的函数是一样的( sem_wait()、sem_post()以及 sem_getvalue()等) 以下两函数不能属于匿名信号量独有!     sem_init 函数: 使用 value 中指定的值来对 sem 指向的未命名信号量进行初始化。   # include<semaphore.h> int sem_init(sem_t * sem, int pshared, unsigned int value); 返回值: 成功返回0, 失败返回-1并设置erro 参数: pshared:表明这个信号量是在线程间共享还是在进程间共享。 等于0,那该信号量将会在调用进程中的线程间进行共享; 不等于 0,那么信号量将会在进程间共享;这种情况下,sem 必须是共享内存区域 (一个 POSIX 共享内存对象、一个使用 mmap()创建的共享映射、或一个System V 共享内存段)中的某个位置的地址。

    sem_destroy函数: 销毁一个信号量;

    # include<semaphore.h> int sem_destroy(sem_t * sem); 其中 sem 必须是一个之前使用 sem_init()进行初始化的未命名信号量

    匿名信号量实生产者消费者模型现代码如下:

    #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<pthread.h> #include <semaphore.h> #define NUM 5 // 信号量数 int queue[NUM]; sem_t blank_number, product_number;//两个信号量 void* consumer(void* arg) { int i = 0; while(1) { sem_wait( &product_number);//生产者将空格子数 -- ,为0则阻塞等待 printf("---blank---%d\n", queue[i]);//将产品数++ queue[i] = 0;//消费一个产品 sem_post( &blank_number); i = (i + 1) % NUM;//借助下标实现环形队列 sleep( rand() % 3); } return NULL; } void* product(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() % 3 ); } return NULL; } int main(void) { pthread_t ptid, ctid; sem_init( &blank_number, 0, NUM);//信号量为5 sem_init( &product_number, 0, 0);//产品数 pthread_create( &ptid, NULL, product, NULL);//线程创建 pthread_create( &ctid, NULL, consumer, NULL); pthread_join(ptid, NULL);//线程回收 pthread_join(ctid, NULL); sem_destroy( &blank_number);//销毁信号量 sem_destroy( &product_number); return 0; }

     

    读写锁

    读写锁与互斥量类似,不过读写锁允许更高的并行性。

    互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。

    读写锁可以有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态

    一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁

    读写锁非常适合于对数据结构读的次数远大于写的情况。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为当前只有一个线程可以在写模式下拥有这个锁。当读写锁在读模式下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结构可以被多个获得读模式锁的线程读取。写锁独占;读锁共享;写锁优先级高

      pthread_rwlock_init 函数:   # include <pthread.h> int pthread rwlock_init(pthread_rwlock t * restrict rwlock, const pthread_rwlockattr_t * restrict attr);

    pthread_rwlock_rdlock函数:添加读锁

    # include <pthread.h> int pthread_rwlock_rdlock(pthread_rwlock t * rwlock);

    pthread_rwlock_wrlock函数:添加写锁

    # include <pthread.h> int pthread_rwlock_wrlock(pthread_rwlock_t * rwlock); pthread_rwlock_tryrdlock 函数:   pthread_rwlock_trywrlock 函数:   pthread_rwlock_unlock 函数: # include <pthread.h> int pthread_rwlock_unlock(pthread_rwlock_t * rwlock);

    pthread_rwlock_destroy函数:

    # include <pthread.h> int pthread_rwlock_destroy(pthread_rwlock_t * rwlock);

    读写锁实例如下:

    #include<stdio.h> #include<pthread.h> #include<unistd.h> int global_var; pthread_rwlock_t rwlock; void* my_write_thread(void* arg) { int t; int i = (int)arg; while(1) { t = global_var; sleep(3); pthread_rwlock_wrlock( &rwlock);//添加写锁 printf("write %d: %lu: global_var=%d ++global_var=%d\n", i, pthread_self(), t, ++global_var); pthread_rwlock_unlock( &rwlock);//释放读锁 sleep(3); } return NULL; } void* my_read_thread(void* arg) { int i = (int)arg; while(1) { pthread_rwlock_rdlock( &rwlock);//添加读锁 printf("read %d: %lu: %d\n", i, pthread_self(), global_var); pthread_rwlock_unlock( &rwlock);//释放读锁 sleep(1); } 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, my_write_thread, (void*)i );//创建线程 for(i = 0; i < 5; ++i) pthread_create( &tid[i+3], NULL, my_read_thread, (void*)i );//创建线程 for(i = 0; i < 8; ++i) pthread_join( tid[i], NULL);//回收子线程 pthread_rwlock_destroy( &rwlock );//释放读写锁 return 0; }

    总结:(待完善...)

    多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。线程间的通信目的主要是用于线程同步, 所以线程没有像进程通信中的用于数据交换的通信机制。 锁机制:包括互斥锁、条件变量、读写锁; 互斥锁提供了以排他方式防止数据结构被并发修改的方法。 读写锁允许多个线程同时读共享数据,而对写操作是互斥的。 条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。 对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量 信号机制(Signal):类似进程间的信号处理

     

           
    Processed: 0.010, SQL: 9