多线程入门知识

一些比较基础的知识

先来谈一谈线程安全问题

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
#include <stdio.h>   
#include <pthread.h>
#include <time.h>
#include <windows.h>
#pragma comment(lib, "pthreadVC2.lib")

int temp = 0;
//pthread_mutex_t mut;

void* counter3(void* args) {
int i = temp;
i++;
Sleep(10);
temp = i;
return 0;
}

void* counter4(void* args) {
int i = temp;
i++;
Sleep(10);
temp = i;
return 0;
}

int main() {
//pthread_mutex_init(&mut, NULL);
pthread_t t3;
pthread_t t4;
pthread_create(&t3, NULL, counter3, NULL);
pthread_create(&t4, NULL, counter4, NULL);
Sleep(1000);
printf("%d\n", temp);
getchar();
return 0;
}

示例程序如上

照理说temp这个全局变量应该是1

但是运行结果如下

1

在执行线程1处理函数时,当cpu还没来得及给temp赋值,因为时间片到点cpu进行调度,开始执行线程2处理函数,此时temp还是0,所以最终temp值为1.

此处加sleep函数是为了让cpu来不及给temp赋值

由此产生了一个东西叫做临界区。

临界区

如下

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 <pthread.h>
#include <time.h>
#include <windows.h>
#pragma comment(lib, "pthreadVC2.lib")


CRITICAL_SECTION aaa;
int temp=0;
//pthread_mutex_t mut;

void* counter3(void* args) {
EnterCriticalSection(&aaa);
int i = temp;
i++;
Sleep(10);
temp = i;
LeaveCriticalSection(&aaa);
return 0;
}

void* counter4(void* args) {
EnterCriticalSection(&aaa);
int i = temp;
i++;
Sleep(10);
temp = i;
LeaveCriticalSection(&aaa);
return 0;
}

int main() {
InitializeCriticalSection(&aaa);
//pthread_mutex_init(&mut, NULL);
pthread_t t3;
pthread_t t4;
pthread_create(&t3, NULL, counter3, NULL);
pthread_create(&t4, NULL, counter4, NULL);
Sleep(1000);
printf("%d", temp);
getchar();
DeleteCriticalSection(&aaa);
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
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>   
#include <pthread.h>
#include <time.h>
#include <windows.h>
#pragma comment(lib, "pthreadVC2.lib")


CRITICAL_SECTION aaa;
CRITICAL_SECTION bbb;
int temp=0;
//pthread_mutex_t mut;

void* counter3(void* args) {
EnterCriticalSection(&aaa);
Sleep(10);
EnterCriticalSection(&bbb);
printf("done");
int i = temp;
i++;
temp = i;
LeaveCriticalSection(&bbb);
LeaveCriticalSection(&aaa);
return 0;
}

void* counter4(void* args) {
EnterCriticalSection(&bbb);
Sleep(10);
EnterCriticalSection(&aaa);
printf("done");
int i = temp;
i++;
temp = i;
LeaveCriticalSection(&aaa);
LeaveCriticalSection(&bbb);
return 0;
}

int main() {
InitializeCriticalSection(&aaa);
InitializeCriticalSection(&bbb);
//pthread_mutex_init(&mut, NULL);
pthread_t t3;
pthread_t t4;
pthread_create(&t3, NULL, counter3, NULL);
pthread_create(&t4, NULL, counter4, NULL);
Sleep(1000*60);
getchar();
DeleteCriticalSection(&aaa);
DeleteCriticalSection(&bbb);
return 0;
}

程序始终不会打印出done
最简单的处理方法就是获取锁的顺序一样。

1
2
3
4
WaitForSingleObject  (
HANDLE hHandle, // handle to object
DWORD dwMilliseconds // time-out interval
);

个人理解为获取,如何获取分俩种情况一种是内核对象编程已触发,还有一种是时间到点。
hHandle为内核句柄(如线程进程)
dwMilliseconds为等待时间,单位是毫秒 INFINITE(-1)一直等待
返回值:
WAIT_OBJECT_0(0) 等待对象变为已通知
WAIT_TIMEOUT(0x102) 超时
特别说明:
1、内核对象中的每种对象都可以说是处于已通知或未通知的状态之中
2、这种状态的切换是由Microsoft为每个对象建立的一套规则来决定的
3、当线程正在运行的时候,线程内核对象处于未通知状态
4、当线程终止运行的时候,它就变为已通知状态
5、在内核中就是个BOOL值,运行时FALSE 结束TRUE

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
#include <stdio.h>   
#include <pthread.h>
#include <time.h>
#include <windows.h>
#pragma comment(lib, "pthreadVC2.lib")


DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
for (int i = 0; i < 5; i++)
{
printf("+++++++++\n");
Sleep(1000);
}
return 0;
}

int main(int argc, char* argv[])
{
HANDLE hThread1 = ::CreateThread(NULL, 0, ThreadProc1,
NULL, 0, NULL);
DWORD dwCode = ::WaitForSingleObject(hThread1, 2000);
MessageBox(0, 0, 0, 0);

return 0;
}

DWORD WaitForMultipleObjects(
DWORD nCount, // number of handles in array
CONST HANDLE *lpHandles, // object-handle array
BOOL bWaitAll, // wait option
DWORD dwMilliseconds // time-out interval
);

nCount为线程数目
lpHandles为线程句柄的数组
bWaitAll表示是否等待,false时只要一个线程执行完毕即结束,true时要等俩个线程都结束
dwMilliseconds表示等待时间,和WaitForSingleObject一样

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 <stdio.h>   
#include <pthread.h>
#include <time.h>
#include <windows.h>
#pragma comment(lib, "pthreadVC2.lib")

DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
for (int i = 0; i < 5; i++)
{
printf("+++++++++\n");
Sleep(1000);
}
return 0;
}

DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
for (int i = 0; i < 10; i++)
{
printf("---------\n");
Sleep(1000);
}
return 0;
}


int main(int argc, char* argv[])
{
HANDLE hArray[2];
//创建一个新的线程
HANDLE hThread1 = ::CreateThread(NULL, 0, ThreadProc1,
NULL, 0, NULL);
//创建一个新的线程
HANDLE hThread2 = ::CreateThread(NULL, 0, ThreadProc2,
NULL, 0, NULL);
hArray[0] = hThread1;
hArray[1] = hThread2;
DWORD dwCode = ::WaitForMultipleObjects(2, hArray, TRUE, INFINITE);
MessageBox(0, 0, 0, 0);
return 0;
}

互斥量

互斥量也是一个内核对象

HANDLE CreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes,
BOOLbInitialOwner,
LPCTSTRlpName
);
第一个参数表示安全控制,一般直接传入NULL。
第二个参数用来确定互斥量的初始拥有者。如果传入TRUE表示互斥量对象内部会记录创建它的线程的线程ID号并将递归计数设置为1,表示被占用,由于该线程ID非零,所以互斥量处于未触发状态。如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态。
第三个参数用来设置互斥量的名称,在多个进程中的线程就是通过名称来确保它们访问的是同一个互斥量。
函数返回值:成功返回一个表示互斥量的句柄,失败返回NULL。
个人理解:id号的递归计数表示是否占用,1表示占用也就是运行中,0表示已完成,而是否触发,触发可以理解为完成,已触发表示完成,未触发表示未完成。

HANDLE OpenMutex(
DWORDdwDesiredAccess,
BOOLbInheritHandle,
LPCTSTRlpName
);
第一个参数表示权限,对互斥量一般传入MUTEX_ALL_ACCESS。
第二个参数表示互斥量句柄继承性,一般传入TRUE即可。
第三个参数表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。
函数返回值:成功返回一个表示互斥量的句柄,失败返回NULL。

BOOL ReleaseMutex (HANDLEhMutex)表示触发互斥量
传入参数为从创建或打开互斥量时返回的句柄。 访问互斥资源前应该要调用等待函数,结束访问时就要调用ReleaseMutex()来表示自己已经结束访问,其它线程可以开始访问了。
由于互斥量是一个内核对象,释放时直接调用 CloseHandle(HANDLE hObject) 函数就可以了,所有内核对象都是这样释放。

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
#include <stdio.h>   
#include <pthread.h>
#include <time.h>
#include <windows.h>
#pragma comment(lib, "pthreadVC2.lib")

const char MutexName[] = "MyMutex"; //互斥量名字

int main()
{
HANDLE hMutex = OpenMutexA(MUTEX_ALL_ACCESS, TRUE, MutexName); //打开互斥量

if (NULL != hMutex)
{
printf("打开互斥量成功,等待互斥量被触发\n");
WaitForSingleObject(hMutex, INFINITE); // 等待互斥量被触发
printf("互斥量已经被触发\n");
}
else
{
printf("打开互斥量失败。\n");
}


CloseHandle(hMutex);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>   
#include <pthread.h>
#include <time.h>
#include <windows.h>
#pragma comment(lib, "pthreadVC2.lib")

const char MutexName[] = "MyMutex"; //互斥量名字

int main()
{
HANDLE hMutex = CreateMutexA(NULL, TRUE, MutexName); //创建互斥量并初始化为未触发状态
printf("互斥量已经创建,按任意键触发\n");
getch();

ReleaseMutex(hMutex); // 触发互斥量
printf("互斥量已经被触发\n");
CloseHandle(hMutex);
return 0;
}

这边只需要注意windows宽字符问题,ASCII就用A,Unicode用W即可

内核对象

waitforsingleobject
俩种方法,俩个返回值
当返回值为0时说明对象变为已通知
当返回值为0x102时说明是因为超时而继续执行
waitformuity
返回值为0不一定是说明等待对象全部已通知
具体看参数
如果是true那么正确
否则返回值为已通知的下标
createevent
第二个参数当true时调用时即修改状态
False时需要自己写函数去修改
第三个参数是表明建立时状态
True说明刚建立就是已通知状态
false相反
事件实现线程同步
俩个事件 初始状态不同
俩个线程分别等待俩个事件
分别修改不同的事件状态即可

信号量

内核对象
用于实现线程同步
第一个参数与其他内核对象一样都是表示安全控制
第二个参数表示初始资源数量
第三个参数表示最大并发数量
第四个参数是信号量名称
当初始资源数量大于等于1时才会发送信号
0时不发送信号
并且值要小于等于最大并发数量

进行线程同步时可以设置初始值
每次wait到都会减1

线程互斥 初始化 进入 离开 销毁
互斥体 initialize-criticalsection enter。。。 leave。。。 delete。。。
临界区 createmutex waitfor releasemutex closehandle

线程同步 创建 进入触发 进入未触发 销毁
事件 createevent setevent resetevent closehandle
创建 递减计数 递增计数 销毁
信号量 createsemaphore waitfor releasesemaphore closehandle

进程:
系统启动会创建一个进程explorer.exe也就是桌面进程
所有双击运行的程序全是该进程的子进程
createprocess
用户层调用时内核层 ntcreateprocess
会生成句柄表
成员有句柄和真实地址
因为安全问题不会吧真实地址返回,只会返回一个句柄,当使用时会根据句柄去寻找对应的地址
第三个成员表示是否可以继承

参数
第一个参数是常量字符串,表示进程路径

文章目录
  1. 1. 临界区
  2. 2. 互斥量
  • 内核对象
  • |