这是 C 语言版的,C++ 版 https://xingzhu.top/archives/duo-xian-cheng-xian-cheng-chi
线程基础
进程有自己独立的地址空间,多个线程共用同一个地址空间
线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位
CPU 划分时间片,多个线程抢占时间片执行
线程的上下文切换比进程要快的多
上下文切换是指继续上次线程没执行完的部分,接着执行后续的操作
Linux 看来,线程就是轻量版的进程,但是 Windows 不是这样的
父线程创建的子线程,子线程之间共享堆区、全局区、代码区,但是栈区和寄存器是各自独有
线程基本函数
在一个 main 函数中,是一个进程,此时创建线程后,这个进程变为了父线程和子线程
创建线程 1 pthread_t pthread_self (void ) ;
1 2 3 4 #include <pthread.h> int pthread_create (pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg) ;
参数:
thread
: 传出参数,是无符号长整形数,线程创建成功, 会将子线程 ID 写入到这个指针指向的内存中
attr
: 线程的属性, 一般情况下使用默认属性即可, 写 NULL
start_routine
: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行
arg
: 作为实参传递到 start_routine
指针指向的函数内部
返回值:线程创建成功返回 0,创建失败返回对应的错误号
线程退出 1 2 #include <pthread.h> void pthread_exit (void *retval) ;
参数: 线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为 NULL
只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,其他线程照常进行,父进程退出,子线程同理会继续执行(如果分离线程了,不分离线程,虽然也可执行,但是会造成内存泄漏,子线程资源得不到收回)
线程回收 1 2 3 4 #include <pthread.h> int pthread_join (pthread_t thread, void **retval) ;
参数:
thread
: 要被回收的子线程的线程 ID
retval
: 二级指针, 指向一级指针的地址, 是一个传出参数, 这个地址中存储了 pthread_exit()
传递出的数据,如果不需要这个参数,可以指定为 NULL
返回值:线程回收成功返回 0,回收失败返回错误号
如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收
void * 是指任何类型的都可以指向
示例
使用主线程栈方式获取子线程数据
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 #include <stdio.h> #include <string.h> #include <pthread.h> struct Persion { int id; char name[36 ]; int age; }; void * working (void * arg) { struct Persion * p = (struct Persion*)arg; printf ("我是子线程, 线程ID: %ld\n" , pthread_self()); for (int i=0 ; i<9 ; ++i) { printf ("child == i: = %d\n" , i); if (i == 6 ) { p->age =12 ; strcpy (p->name, "tom" ); p->id = 100 ; pthread_exit(p); } } return NULL ; } int main () { pthread_t tid; struct Persion p ; pthread_create(&tid, NULL , working, &p); printf ("子线程创建成功, 线程ID: %ld\n" , tid); printf ("我是主线程, 线程ID: %ld\n" , pthread_self()); for (int i = 0 ; i < 3 ; ++i) { printf ("i = %d\n" , i); } void * ptr = NULL ; pthread_join(tid, &ptr); struct Persion * ptr2 = (struct Persion*)ptr; printf ("name: %s, age: %d, id: %d\n" , ptr2->name, ptr2->age, ptr2->id); void * ptr = NULL ; void **ptr1 = &ptr; pthread_join(tid, ptr1); struct Persion ** ptr3 = (struct Persion**)ptr1; printf ("name: %s, age: %d, id: %d\n" , (*ptr3)->name, (*ptr3)->age, (*ptr3)->id); printf ("name: %s, age: %d, id: %d\n" , p.name, p.age, p.id); printf ("子线程资源被成功回收...\n" ); return 0 ; }
总结
首先在主线程访问的子线程资源,一定要是有效的,子线程定义在栈区的资源会被释放,这种就可以在主线程传地址进去,还可全局变量
这里以传地址形式示例返回值使用教程
要明白这两个线程函数(退出和回收的机制),当传递 void **retval
时,pthread_join
可以通过解引用这个指针来修改它指向的一级指针,从而将子线程返回的指针传递回主线程,本质是修改了一级指针指向的(存的)地址值
所以要用二级指针,一级指针就不能修改地址值了,不灵活了,然后这个指针就能访问子线程数据了
上述案例可能不太好,因为可以直接调用,是从主线程传地址进去的 但使用返回值可以访问子线程创建在堆区的数据,这是一个使用场景
线程分离 1 2 3 #include <pthread.h> int pthread_detach (pthread_t thread) ;
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 void * working (void * arg) { printf ("我是子线程, 线程ID: %ld\n" , pthread_self()); for (int i = 0 ; i < 20 ; ++i) { printf ("child == i: = %d\n" , i); } return NULL ; } int main () { pthread_t tid; pthread_create(&tid, NULL , working, NULL ); printf ("子线程创建成功, 线程ID: %ld\n" , tid); pthread_detach(tid); pthread_exit(NULL ); return 0 ; }
程序会打印完子线程的内容,然后被回收
注意,若不调用 pthread_exit(NULL);
,父线程声明周期结束,那么内存会被释放,而子线程是使用的父线程内存,所以子线程会被终止
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void * working (void * arg) { printf ("我是子线程, 线程ID: %ld\n" , pthread_self()); for (int i = 0 ; i < 10000 ; ++i) { printf ("child == i: = %d\n" , i); } return NULL ; } int main () { pthread_t tid; pthread_create(&tid, NULL , working, NULL ); printf ("子线程创建成功, 线程ID: %ld\n" , tid); pthread_detach(tid); usleep(1000 ); return 0 ; }
上述休眠只是为了演示,看出此时子线程还为执行完毕就终止了
这个案例看出父线程终止,子线程也会随之终止
线程同步 互斥锁
1 2 3 4 5 6 7 8 int pthread_mutex_init (pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr) ;int pthread_mutex_destroy (pthread_mutex_t *mutex) ;
mutex
: 互斥锁变量的地址
attr
: 互斥锁的属性, 一般使用默认属性即可,这个参数指定为 NULL
1 2 3 4 5 6 7 8 int pthread_mutex_lock (pthread_mutex_t *mutex) ;int pthread_mutex_trylock (pthread_mutex_t *mutex) ;
不是所有的线程都可以对互斥锁解锁,哪个线程加的锁, 哪个线程才能解锁成功
示例
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 #include <pthread.h> int cur; pthread_mutex_t mutex; void * funcA_num (void * arg) { pthread_mutex_lock(&mutex); cur++; pthread_mutex_unlock(&mutex); return NULL ; } void * funcB_num (void * arg) { pthread_mutex_lock(&mutex); cur++; pthread_mutex_unlock(&mutex); return NULL ; } int main (int argc, const char * argv[]) { pthread_t p1, p2; pthread_mutex_init(&mutex, NULL ); pthread_create(&p1, NULL , funcA_num, NULL ); pthread_create(&p2, NULL , funcB_num, NULL ); pthread_join(p1, NULL ); pthread_join(p2, NULL ); pthread_mutex_destroy(&mutex); return 0 ; }
读写锁 概述
1 pthread_rwlock_t rwlock;
锁的记录
锁的状态: 锁定/打开
锁定的是什么操作: 读操作/写操作,使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之亦然
哪个线程将这把锁锁上了
读写锁特点:
使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的
使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的
使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为 写锁比读锁的优先级高
线程对共享资源写操作,读写锁和互斥锁一样,读写锁没有优势 线程对共享资源有读操作和写操作,且读操作较多,读写锁优势明显
函数 1 2 3 4 5 6 7 8 #include <pthread.h> pthread_rwlock_t rwlock;int pthread_rwlock_init (pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr) ;int pthread_rwlock_destroy (pthread_rwlock_t *rwlock) ;
rwlock
: 读写锁的地址,传出参数
attr
: 读写锁属性,一般使用默认属性,指定为 NULL
1 2 int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock) ;
调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞
1 2 int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock) ;
调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞
不阻塞的加锁
1 2 3 4 5 6 int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock) ;int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock) ;
1 2 int pthread_rwlock_unlock (pthread_rwlock_t *rwlock) ;
示例 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> int number = 0 ; pthread_rwlock_t rwlock; void * writeNum (void * arg) { for (int i = 0 ; i < 100 ; i++) { pthread_rwlock_wrlock(&rwlock); number++; printf ("写操作: tid = %ld, number : %d\n" , pthread_self(), number); pthread_rwlock_unlock(&rwlock); usleep(rand() % 5000 ); } return NULL ; } void * readNum (void * arg) { for (int i = 0 ; i < 100 ; i++) { pthread_rwlock_rdlock(&rwlock); printf ("读操作: tid = %ld, number = %d\n" , pthread_self(), number); pthread_rwlock_unlock(&rwlock); usleep(rand() % 5 ); } return NULL ; } int main () { pthread_rwlock_init(&rwlock, NULL ); pthread_t wtid[3 ]; pthread_t rtid[5 ]; for (int i = 0 ; i < 3 ; ++i) { pthread_create(&wtid[i], NULL , writeNum, NULL ); } for (int i=0 ; i<5 ; ++i) { pthread_create(&rtid[i], NULL , readNum, NULL ); } for (int i = 0 ; i < 3 ; ++i) { pthread_join(wtid[i], NULL ); pthread_join(rtid[i], NULL ); } pthread_rwlock_destroy(&rwlock); return 0 ; }
1 2 3 4 5 6 7 8 9 10 写操作: tid = 1 , number : 1 写操作: tid = 1 , number : 2 写操作: tid = 2 , number : 3 写操作: tid = 2 , number : 4 写操作: tid = 3 , number : 5 写操作: tid = 3 , number : 6 读操作: tid = 4 , 全局变量number = 6 读操作: tid = 4 , 全局变量number = 6 读操作: tid = 4 , 全局变量number = 6
条件变量 函数
用于阻塞线程和唤醒线程
1 2 3 4 5 6 7 8 #include <pthread.h> pthread_cond_t cond;int pthread_cond_init (pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr) ;int pthread_cond_destroy (pthread_cond_t *cond) ;
1 2 int pthread_cond_wait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex) ;
这个函数执行后,如果阻塞了,如果当前上了锁,会把当前锁打开,防止后续的死锁,比如生产者要生产等
当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个 mutex
互斥锁锁上,继续向下访问临界区
被唤醒后,如果抢到了互斥锁控制权,则继续执行这个 pthread_cond_wait
之后的执行体
因此这个条件变量的顺序只能是在加锁的后面,也就是临界区内,因为会自动解锁和加锁,如果实现在加锁前,就相当于二次加锁,会导致死锁现象
1 2 3 4 5 6 7 8 9 struct timespec { time_t tv_sec; long tv_nsec; }; int pthread_cond_timedwait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime) ;
第三个参数表示线程阻塞的时长,如果条件变量达到要求,线程不阻塞
如果不达要求,会阻塞,但是如果超过阻塞时长,还为达到要求,返回 -1
需要额外注意一点:struct timespec
这个结构体中记录的时间是从 1971.1.1
到某个时间点的时间,总长度使用秒/纳秒表示
1 2 3 4 time_t mytim = time(NULL ); struct timespec tmsp ;tmsp.tv_nsec = 0 ; tmsp.tv_sec = time(NULL ) + 100 ;
1 2 3 4 int pthread_cond_signal (pthread_cond_t *cond) ;int pthread_cond_broadcast (pthread_cond_t *cond) ;
pthread_cond_signal
是唤醒至少一个被阻塞的线程(总个数不定)
pthread_cond_broadcast
是唤醒所有被阻塞的线程
生产者消费者 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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> struct Node { int number; struct Node * next ; }; pthread_cond_t cond;pthread_mutex_t mutex;struct Node * head = NULL ;void * producer (void * arg) { while (1 ) { pthread_mutex_lock(&mutex); struct Node * newnode = (struct Node*)malloc (sizeof (struct Node)); newnode->number = rand() % 1000 ; newnode->next = head; head = newnode; printf ("+++producer, number = %d, tid = %ld\n" , newnode->number, pthread_self()); pthread_mutex_unlock(&mutex); pthread_cond_broadcast(&cond); usleep(rand() % 5000 ); } return NULL ; } void * consumer (void * arg) { while (1 ) { pthread_mutex_lock(&mutex); while (head == NULL ) { pthread_cond_wait(&cond, &mutex); } struct Node * pnode = head; printf ("--consumer: number: %d, tid = %ld\n" , pnode->number, pthread_self()); head = pnode->next; free (pnode); pthread_mutex_unlock(&mutex); usleep(rand() % 5000 ); } return NULL ; } int main () { pthread_cond_init(&cond, NULL ); pthread_mutex_init(&mutex, NULL ); pthread_t ptid[5 ]; pthread_t ctid[5 ]; for (int i = 0 ; i < 5 ; ++i) { pthread_create(&ptid[i], NULL , producer, NULL ); } for (int i = 0 ; i < 5 ; ++i) { pthread_create(&ctid[i], NULL , consumer, NULL ); } for (int i = 0 ; i < 5 ; ++i) { pthread_join(ptid[i], NULL ); pthread_join(ctid[i], NULL ); } pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); return 0 ; }
注意消费者的条件变量那里不能使用 if(head == NULL)
由于生产者调用的 pthread_cond_broadcast(&cond);
唤醒了所有线程,此时所有线程都竞争抢锁的控制权
就会存在一个锁加锁成功,删除节点成功,另一个锁在这之后加锁成功,但是删除节点的时候删除了空节点,报段错误
因为换成 if
后,当前线程被唤醒,重新获得锁后,就接着 pthread_cond_wait
之后执行语句,也就是 if
之后的语句了,就有 bug
但是使用 while
就不同了,执行后面的语句,下一步就是判断循环条件成立与否了,就能避免段错误,而 if
执行体的下一步是跳出 if
语句
信号量 函数 1 2 3 4 5 6 7 8 #include <semaphore.h> sem_t sem;int sem_init (sem_t *sem, int pshared, unsigned int value) ;int sem_destroy (sem_t *sem) ;
sem
:信号量变量地址
pshared
:
value
:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了
1 2 3 int sem_wait (sem_t *sem) ;
当线程调用这个函数,并且 sem
中的资源数 >0
,线程不会阻塞,线程会占用 sem
中的一个资源,因此 资源数-1
,直到 sem
中的资源数减为 0
时,资源被耗尽,因此线程也就被阻塞了
1 2 3 4 int sem_trywait (sem_t *sem) ;
1 2 3 4 5 6 7 8 9 struct timespec { time_t tv_sec; long tv_nsec; }; int sem_timedwait (sem_t *sem, const struct timespec *abs_timeout) ;
1 2 int sem_post (sem_t *sem) ;
1 2 3 int sem_getvalue (sem_t *sem, int *sval) ;
生产者消费者 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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <semaphore.h> #include <pthread.h> struct Node { int number; struct Node * next ; }; sem_t psem;sem_t csem;pthread_mutex_t mutex;struct Node * head = NULL ;void * producer (void * arg) { while (1 ) { sem_wait(&psem); pthread_mutex_lock(&mutex); struct Node * newnode = (struct Node*)malloc (sizeof (struct Node)); newnode->number = rand() % 1000 ; newnode->next = head; head = newnode; printf ("+++producer, number = %d, tid = %ld\n" , newnode->number, pthread_self()); pthread_mutex_unlock(&mutex); sem_post(&csem); usleep(rand() % 5000 ); } return NULL ; } void * consumer (void * arg) { while (1 ) { sem_wait(&csem); pthread_mutex_lock(&mutex); struct Node * pnode = head; printf ("--consumer: number: %d, tid = %ld\n" , pnode->number, pthread_self()); head = pnode->next; free (pnode); pthread_mutex_unlock(&mutex); sem_post(&psem); usleep(rand() % 5000 ); } return NULL ; } int main () { sem_init(&psem, 0 , 5 ); sem_init(&csem, 0 , 0 ); pthread_mutex_init(&mutex, NULL ); pthread_t ptid[5 ]; pthread_t ctid[5 ]; for (int i = 0 ; i < 5 ; ++i) { pthread_create(&ptid[i], NULL , producer, NULL ); } for (int i = 0 ; i < 5 ; ++i) { pthread_create(&ctid[i], NULL , consumer, NULL ); } for (int i = 0 ; i < 5 ; ++i) { pthread_join(ptid[i], NULL ); pthread_join(ctid[i], NULL ); } sem_destroy(&psem); sem_destroy(&csem); pthread_mutex_destroy(&mutex); return 0 ; }
1 2 3 4 5 6 7 sem_wait(&csem); pthread_mutex_lock(&mutex); sem_wait(&csem); pthread_mutex_lock(&mutex);
说明:参考 https://subingwen.cn/
xingzhu
keep trying!keep doing!believe in yourself!
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 星竹 !