线程的简单使用

Last updated on 3 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即可

线程终止

终止有三种方式

  • 运行完线程代码return 结束

  • 被其他线程中终止

  • 线程可以调用 pthread_exit ( 自己终结自己 )

    pthread_exit() 可以携带返回值

如果需要 现有线程的返回值怎么办? 有一个函数可以做到

1
2
3
4
5
6
7
8
/* Make calling thread wait for termination of the thread TH.  The
exit status of the thread is stored in *THREAD_RETURN, if THREAD_RETURN
is not NULL.
让调用线程等待线程TH的终止。线程的退出状态存储在*THREAD_RETURN中
This function is a cancellation point and therefore not marked with
__THROW. */
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;
}

image-20220326154257729

线程结束后可以得到返回值,返回值分别是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 是一个结构体
pthread_mutex_t __mutex;
//init
extern int pthread_mutex_init (pthread_mutex_t *__mutex,
const pthread_mutexattr_t *__mutexattr)
__THROW __nonnull ((1));

/* Destroy a mutex. */
extern int pthread_mutex_destroy (pthread_mutex_t *__mutex)
__THROW __nonnull ((1));

/* Try locking a mutex. */
extern int pthread_mutex_trylock (pthread_mutex_t *__mutex)
__THROWNL __nonnull ((1));

/* Lock a mutex. */
extern int pthread_mutex_lock (pthread_mutex_t *__mutex)
__THROWNL __nonnull ((1));

/* Unlock a mutex. */
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);
// pthread_create(&pid[2],NULL,fun3,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;
}

image-20220327204645817 plock1 和 plock2都 已经上锁,两个线程都在等待对方释放锁,才能进行下去,这种情况下双方都不会释放

读写锁( 共享互斥锁)

写操作的时候才会互斥,而在进行读的时候是可以共享的进行访问临界区的, 在写少读多的情况下用读写锁更好,为什么呢? 写的时候临界区加锁,这没毛病,读的时候并不需要进修改数据,多线程读数据并不影响数据内容,所以读的时候不用锁共享资源,也节省了系统的开销

读写锁的分配规则:

  1. 只有线程没有使用读写锁的写转态,那么任意数目的线程都可以使用读写锁
  2. 当有线程持有读写锁的写转态时,其他线程使用读写锁都会被阻塞
  3. 如果读写锁的读模式加锁时,所有线程以读模式它进行加锁的线程都可以得到访问权

做了实验 使用读写锁,一个线程写数据,两个线程读数据

写的时候,读线程全部被阻塞
读的时候,读线程全都可以获取资源,写线程被阻塞

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(先留坑)

。。。。。。。。。。。。。。