Last updated on 8 months ago
线程方面的知识一直是很薄弱的, 所以再次翻开 《APUE 》 ,重新再看一遍 第11 章 线程,有不一样的收获,在这总结下 线程的一些使用方法 ; (PS: 发现以前好多看得云里雾里的书籍,现在重头看,有种恍然大悟的感觉!)
线程概念
进程是程序执行时的一个实例,是担当分配系统资源(CPU时间、内存等)的基本单位。在面向线程设计的系统中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程包含了表示进程内执行环境必须的信息,其中包括进程中表示线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno常量以及线程私有数据。进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本、程序的全局内存和堆内存、栈以及文件描述符。在Unix和类Unix操作系统中线程也被称为轻量级进程(lightweight processes),但轻量级进程更多指的是内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程和进程一样也有标志号作为识别
线程创建 1 int pthread_create (pthread_t *__restrict __newthread, const pthread_attr_t *__attr,void *(*fun) (void *),void *arg) ;
参数分别是 线程的pid,线程属性,线程的函数,传入线程的参数,创建失败会返回错误码
对于 传参 的arg指针,不需要给线程传入参数的话直接 设置为 null即可
线程终止 终止有三种方式
如果需要 现有线程的返回值怎么办? 有一个函数可以做到
1 2 3 4 5 6 7 8 extern int pthread_join (pthread_t __th, void **__thread_return) ;
线程的返回转态可以保存在 __thread_return 中;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <arpa/inet.h> #include <errno.h> #include <netinet/tcp.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/epoll.h> #include <unistd.h> #include <string> #include <iostream> using namespace std ;void cleanup (void *arg) { printf ("cleanup : %s \n" ,(char *)arg); } void * fun (void *arg) { char *str = (char *)arg; printf ("%s\n" ,str); sleep(1 ); char *ret = "hrrrr" ; return ((void *)ret); }void * fun2 (void *arg) { char *str = (char *)arg; printf ("%s\n" ,str); sleep(1 ); char *ret = "cccc" ; pthread_exit((void *)ret); }int main () { pthread_t pid[5 ]; char *str1 = "str1 传入的参数内容" ; char *str2 = "str2 传入的参数内容" ; pthread_create(&pid[0 ],NULL ,fun,str1); pthread_create(&pid[1 ],NULL ,fun2,str2); void *tre,*tre1; pthread_join(pid[0 ],&tre); pthread_join(pid[1 ],&tre1); printf ("%s \n" ,(char *)tre); printf ("%s \n" ,(char *)tre1); sleep(10 ); return 1 ; }
线程结束后可以得到返回值,返回值分别是return返回的和 pthread_exit结束时返回的
线程同步 提到线程必然有很多问题 ,多线程并发的时,同时读取相同变量,不是原子性 可以能会有数据不一致的情况(i++)
i++ 操作 是
将内存读取寄存器
操作寄存器的值
新的值写内存单元
哪如何保证线程的同步呢?
互斥锁 对资源加锁后,任何线程试图再次对该互斥量加锁的线程都会阻塞,直到到该锁释放为止,这样可以确保每次只有一个线程可以获取资源,保证原子性
使用互斥锁得先初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 pthread_mutex_t __mutex;extern int pthread_mutex_init (pthread_mutex_t *__mutex, const pthread_mutexattr_t *__mutexattr) __THROW __nonnull ((1 )) ;extern int pthread_mutex_destroy (pthread_mutex_t *__mutex) __THROW __nonnull ((1 )) ;extern int pthread_mutex_trylock (pthread_mutex_t *__mutex) __THROWNL __nonnull ((1 )) ;extern int pthread_mutex_lock (pthread_mutex_t *__mutex) __THROWNL __nonnull ((1 )) ;extern int pthread_mutex_unlock (pthread_mutex_t *__mutex) __THROWNL __nonnull ((1 )) ;
使用的话就是在 访问临界区资源之前对 互斥量加锁,尽管这样可以解决线程同步的问题,但是如果存在多个锁,没有设计好加锁顺序,可能造成死锁,看代码
此时有两个锁和两个线程,每个线程加锁顺序相反的话,就会造成死锁, 互相等待对方释放资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <bits/stdc++.h> #include <unistd.h> using namespace std;pthread_mutex_t plock2;pthread_mutex_t plock1;pthread_mutex_t plock3 = PTHREAD_MUTEX_INITIALIZER;static int i = 0 ;void * fun (void *arg) { while (i < 100 ) { pthread_mutex_lock (&plock1); cout << "plock1 已经上锁,准备获取plock2锁 " << endl; pthread_mutex_lock (&plock2); cout << "plock2 已经上锁" << endl; pthread_mutex_unlock (&plock2); pthread_mutex_unlock (&plock1); } }void * fun2 (void *arg) { sleep (1 ); while (i < 100 ) { pthread_mutex_lock (&plock2); cout << "plock2 已经上锁,准备获取plock1锁" << endl; pthread_mutex_lock (&plock1); cout << "plock1 已经上锁" << endl; pthread_mutex_unlock (&plock2); pthread_mutex_unlock (&plock1); } }int main () { pthread_t pid[5 ]; char *str1 = "str1 传入的参数内容" ; char *str2 = "str2 传入的参数内容" ; pthread_mutex_init (&plock2,NULL ); pthread_mutex_init (&plock1,NULL ); pthread_create (&pid[0 ],NULL ,fun,str1); pthread_create (&pid[1 ],NULL ,fun2,str2); void *tre,*tre1; pthread_join (pid[0 ],&tre); pthread_join (pid[1 ],&tre1); printf ("%s \n" ,(char *)tre); printf ("%s \n" ,(char *)tre1); while (i < 100 ) { cout << "i的值" << i++ << endl; sleep (2 ); } return 1 ; }
plock1 和 plock2都 已经上锁,两个线程都在等待对方释放锁,才能进行下去,这种情况下双方都不会释放
读写锁( 共享互斥锁) 写操作的时候才会互斥,而在进行读的时候是可以共享的进行访问临界区的, 在写少读多的情况下用读写锁更好,为什么呢? 写的时候临界区加锁,这没毛病,读的时候并不需要进修改数据,多线程读数据并不影响数据内容,所以读的时候不用锁共享资源,也节省了系统的开销
读写锁的分配规则:
只有线程没有使用读写锁的写转态,那么任意数目的线程都可以使用读写锁
当有线程持有读写锁的写转态时,其他线程使用读写锁都会被阻塞
如果读写锁的读模式加锁时,所有线程以读模式它进行加锁的线程都可以得到访问权
做了实验 使用读写锁,一个线程写数据,两个线程读数据
写的时候,读线程全部被阻塞 读的时候,读线程全都可以获取资源,写线程被阻塞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 #include <bits/stdc++.h> #include <unistd.h> using namespace std;pthread_mutex_t plock2;pthread_mutex_t plock1;pthread_rwlock_t rw_lock;pthread_rwlock_t rwlock;int i = 13 ;void * fun (void *arg) { char *arg1 = (char *)arg; while (i < 100 ) { pthread_rwlock_wrlock (&rwlock); cout << "开启 写的锁后 i = " << i++ << " 并且i++" << endl; cout << "此时所获取这个锁的线程都会被堵塞 3s" << endl; cout << endl; cout << endl; sleep (3 ); pthread_rwlock_unlock (&rwlock); } }void * fun2 (void *arg) { sleep (1 ); char *arg1 = (char *)arg; while (i < 100 ) { pthread_rwlock_rdlock (&rwlock); cout << " 我是 fun2 开启只读的锁后 i =" << i << endl; pthread_rwlock_unlock (&rwlock); sleep (1 ); } }void * fun3 (void *arg) { sleep (1 ); char *arg1 = (char *)arg; while (i < 100 ) { pthread_rwlock_rdlock (&rwlock); cout << " 我是 fun3 开启只读的锁后 i =" << i << endl; pthread_rwlock_unlock (&rwlock); sleep (1 ); } }int main () { pthread_t pid[5 ]; char *str1 = "str1 传入的参数内容" ; char *str2 = "str2 传入的参数内容" ; pthread_rwlock_init (&rwlock,NULL ); pthread_create (&pid[0 ],NULL ,fun,str1); pthread_create (&pid[1 ],NULL ,fun2,str1); pthread_create (&pid[2 ],NULL ,fun3,str1); while (1 ) { sleep (1 ); } pthread_rwlock_destroy (&rwlock); return 1 ; }
条件变量 条件变量也是同步线程的同步的方式,但还是得借助互斥锁; 当一个线程获取锁之后,他需要等待一定的条件才能继续执行,执行完成再释放锁,这个条件可能是其他线程计算的结果,等待条件的时候可以 一直判断这个条件是否成立(加个while)但这样太消耗CPU的资源了,资源会被一个线程都占用,使用条件变量可以 使线程进入等待转态,等待其他线程完后发送信号给条件变量,从而继续执行
自旋锁 与互斥锁类似,等待互斥锁时线程是阻塞的,而自旋锁是一直在询问这个锁是否可用(会占用CPU),所以自旋锁适用于锁持有时间短的场景,长时间占用自旋锁是很占用CPU资源的,使用自旋锁的话对于 竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗,也是自旋锁适用于所持有时间的场景的原因。
补充 以上加锁的目的保证数据/操作是原子性的,如果说锁里面的操作/数据是原子性的,没有锁也是可以的,就好比 i++,我们都知道这个并不是 原子性的操作,但是如果说用一个函数实现 i++,并且是要原子性的可以吗?有一些汇编指令就是可以原子操作,在函数里嵌入 汇编语言,用这个汇编来使用 i++的操作也是可以的;(其实c++ 11中的 atmoic 可以,对于atomic 以后再介绍)
1 2 3 4 5 6 7 8 9 10 int inc(int *value, int add) { int old; __asm__ volatile ( "lock; xaddl %2, %1;" // "lock; xchg %2, %1, %3;" : "=a" (old) : "m" (*value), "a" (add) : "cc", "memory" ); return old; }
CAS(先留坑) 。。。。。。。。。。。。。。