The workflow is quite different. One can do for and while loops in compute shaders but in this case they are not needed (and would perform much worse than C# side script if all is done in one thread). What you need to do instead is dispatch enough groups/threads to process each pixel once. Then the problem just becomes the game of finding the texture pixel index that corresponds to the thread index and reading and modifying that pixel. There are countless ways you could do the indexing and probably doesn't matter too much, you just have to have some 1 to 1 match between the threads and the pixels. The easiest would be to have something like 8x8x1 threads in a group and then having (width/8)x(height/8)x1 groups so the indexing becomes trivial. (each SV_Dispatch_ThreadID would directly match a pixel ID)
In the shader code, you can directly access the pixel with something like texture[id] = whatever where id is a uint2 (which can be directly taken from the SV_Dispatch_ThreadID) and texture is RWTexture2D. With compute shaders you will access the the pixels with the 2D integer index rather than UV coordinates which you would use with regular shaders.
Note that if the size of the texture is not divisible by the number of threads in a group (8 in both directions in the given 8x8x1 example), you need to ceil the division result which means you will have couple extra threads executing beyond the size of the texture. I don't remember for sure if you necessarily need to take that into account but safer would be to do a bound check in the shader code to not try to access pixels outside the bounds.
In case the threads and groups are confusing, this visualization may help https://www.reddit.com/r/Unity3D/comments/1eywb95/a_visual_guide_to_the_structure_of_compute_shaders/. I think that very clearly visualizes what each ID means at least. Feel free to ask for more info if anything is unclear, my rambling is probably all over the places at this time of day.