0%

多线程编程

1. 线程的标识 pthread_t

对于每一个进程都有唯一PID号与之对应,而对于线程而言,也有类似的tid号,即一个pthread_t类型的变量。线程号是表示线程的唯一标识,但是对于线程号而言,其仅仅在其所属的进程上下文中才有意义。

获取当前线程的线程号:

1
2
#include <pthread.h> 
pthread_t pthread_self(void);

2. 线程的创建

1
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
  • 该函数第一个参数为 pthread_t 指针,用来保存新建线程的线程号;
  • 第二个参数表示了线程的属性,一般传入 NULL 表示默认属性;
  • 第三个参数是一个函数指针,就是线程执行的函数。这个函数返回值为 void*, 形参为 void*。
  • 第四个参数则表示为向线程处理函数传入的参数,若不传入,可用 NULL 填充

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

static void* my_thread_func(void* arg)
{
printf("pthread_New = %lu\n",(unsigned long)pthread_self());//打印线程的 tid 号
}

int main(int argc, char *argv[])
{
pthread_t tid;
int iRet;

iRet = pthread_create(&tid, NULL, my_thread_func, NULL);
if(iRet)
{
printf("pthread_create err!\n");
return -1;
}
printf("tid_main = %lu tid_new = %lu \n",(unsigned long)pthread_self(),(unsigned long)tid);
sleep(1);
return 0;
}

分配资源是以进程为单位的,调度是以线程为单位的,上述代码中主线程和创建出的线程执行的顺序是随机调度的

2.1 向线程传入参数

2.1.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
#include <stdio.h> 
#include <unistd.h>
#include <pthread.h>

static void* func1(void* arg)
{
while(1)
{
printf("%s:a = %d Addr = %p \n", __FUNCTION__, *(int *)arg, arg);
sleep(1);
}
}
static void* func2(void* arg)
{
while(1)
{
printf("%s:a = %ld Addr = %p \n", __FUNCTION__, (long)arg, arg);
sleep(1);
}
}

int main(int argc, char **argv)
{
pthread_t tid1, tid2;
int a = 5, iRet;
iRet = pthread_create(&tid1, NULL, func1, (void *)&a);
if(iRet != 0)
{
printf("Error: pthread_create\n");
return -1;
}
iRet = pthread_create(&tid2, NULL, func2, (void *)(long)a);
if(iRet != 0)
{
printf("Error: pthread_create\n");
return -1;
}
while (1)
{
a++;
sleep(1);
printf("%s:a = %d Addr = %p \n", __FUNCTION__, a, &a);
}
return 0;
}
  • func1对应地址传递,比较常用好理解
  • func2对应值传递:
    • pthread_create(&tid2, NULL, func2, (void *)(long)a);:先将a转成long类型(8byte),则指针void *arg的值为a=5,即指针指向地址5
    • printf("%s:a = %d Addr = %p \n", __FUNCTION__, (int)(long)arg, arg);:指针转换为long类型,值为5。

因此,不难理解,部分输出如下:

1
2
3
4
5
func1:a = 5 Addr = 0x7fff2dcd2eb0 
func2:a = 5 Addr = 0x5
main:a = 6 Addr = 0x7fff2dcd2eb0
func1:a = 7 Addr = 0x7fff2dcd2eb0
func2:a = 5 Addr = 0x5

2.2.2 多参数/结构体传递

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
#include <stdio.h> 
#include <unistd.h>
#include <pthread.h>

struct Stu{
int iId;
char cName[32];
float fMark;
};
static void* func1(void* arg)
{
struct Stu *tmp = (struct Stu*)arg;
printf("%s:Id = %d Name = %s Mark = %.2f\n", __FUNCTION__, tmp->iId, tmp->cName, tmp->fMark);
}

int main(int argc, char **argv)
{
pthread_t tid;
int iRet;
struct Stu stu = {1000, "lili", 55};


iRet = pthread_create(&tid, NULL, func1, (void *)&stu);
if(iRet != 0)
{
printf("Error: pthread_create\n");
return -1;
}
printf("%s:Id = %d Name = %s Mark = %.2f\n", __FUNCTION__, stu.iId, stu.cName, stu.fMark);
sleep(1);
return 0;
}

3. 线程的退出与回收

线程的退出情况有三种:

  1. 第一种是进程结束,进程中所有的线程也会随之结束。

  2. 第二种是通过函数 pthread_exit 来主动的退出线程。

  3. 第三种被其他线程调用 pthread_cancel 来被动退出。

当线程结束后,主线程可以通过函数 pthread_join/pthread_tryjoin_np 来回收线程的资源,并且获得线程结束后需要返回的数据。

3.1 线程退出

(1) 线程主动退出

1
void pthread_exit(void *retval);

pthread_exit 函数为线程退出函数,在退出时候可以传递一个 void*类型的数据带给主线程,若选择不传出数据,可将参数填充为 NULL。

(2) 线程被动退出

1
int pthread_cancel(pthread_t thread);

该函数传入一个 tid 号,会强制退出该 tid 所指向的线程,若成功执行会返回 0。

示例代码:

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
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* func1(void *arg)
{
printf("Pthread:1 Come !\n");
while(1)
{
sleep(1);
}
}

void* func2(void *arg)
{
printf("Pthread:2 Come !\n");
pthread_cancel((pthread_t)arg);
pthread_exit(NULL);
}

int main(int argc, char **argv)
{
pthread_t tid[2];
int i, iRet, flag = 0;

iRet = pthread_create(&tid[0], NULL, func1, NULL);
if(iRet)
{
perror("pthread_create");
return -1;
}
iRet = pthread_create(&tid[1], NULL, func2, (void *)tid[0]);
if(iRet)
{
perror("pthread_create");
return -1;
}

while(1)
{
for(i = 0; i < 2; i++)
if(pthread_tryjoin_np(tid[i], NULL) == 0)
{
flag++;
printf("Pthread:%d back !\n", i + 1);
}
if(flag == 2) break;
}
return 0;
}

第一个的线程执行死循环睡眠逻辑,理论上除非进程结束,其永远不会结束,但在第二个线程中调用了 pthread_cancel 函数,相当于向该线程发送一个退出的指令,导致线程被退出,最终资源被非阻塞回收掉。

3.2 线程资源回收

(1) 线程资源回收(阻塞方式)

1
int pthread_join(pthread_t thread, void **retval);

该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后才返回。第一个参数为要回收线程的 tid 号,第二个参数为线程回收后接受线程传出的数据。

(2) 线程资源回收(非阻塞方式)

1
int pthread_tryjoin_np(pthread_t thread, void **retval);

该函数为非阻塞模式回收函数,通过返回值判断是否回收掉线程,成功回收则返回 0,其余参数与 pthread_join 一致。

3.2.3 示例代码

(1) 示例代码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
#include <pthread.h>
#include <stdio.h>

void* func1(void *arg)
{
static int tmp = 0;
printf("%s:a = %d Addr = %p \n", __FUNCTION__, *(int *)arg, arg);
tmp = *(int *) arg + 100;
pthread_exit((void *) &tmp);
}

int main(int argc, char **argv)
{
pthread_t tid;
int iRet, a = 100;
void *vTmp = NULL;

iRet = pthread_create(&tid, NULL, func1, (void *)&a);
if(iRet)
{
perror("pthread_create");
return -1;
}
pthread_join(tid, &vTmp);
printf("%s:Addr = %p Val = %d\n",__FUNCTION__,vTmp,*(int *)vTmp);

return 0;
}

(2) 示例代码2:

通过非阻塞方式回收线程资源

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
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* func1(void *arg)
{
printf("Pthread:%ld Come !\n", (long)arg);
pthread_exit(arg);

}

int main(int argc, char **argv)
{
pthread_t tid[3];
int i, iRet, flag = 0;
void *vTmp = NULL;

for(i = 1; i <= 3; i++)
{
iRet = pthread_create(&tid[i - 1], NULL, func1, (void *)(long)i);
if(iRet)
{
perror("pthread_create");
return -1;
}
}
while(1)
{
for(i = 1; i <= 3; i++)
{
if(pthread_tryjoin_np(tid[i - 1], &vTmp) == 0)
{
flag++;
printf("Pthread:%ld back !\n", (long)vTmp);
}
}
if(flag == 3)
break;
}
return 0;
}

通过阻塞方式回收线程几乎规定了线程回收的顺序,若最先回收的线程未退出,则一直会被阻塞,导致后续先退出的线程无法及时的回收。

通过函数 pthread_tryjoin_np,使用非阻塞回收,线程可以根据退出先后顺序及时进行资源的回收。

4. 线程的控制

4.1 互斥锁

多个线程都要访问某个临界资源,比如某个全局变量时,需要互斥地进行访问。

(1) 初始化互斥量

1
int pthread_mutex_init(phtread_mutex_t *mutex, const pthread_mutexattr_t *restrict attr);

第一个参数是改互斥量指针,第二个参数为控制互斥量的属性,一般为 NULL。当函数成功后会返回 0,代表初始化互斥量成功。

互斥量的初始化也可以调用宏快速初始化:

1
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;

(2) 互斥量加锁/解锁

1
2
3
int pthread_mutex_lock(pthread_mutex_t *mutex);     //阻塞方式
int pthread_mutex_trylock(pthread_mutex_t *mutex); //非阻塞方式
int pthread_mutex_unlock(pthread_mutex_t *mutex);

成功后会返回 0。当某一个线程获得了执行权后,执行 lock 函数,一旦加锁成功后,其余线程遇到 lock 函数时候会发生阻塞,直至获取资源的线程执行 unlock 函数后。 unlock 函数会唤醒其他正在等待互斥量的线程。

(3) 销毁互斥量

1
int pthread_mutex_destory(pthread_mutex_t *mutex);

成功后会返回 0。

(4) 示例

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
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

pthread_mutex_t mutex; // 互斥量一般申请全局变量
int iNum; // 临界变量

void* func1(void *arg)
{
pthread_mutex_lock(&mutex);
while(iNum < 3)
{
iNum++;
printf("%s:iNum = %d \n", __FUNCTION__, iNum);
}
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}

int main(int argc, char **argv)
{
pthread_t tid;
int i, iRet;

iRet = pthread_mutex_init(&mutex, NULL);
if(iRet != 0)
{
perror("pthread_mutex_init");
return -1;
}

iRet = pthread_create(&tid, NULL, func1, NULL);
if(iRet)
{
perror("pthread_create");
return -1;
}

pthread_mutex_lock(&mutex);
while(iNum > -3)
{
iNum--;
printf("%s:iNum = %d \n", __FUNCTION__, iNum);
}
pthread_mutex_unlock(&mutex);
pthread_join(tid, NULL); // 回收线程
pthread_mutex_destroy(&mutex); // 销毁互斥量
return 0;
}

4.2 信号量

信号量跟互斥量不一样,互斥量用来防止多个线程同时访问某个临界资源信号量起通知作用,线程 A 在等待某件事,线程 B 完成了这件事后就可以给线程 A 发信号

(1) 初始化信号量

1
2
#include <semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
  • 该函数可以初始化一个信号量,第一个参数传入 sem_t 类型指针;
  • 第二个参数传入 0 代表线程控制,否则为进程控制;
  • 第三个参数表示信号量的初始值,0 表示没有资源,正数表示资源的个数,不允许为负数。
  • 待初始化结束信号量后,若执行成功会返回 0。

(2) 信号量PV操作

1
2
3
int sem_wait(sem_t *sem);      // 阻塞方式
int sem_trywait(sem_t *sem); // 非阻塞方式
int sem_post(sem_t *sem);
  • sem_wait 函数作用为检测指定信号量是否有资源可用,若无资源可用会阻塞等待,若有资源可用会自动的执行”sem-1”的操作。所谓的”sem-1”是与上述初始化函数中第三个参数值一致,成功执行会返回 0
  • sem_trywait:此函数是信号量申请资源的非阻塞函数,功能与 sem_wait 一致。
  • sem_post 函数会释放指定信号量的资源,执行”sem+1”操作。 通过以上 2 个函数可以完成所谓的 PV 操作,即信号量的申请与释放,完成 对线程执行顺序的控制。

(3) 信号量销毁

1
int sem_destory(sem_t *sem);

该函数为信号量销毁函数,执行过后可将信号量进行销毁。成功时返回0。

(4) 示例代码

通过加入信号量,使得线程的执行顺序变得可控。

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
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>

sem_t sem[3];
void* func1(void *arg)
{
sem_wait(&sem[0]);
printf("%s executed\n", __FUNCTION__);
sem_post(&sem[1]);
pthread_exit(NULL);
}

void* func2(void *arg)
{
sem_wait(&sem[1]);
printf("%s executed\n", __FUNCTION__);
sem_post(&sem[2]);
pthread_exit(NULL);
}

void* func3(void *arg)
{
sem_wait(&sem[2]);
printf("%s executed\n", __FUNCTION__);
pthread_exit(NULL);
}

int main(int argc, char **argv)
{
pthread_t tid[3];
int i, iRet, flag = 0;

sem_init(&sem[0], 0, 1); // 信号量初始化要在线程之前
sem_init(&sem[1], 0, 0);
sem_init(&sem[2], 0, 0);
pthread_create(&tid[0], NULL, func1, NULL);
pthread_create(&tid[1], NULL, func2, NULL);
pthread_create(&tid[2], NULL, func3, NULL);

for (i = 0; i < 3; i++)
{
pthread_join(tid[i], NULL);
sem_destroy(&sem[i]);
}
return 0;
}

4.3 条件变量

条件变量时一种同步机制,用来通知其他线程条件满足了。一般是用来通知对方共享数据的状态信息,因此条件变量时结合互斥量来使用的。

(1) 初始化

1
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
  • 该函数可以初始化一个条件变量,第一个参数传入 pthread_cond_t类型指针;
  • 第二个参数传入控制条件变量的属性,一般为 NULL。
  • 待初始化结束信号量后,若执行成功会返回 0。

可以通过如下宏直接初始化条件变量:

1
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 

(2) wait/signal

1
2
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_wait函数需要结合互斥量一起使用:

1
2
3
4
5
6
7
8
pthread_mutex_lock(&g_tMutex); 
// 如果条件不满足则,会 unlock g_tMutex
// 条件满足后被唤醒,会 lock g_tMutex
pthread_cond_wait(&g_tConVar, &g_tMutex);
/* ----- 操作临界资源 ----- */

/* -------- End -------- */
pthread_mutex_unlock(&g_tMutex);

如果条件满足,则继续往下执行;如果条件不满足,则会释放互斥量,阻塞直到条件满足并且可以获得互斥锁时,继续往下执行。

(3) 销毁条件变量

1
int pthread_cond_destroy(pthread_cond_t *cond);

成功时返回0。

(4) 示例代码

在主线程中从stdin中接收输入,并在创建的线程中进行打印

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
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>

static char g_buf[1000];
static pthread_mutex_t g_tMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_tConVar = PTHREAD_COND_INITIALIZER;

static void *my_thread_func (void *data)
{
while (1)
{
pthread_mutex_lock(&g_tMutex);
pthread_cond_wait(&g_tConVar, &g_tMutex);

printf("recv: %s\n", g_buf);
pthread_mutex_unlock(&g_tMutex);
}

return NULL;
}

int main(int argc, char **argv)
{
pthread_t tid;
int ret;
char buf[1000];

ret = pthread_create(&tid, NULL, my_thread_func, NULL);

while (1)
{
fgets(buf, 1000, stdin);
pthread_mutex_lock(&g_tMutex);
memcpy(g_buf, buf, 1000);
pthread_cond_signal(&g_tConVar); /* 通知接收线程 */
pthread_mutex_unlock(&g_tMutex);
}
return 0;
}

5.总结

5.1 线程使用流程图

image-20231004201228798

5.2 互斥量使用流程图

image-20231004201303646

5.3 信号量使用流程图

image-20231004201343650